audio_device.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. package usbcapture
  2. import (
  3. "bufio"
  4. "fmt"
  5. "log"
  6. "net/http"
  7. "os"
  8. "os/exec"
  9. "regexp"
  10. "strings"
  11. "syscall"
  12. "time"
  13. "github.com/gorilla/websocket"
  14. )
  15. // upgrader is used to upgrade HTTP connections to WebSocket connections
  16. var upgrader = websocket.Upgrader{
  17. ReadBufferSize: 1024,
  18. WriteBufferSize: 1024,
  19. CheckOrigin: func(r *http.Request) bool {
  20. return true
  21. },
  22. }
  23. // ListCaptureDevices lists all available audio capture devices in the /dev/snd directory.
  24. func ListCaptureDevices() ([]string, error) {
  25. files, err := os.ReadDir("/dev/snd")
  26. if err != nil {
  27. return nil, fmt.Errorf("failed to read /dev/snd: %w", err)
  28. }
  29. var captureDevs []string
  30. for _, file := range files {
  31. name := file.Name()
  32. if strings.HasPrefix(name, "pcm") && strings.HasSuffix(name, "c") {
  33. fullPath := "/dev/snd/" + name
  34. captureDevs = append(captureDevs, fullPath)
  35. }
  36. }
  37. return captureDevs, nil
  38. }
  39. // FindHDMICaptureCard searches for an HDMI capture card using the `arecord -l` command.
  40. func FindHDMICapturePCMPath() (string, error) {
  41. out, err := exec.Command("arecord", "-l").Output()
  42. if err != nil {
  43. return "", fmt.Errorf("arecord -l failed: %w", err)
  44. }
  45. lines := strings.Split(string(out), "\n")
  46. for _, line := range lines {
  47. lower := strings.ToLower(line)
  48. if strings.Contains(lower, "ms2109") || strings.Contains(lower, "ms2130") {
  49. // Example line:
  50. // card 1: MS2109 [MS2109], device 0: USB Audio [USB Audio]
  51. parts := strings.Fields(line)
  52. var cardNum, devNum string
  53. for i := range parts {
  54. if parts[i] == "card" && i+1 < len(parts) {
  55. cardNum = parts[i+1][:1] // "1"
  56. }
  57. if parts[i] == "device" && i+1 < len(parts) {
  58. devNum = strings.TrimSuffix(parts[i+1], ":") // "0"
  59. }
  60. }
  61. if cardNum != "" && devNum != "" {
  62. return fmt.Sprintf("/dev/snd/pcmC%vD%vc", cardNum, devNum), nil
  63. }
  64. }
  65. }
  66. return "", fmt.Errorf("no HDMI capture card found")
  67. }
  68. // Convert a PCM device name to a hardware device name.
  69. // Example: "pcmC1D0c" -> "hw:1,0"
  70. func pcmDeviceToHW(dev string) (string, error) {
  71. // Regex to extract card and device numbers
  72. re := regexp.MustCompile(`pcmC(\d+)D(\d+)[cp]`)
  73. matches := re.FindStringSubmatch(dev)
  74. if len(matches) < 3 {
  75. return "", fmt.Errorf("invalid device format")
  76. }
  77. card := matches[1]
  78. device := matches[2]
  79. return fmt.Sprintf("hw:%s,%s", card, device), nil
  80. }
  81. func GetDefaultAudioConfig() *AudioConfig {
  82. return &AudioConfig{
  83. SampleRate: 48000,
  84. Channels: 2,
  85. BytesPerSample: 2, // 16-bit
  86. FrameSize: 1920, // 1920 samples per frame = 40ms @ 48kHz
  87. }
  88. }
  89. func GetDefaultAudioDevice() string {
  90. //Check if the default ALSA device exists
  91. if _, err := os.Stat("/dev/snd/pcmC0D0c"); err == nil {
  92. return "/dev/snd/pcmC0D0c"
  93. }
  94. //If not, list all capture devices and return the first one
  95. devs, err := ListCaptureDevices()
  96. if err != nil || len(devs) == 0 {
  97. return ""
  98. }
  99. return devs[0]
  100. }
  101. // AudioStreamingHandler handles incoming WebSocket connections for audio streaming.
  102. func (i *Instance) AudioStreamingHandler(w http.ResponseWriter, r *http.Request) {
  103. // Check if the request contains ?quality=low
  104. quality := r.URL.Query().Get("quality")
  105. qualityKey := []string{"low", "standard", "high"}
  106. selectedQuality := "standard"
  107. for _, q := range qualityKey {
  108. if quality == q {
  109. selectedQuality = q
  110. break
  111. }
  112. }
  113. conn, err := upgrader.Upgrade(w, r, nil)
  114. if err != nil {
  115. log.Println("Failed to upgrade to websocket:", err)
  116. return
  117. }
  118. defer conn.Close()
  119. if alsa_device_occupied(i.Config.AudioDeviceName) {
  120. //Another instance already running
  121. log.Println("Audio pipe already running, stopping previous instance")
  122. i.audiostopchan <- true
  123. retryCounter := 0
  124. for alsa_device_occupied(i.Config.AudioDeviceName) {
  125. time.Sleep(500 * time.Millisecond) //Wait a bit for the previous instance to stop
  126. retryCounter++
  127. if retryCounter > 5 {
  128. log.Println("Failed to stop previous audio instance")
  129. return
  130. }
  131. }
  132. }
  133. //Get the capture card audio input
  134. pcmdev, err := FindHDMICapturePCMPath()
  135. if err != nil {
  136. log.Println("Failed to find HDMI capture PCM path:", err)
  137. http.Error(w, "Internal Server Error", http.StatusInternalServerError)
  138. return
  139. }
  140. log.Println("Found HDMI capture PCM path:", pcmdev)
  141. // Convert PCM device to hardware device name
  142. hwdev, err := pcmDeviceToHW(pcmdev)
  143. if err != nil {
  144. log.Println("Failed to convert PCM device to hardware device:", err)
  145. http.Error(w, "Internal Server Error", http.StatusInternalServerError)
  146. return
  147. }
  148. log.Println("Using hardware device:", hwdev)
  149. // Create a buffered reader to read audio data
  150. log.Println("Starting audio pipe with arecord...")
  151. // Start arecord with 48kHz, 16-bit, stereo
  152. cmd := exec.Command("arecord",
  153. "-f", "S16_LE", // Format: 16-bit little-endian
  154. "-r", fmt.Sprint(i.Config.AudioConfig.SampleRate),
  155. "-c", fmt.Sprint(i.Config.AudioConfig.Channels),
  156. "-D", hwdev, // Use the hardware device
  157. )
  158. stdout, err := cmd.StdoutPipe()
  159. if err != nil {
  160. log.Println("Failed to get arecord stdout pipe:", err)
  161. http.Error(w, "Internal Server Error", http.StatusInternalServerError)
  162. return
  163. }
  164. if err := cmd.Start(); err != nil {
  165. log.Println("Failed to start arecord:", err)
  166. http.Error(w, "Internal Server Error", http.StatusInternalServerError)
  167. return
  168. }
  169. reader := bufio.NewReader(stdout)
  170. bufferSize := i.Config.AudioConfig.FrameSize * i.Config.AudioConfig.Channels * i.Config.AudioConfig.BytesPerSample
  171. log.Printf("Buffer size: %d bytes (FrameSize: %d, Channels: %d, BytesPerSample: %d)",
  172. bufferSize, i.Config.AudioConfig.FrameSize, i.Config.AudioConfig.Channels, i.Config.AudioConfig.BytesPerSample)
  173. buf := make([]byte, bufferSize*2)
  174. // Start a goroutine to handle WebSocket messages
  175. log.Println("Listening for WebSocket messages...")
  176. go func() {
  177. _, msg, err := conn.ReadMessage()
  178. if err == nil {
  179. if string(msg) == "exit" {
  180. log.Println("Received exit command from client")
  181. i.audiostopchan <- true // Signal to stop the audio pipe
  182. return
  183. }
  184. }
  185. }()
  186. log.Println("Starting audio capture loop...")
  187. i.isAudioStreaming = true
  188. for {
  189. select {
  190. case <-i.audiostopchan:
  191. log.Println("Audio pipe stopped")
  192. goto DONE
  193. default:
  194. n, err := reader.Read(buf)
  195. if err != nil {
  196. log.Println("Read error:", err)
  197. if i.audiostopchan != nil {
  198. i.audiostopchan <- true // Signal to stop the audio pipe
  199. }
  200. goto DONE
  201. }
  202. if n == 0 {
  203. continue
  204. }
  205. downsampled := buf[:n] // Default to original buffer if no downsampling
  206. switch selectedQuality {
  207. case "high":
  208. // Keep original 48kHz stereo
  209. case "standard":
  210. // Downsample to 24kHz stereo
  211. downsampled = downsample48kTo24kStereo(buf[:n]) // Downsample to 24kHz stereo
  212. copy(buf, downsampled) // Copy downsampled data back into buf
  213. n = len(downsampled) // Update n to the new length
  214. case "low":
  215. downsampled = downsample48kTo16kStereo(buf[:n]) // Downsample to 16kHz stereo
  216. copy(buf, downsampled) // Copy downsampled data back into buf
  217. n = len(downsampled) // Update n to the new length
  218. }
  219. //Send only the bytes read to WebSocket
  220. err = conn.WriteMessage(websocket.BinaryMessage, downsampled[:n])
  221. if err != nil {
  222. log.Println("WebSocket send error:", err)
  223. goto DONE
  224. }
  225. }
  226. }
  227. DONE:
  228. i.isAudioStreaming = false
  229. cmd.Process.Kill()
  230. log.Println("Audio pipe finished")
  231. }
  232. // Downsample48kTo24kStereo downsamples a 48kHz stereo audio buffer to 24kHz.
  233. // It assumes the input buffer is in 16-bit stereo format (2 bytes per channel).
  234. // The output buffer will also be in 16-bit stereo format.
  235. func downsample48kTo24kStereo(buf []byte) []byte {
  236. const frameSize = 4 // 2 bytes per channel × 2 channels
  237. if len(buf)%frameSize != 0 {
  238. // Trim incomplete frame (rare case)
  239. buf = buf[:len(buf)-len(buf)%frameSize]
  240. }
  241. out := make([]byte, 0, len(buf)/2)
  242. for i := 0; i < len(buf); i += frameSize * 2 {
  243. // Copy every other frame (drop 1 in 2)
  244. if i+frameSize <= len(buf) {
  245. out = append(out, buf[i:i+frameSize]...)
  246. }
  247. }
  248. return out
  249. }
  250. // Downsample48kTo16kStereo downsamples a 48kHz stereo audio buffer to 16kHz.
  251. // It assumes the input buffer is in 16-bit stereo format (2 bytes per channel).
  252. // The output buffer will also be in 16-bit stereo format.
  253. func downsample48kTo16kStereo(buf []byte) []byte {
  254. const frameSize = 4 // 2 bytes per channel × 2 channels
  255. if len(buf)%frameSize != 0 {
  256. // Trim incomplete frame (rare case)
  257. buf = buf[:len(buf)-len(buf)%frameSize]
  258. }
  259. out := make([]byte, 0, len(buf)/3)
  260. for i := 0; i < len(buf); i += frameSize * 3 {
  261. // Copy every third frame (drop 2 in 3)
  262. if i+frameSize <= len(buf) {
  263. out = append(out, buf[i:i+frameSize]...)
  264. }
  265. }
  266. return out
  267. }
  268. func alsa_device_occupied(dev string) bool {
  269. f, err := os.OpenFile(dev, os.O_RDONLY|syscall.O_NONBLOCK, 0)
  270. if err != nil {
  271. //result <- true // Occupied or cannot open
  272. return true
  273. }
  274. f.Close()
  275. return false
  276. }