audio_device.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. package usbcapture
  2. import (
  3. "bufio"
  4. "fmt"
  5. "log"
  6. "net/http"
  7. "os"
  8. "os/exec"
  9. "regexp"
  10. "strings"
  11. "github.com/gorilla/websocket"
  12. )
  13. // upgrader is used to upgrade HTTP connections to WebSocket connections
  14. var upgrader = websocket.Upgrader{
  15. ReadBufferSize: 1024,
  16. WriteBufferSize: 1024,
  17. CheckOrigin: func(r *http.Request) bool {
  18. return true
  19. },
  20. }
  21. // ListCaptureDevices lists all available audio capture devices in the /dev/snd directory.
  22. func ListCaptureDevices() ([]string, error) {
  23. files, err := os.ReadDir("/dev/snd")
  24. if err != nil {
  25. return nil, fmt.Errorf("failed to read /dev/snd: %w", err)
  26. }
  27. var captureDevs []string
  28. for _, file := range files {
  29. name := file.Name()
  30. if strings.HasPrefix(name, "pcm") && strings.HasSuffix(name, "c") {
  31. fullPath := "/dev/snd/" + name
  32. captureDevs = append(captureDevs, fullPath)
  33. }
  34. }
  35. return captureDevs, nil
  36. }
  37. // FindHDMICaptureCard searches for an HDMI capture card using the `arecord -l` command.
  38. func FindHDMICapturePCMPath() (string, error) {
  39. out, err := exec.Command("arecord", "-l").Output()
  40. if err != nil {
  41. return "", fmt.Errorf("arecord -l failed: %w", err)
  42. }
  43. lines := strings.Split(string(out), "\n")
  44. for _, line := range lines {
  45. lower := strings.ToLower(line)
  46. if strings.Contains(lower, "ms2109") || strings.Contains(lower, "ms2130") {
  47. // Example line:
  48. // card 1: MS2109 [MS2109], device 0: USB Audio [USB Audio]
  49. parts := strings.Fields(line)
  50. var cardNum, devNum string
  51. for i := range parts {
  52. if parts[i] == "card" && i+1 < len(parts) {
  53. cardNum = parts[i+1][:1] // "1"
  54. }
  55. if parts[i] == "device" && i+1 < len(parts) {
  56. devNum = strings.TrimSuffix(parts[i+1], ":") // "0"
  57. }
  58. }
  59. if cardNum != "" && devNum != "" {
  60. return fmt.Sprintf("/dev/snd/pcmC%vD%vc", cardNum, devNum), nil
  61. }
  62. }
  63. }
  64. return "", fmt.Errorf("no HDMI capture card found")
  65. }
  66. // Convert a PCM device name to a hardware device name.
  67. // Example: "pcmC1D0c" -> "hw:1,0"
  68. func pcmDeviceToHW(dev string) (string, error) {
  69. // Regex to extract card and device numbers
  70. re := regexp.MustCompile(`pcmC(\d+)D(\d+)[cp]`)
  71. matches := re.FindStringSubmatch(dev)
  72. if len(matches) < 3 {
  73. return "", fmt.Errorf("invalid device format")
  74. }
  75. card := matches[1]
  76. device := matches[2]
  77. return fmt.Sprintf("hw:%s,%s", card, device), nil
  78. }
  79. func GetDefaultAudioConfig() *AudioConfig {
  80. return &AudioConfig{
  81. SampleRate: 48000,
  82. Channels: 2,
  83. BytesPerSample: 2, // 16-bit
  84. FrameSize: 1920, // 1920 samples per frame = 40ms @ 48kHz
  85. }
  86. }
  87. // AudioStreamingHandler handles incoming WebSocket connections for audio streaming.
  88. func (i Instance) AudioStreamingHandler(w http.ResponseWriter, r *http.Request) {
  89. conn, err := upgrader.Upgrade(w, r, nil)
  90. if err != nil {
  91. log.Println("Failed to upgrade to websocket:", err)
  92. return
  93. }
  94. defer conn.Close()
  95. if i.audiostopchan != nil {
  96. //Another instance already running
  97. log.Println("Audio pipe already running, stopping previous instance")
  98. i.audiostopchan <- true
  99. }
  100. //Get the capture card audio input
  101. pcmdev, err := FindHDMICapturePCMPath()
  102. if err != nil {
  103. log.Println("Failed to find HDMI capture PCM path:", err)
  104. http.Error(w, "Internal Server Error", http.StatusInternalServerError)
  105. return
  106. }
  107. log.Println("Found HDMI capture PCM path:", pcmdev)
  108. // Convert PCM device to hardware device name
  109. hwdev, err := pcmDeviceToHW(pcmdev)
  110. if err != nil {
  111. log.Println("Failed to convert PCM device to hardware device:", err)
  112. http.Error(w, "Internal Server Error", http.StatusInternalServerError)
  113. return
  114. }
  115. log.Println("Using hardware device:", hwdev)
  116. // Create a buffered reader to read audio data
  117. i.audiostopchan = make(chan bool, 1)
  118. log.Println("Starting audio pipe with arecord...")
  119. // Start arecord with 48kHz, 16-bit, stereo
  120. cmd := exec.Command("arecord",
  121. "-f", "S16_LE", // Format: 16-bit little-endian
  122. "-r", fmt.Sprint(i.Config.AudioConfig.SampleRate),
  123. "-c", fmt.Sprint(i.Config.AudioConfig.Channels),
  124. "-D", hwdev, // Use the hardware device
  125. )
  126. stdout, err := cmd.StdoutPipe()
  127. if err != nil {
  128. log.Println("Failed to get arecord stdout pipe:", err)
  129. http.Error(w, "Internal Server Error", http.StatusInternalServerError)
  130. return
  131. }
  132. if err := cmd.Start(); err != nil {
  133. log.Println("Failed to start arecord:", err)
  134. http.Error(w, "Internal Server Error", http.StatusInternalServerError)
  135. return
  136. }
  137. reader := bufio.NewReader(stdout)
  138. bufferSize := i.Config.AudioConfig.FrameSize * i.Config.AudioConfig.Channels * i.Config.AudioConfig.BytesPerSample
  139. log.Printf("Buffer size: %d bytes (FrameSize: %d, Channels: %d, BytesPerSample: %d)",
  140. bufferSize, i.Config.AudioConfig.FrameSize, i.Config.AudioConfig.Channels, i.Config.AudioConfig.BytesPerSample)
  141. buf := make([]byte, bufferSize*2)
  142. // Start a goroutine to handle WebSocket messages
  143. log.Println("Listening for WebSocket messages...")
  144. go func() {
  145. _, msg, err := conn.ReadMessage()
  146. if err == nil {
  147. if string(msg) == "exit" {
  148. log.Println("Received exit command from client")
  149. i.audiostopchan <- true // Signal to stop the audio pipe
  150. return
  151. }
  152. }
  153. }()
  154. log.Println("Starting audio capture loop...")
  155. for {
  156. select {
  157. case <-i.audiostopchan:
  158. log.Println("Audio pipe stopped")
  159. goto DONE
  160. default:
  161. n, err := reader.Read(buf)
  162. if err != nil {
  163. log.Println("Read error:", err)
  164. if i.audiostopchan != nil {
  165. i.audiostopchan <- true // Signal to stop the audio pipe
  166. }
  167. goto DONE
  168. }
  169. if n == 0 {
  170. continue
  171. }
  172. //log.Println("Read bytes:", n, "size of buffer:", len(buf))
  173. //Send only the bytes read to WebSocket
  174. err = conn.WriteMessage(websocket.BinaryMessage, buf[:n])
  175. if err != nil {
  176. log.Println("WebSocket send error:", err)
  177. goto DONE
  178. }
  179. }
  180. }
  181. DONE:
  182. i.audiostopchan <- true // Signal to stop the audio pipe
  183. cmd.Process.Kill()
  184. log.Println("Audio pipe finished")
  185. }