audio_device.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  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, devicePath string) {
  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. pcmdev := devicePath
  134. if pcmdev == "" {
  135. //Try finding the HDMI capture card automatically
  136. pcmdev, err = FindHDMICapturePCMPath()
  137. if err != nil {
  138. log.Println("Failed to find HDMI capture PCM path:", err)
  139. http.Error(w, "Internal Server Error", http.StatusInternalServerError)
  140. return
  141. }
  142. }
  143. log.Println("Found HDMI capture PCM path:", pcmdev)
  144. // Convert PCM device to hardware device name
  145. hwdev, err := pcmDeviceToHW(pcmdev)
  146. if err != nil {
  147. log.Println("Failed to convert PCM device to hardware device:", err)
  148. http.Error(w, "Internal Server Error", http.StatusInternalServerError)
  149. return
  150. }
  151. log.Println("Using hardware device:", hwdev)
  152. // Create a buffered reader to read audio data
  153. log.Println("Starting audio pipe with arecord...")
  154. // Start arecord with 48kHz, 16-bit, stereo
  155. cmd := exec.Command("arecord",
  156. "-f", "S16_LE", // Format: 16-bit little-endian
  157. "-r", fmt.Sprint(i.Config.AudioConfig.SampleRate),
  158. "-c", fmt.Sprint(i.Config.AudioConfig.Channels),
  159. "-D", hwdev, // Use the hardware device
  160. )
  161. stdout, err := cmd.StdoutPipe()
  162. if err != nil {
  163. log.Println("Failed to get arecord stdout pipe:", err)
  164. http.Error(w, "Internal Server Error", http.StatusInternalServerError)
  165. return
  166. }
  167. if err := cmd.Start(); err != nil {
  168. log.Println("Failed to start arecord:", err)
  169. http.Error(w, "Internal Server Error", http.StatusInternalServerError)
  170. return
  171. }
  172. reader := bufio.NewReader(stdout)
  173. bufferSize := i.Config.AudioConfig.FrameSize * i.Config.AudioConfig.Channels * i.Config.AudioConfig.BytesPerSample
  174. log.Printf("Buffer size: %d bytes (FrameSize: %d, Channels: %d, BytesPerSample: %d)",
  175. bufferSize, i.Config.AudioConfig.FrameSize, i.Config.AudioConfig.Channels, i.Config.AudioConfig.BytesPerSample)
  176. buf := make([]byte, bufferSize*2)
  177. // Start a goroutine to handle WebSocket messages
  178. log.Println("Listening for WebSocket messages...")
  179. go func() {
  180. _, msg, err := conn.ReadMessage()
  181. if err == nil {
  182. if string(msg) == "exit" {
  183. log.Println("Received exit command from client")
  184. i.audiostopchan <- true // Signal to stop the audio pipe
  185. return
  186. }
  187. }
  188. }()
  189. log.Println("Starting audio capture loop...")
  190. i.isAudioStreaming = true
  191. for {
  192. select {
  193. case <-i.audiostopchan:
  194. log.Println("Audio pipe stopped")
  195. goto DONE
  196. default:
  197. n, err := reader.Read(buf)
  198. if err != nil {
  199. log.Println("Read error:", err)
  200. if i.audiostopchan != nil {
  201. i.audiostopchan <- true // Signal to stop the audio pipe
  202. }
  203. goto DONE
  204. }
  205. if n == 0 {
  206. continue
  207. }
  208. downsampled := buf[:n] // Default to original buffer if no downsampling
  209. switch selectedQuality {
  210. case "high":
  211. // Keep original 48kHz stereo
  212. case "standard":
  213. // Downsample to 24kHz stereo
  214. downsampled = downsample48kTo24kStereo(buf[:n]) // Downsample to 24kHz stereo
  215. copy(buf, downsampled) // Copy downsampled data back into buf
  216. n = len(downsampled) // Update n to the new length
  217. case "low":
  218. downsampled = downsample48kTo16kStereo(buf[:n]) // Downsample to 16kHz stereo
  219. copy(buf, downsampled) // Copy downsampled data back into buf
  220. n = len(downsampled) // Update n to the new length
  221. }
  222. //Send only the bytes read to WebSocket
  223. err = conn.WriteMessage(websocket.BinaryMessage, downsampled[:n])
  224. if err != nil {
  225. log.Println("WebSocket send error:", err)
  226. goto DONE
  227. }
  228. }
  229. }
  230. DONE:
  231. i.isAudioStreaming = false
  232. cmd.Process.Kill()
  233. log.Println("Audio pipe finished")
  234. }
  235. // Downsample48kTo24kStereo downsamples a 48kHz stereo audio buffer to 24kHz.
  236. // It assumes the input buffer is in 16-bit stereo format (2 bytes per channel).
  237. // The output buffer will also be in 16-bit stereo format.
  238. func downsample48kTo24kStereo(buf []byte) []byte {
  239. const frameSize = 4 // 2 bytes per channel × 2 channels
  240. if len(buf)%frameSize != 0 {
  241. // Trim incomplete frame (rare case)
  242. buf = buf[:len(buf)-len(buf)%frameSize]
  243. }
  244. out := make([]byte, 0, len(buf)/2)
  245. for i := 0; i < len(buf); i += frameSize * 2 {
  246. // Copy every other frame (drop 1 in 2)
  247. if i+frameSize <= len(buf) {
  248. out = append(out, buf[i:i+frameSize]...)
  249. }
  250. }
  251. return out
  252. }
  253. // Downsample48kTo16kStereo downsamples a 48kHz stereo audio buffer to 16kHz.
  254. // It assumes the input buffer is in 16-bit stereo format (2 bytes per channel).
  255. // The output buffer will also be in 16-bit stereo format.
  256. func downsample48kTo16kStereo(buf []byte) []byte {
  257. const frameSize = 4 // 2 bytes per channel × 2 channels
  258. if len(buf)%frameSize != 0 {
  259. // Trim incomplete frame (rare case)
  260. buf = buf[:len(buf)-len(buf)%frameSize]
  261. }
  262. out := make([]byte, 0, len(buf)/3)
  263. for i := 0; i < len(buf); i += frameSize * 3 {
  264. // Copy every third frame (drop 2 in 3)
  265. if i+frameSize <= len(buf) {
  266. out = append(out, buf[i:i+frameSize]...)
  267. }
  268. }
  269. return out
  270. }
  271. func alsa_device_occupied(dev string) bool {
  272. f, err := os.OpenFile(dev, os.O_RDONLY|syscall.O_NONBLOCK, 0)
  273. if err != nil {
  274. //result <- true // Occupied or cannot open
  275. return true
  276. }
  277. f.Close()
  278. return false
  279. }