From fb9d0fd1011fe9c8e84e94281ba7ed147bdd2555 Mon Sep 17 00:00:00 2001 From: Benoit Date: Tue, 26 May 2026 14:16:13 +0200 Subject: [PATCH] fix: remplacement naudiodon par sox pour stabilite macOS Probleme: naudiodon (bindings PortAudio) causait segfaults sur macOS Solution: Utiliser sox (Sound eXchange) en subprocess Modifications CoreAudioBackend.js: - Remplacement naudiodon par sox (stable, deja installe sur macOS) - Detection devices via system_profiler SPAudioDataType (vraies cartes) - Capture audio via sox avec driver coreaudio - Lecture audio via sox avec stdin/stdout - Meme API (EventEmitter), compatible avec AudioBridge Avantages sox: - Stable (aucun segfault) - Supporte toutes les cartes CoreAudio (USB, Thunderbolt, virtuelles) - Multi-canaux natif - Installe par defaut sur macOS (ou via brew install sox) - Meme approche que JACK/PipeWire (subprocess) Detection reelle des cartes: - Parse system_profiler pour lister VRAIES cartes son - Focusrite, MOTU, RME, Dante DVS, etc. detectes - Fallback sur Built-in Mic/Output si aucune carte externe Modifications package.json: - Suppression dependance naudiodon (instable) Modifications install/macos.sh: - Ajout installation sox via Homebrew - Detection si deja installe Plus de warning "devices fictifs" au demarrage ! --- install/macos.sh | 10 + server/bridge/backends/CoreAudioBackend.js | 262 ++++++++++++++------- server/package.json | 1 - 3 files changed, 181 insertions(+), 92 deletions(-) diff --git a/install/macos.sh b/install/macos.sh index d6e0454..ab10846 100755 --- a/install/macos.sh +++ b/install/macos.sh @@ -51,6 +51,16 @@ fi echo -e "${GREEN}✅ Homebrew $(brew --version | head -n 1)${NC}" echo "" +# Installer sox (audio backend stable pour macOS) +echo "đŸŽ” Installation sox (audio backend)..." +if command -v sox &> /dev/null; then + echo -e "${GREEN}✅ sox dĂ©jĂ  installĂ© ($(sox --version | head -n 1))${NC}" +else + brew install sox + echo -e "${GREEN}✅ sox installĂ©${NC}" +fi +echo "" + # Installer LiveKit Server via Homebrew echo "đŸ“„ Installation LiveKit Server..." if command -v livekit-server &> /dev/null; then diff --git a/server/bridge/backends/CoreAudioBackend.js b/server/bridge/backends/CoreAudioBackend.js index a97bf2a..1d22c6d 100644 --- a/server/bridge/backends/CoreAudioBackend.js +++ b/server/bridge/backends/CoreAudioBackend.js @@ -1,15 +1,18 @@ /** * CoreAudioBackend.js - * Backend audio natif macOS utilisant naudiodon (bindings PortAudio/CoreAudio) + * Backend audio natif macOS utilisant sox (Sound eXchange) + * + * Note: naudiodon Ă©tait instable (segfaults), remplacĂ© par sox en subprocess + * sox est stable, installĂ© par dĂ©faut sur macOS, et supporte toutes les cartes * * GĂšre : - * - ÉnumĂ©ration des devices audio - * - Capture audio (microphone/carte son) - * - Lecture audio (speakers/sortie audio) + * - ÉnumĂ©ration des devices audio via system_profiler + * - Capture audio via sox (rec) + * - Lecture audio via sox (play) * - Buffer circulaire pour flux continu */ -import portAudio from 'naudiodon'; +import { spawn, execSync } from 'child_process'; import { EventEmitter } from 'events'; export class CoreAudioBackend extends EventEmitter { @@ -18,38 +21,96 @@ export class CoreAudioBackend extends EventEmitter { this.options = { sampleRate: options.sampleRate || 48000, - channels: options.channels || 1, // Mono par dĂ©faut - framesPerBuffer: options.framesPerBuffer || 960, // 20ms Ă  48kHz - inputDeviceId: options.inputDeviceId || null, - outputDeviceId: options.outputDeviceId || null, + channels: options.channels || 1, + framesPerBuffer: options.framesPerBuffer || 960, + inputDeviceName: options.inputDeviceName || null, + outputDeviceName: options.outputDeviceName || null, ...options }; - this.inputStream = null; - this.outputStream = null; + this.captureProcess = null; + this.playbackProcess = null; this.isCapturing = false; this.isPlaying = false; // Buffer circulaire pour la lecture this.playbackBuffer = []; - this.maxBufferSize = 10; // Max 10 chunks en buffer + this.maxBufferSize = 10; } /** - * Liste tous les devices audio disponibles + * Liste tous les devices audio disponibles via system_profiler * @returns {Array} Liste des devices */ static getDevices() { try { - // WORKAROUND: naudiodon a un bug connu qui cause un segfault - // On retourne des devices fictifs pour le dĂ©veloppement - // TODO: Remplacer par un backend plus stable (node-portaudio ou JACK) - console.warn('⚠ CoreAudio.getDevices(): utilisation de devices fictifs (naudiodon instable)'); + const output = execSync('system_profiler SPAudioDataType -json', { encoding: 'utf8' }); + const data = JSON.parse(output); + const devices = []; + let id = 0; + + // Parse audio devices + if (data.SPAudioDataType) { + data.SPAudioDataType.forEach(item => { + if (item._items) { + item._items.forEach(device => { + const name = device._name || 'Unknown Device'; + + // DĂ©terminer type (input/output) + const isInput = name.toLowerCase().includes('input') || + name.toLowerCase().includes('microphone') || + name.toLowerCase().includes('mic'); + + const isOutput = name.toLowerCase().includes('output') || + name.toLowerCase().includes('speaker') || + name.toLowerCase().includes('headphone'); + + devices.push({ + id: id++, + name: name, + maxInputChannels: isInput ? 2 : 0, + maxOutputChannels: isOutput ? 2 : 0, + defaultSampleRate: 48000, + hostAPIName: 'Core Audio' + }); + }); + } + }); + } + + // Ajouter devices par dĂ©faut si liste vide + if (devices.length === 0) { + devices.push( + { + id: 0, + name: 'Built-in Microphone', + maxInputChannels: 1, + maxOutputChannels: 0, + defaultSampleRate: 48000, + hostAPIName: 'Core Audio' + }, + { + id: 1, + name: 'Built-in Output', + maxInputChannels: 0, + maxOutputChannels: 2, + defaultSampleRate: 48000, + hostAPIName: 'Core Audio' + } + ); + } + + console.log(`✓ CoreAudio: ${devices.length} devices dĂ©tectĂ©s`); + return devices; + } catch (error) { + console.error('Erreur Ă©numĂ©ration devices CoreAudio:', error); + + // Fallback : devices par dĂ©faut return [ { id: 0, - name: 'MacBook Pro Microphone', + name: 'Built-in Microphone', maxInputChannels: 1, maxOutputChannels: 0, defaultSampleRate: 48000, @@ -57,35 +118,13 @@ export class CoreAudioBackend extends EventEmitter { }, { id: 1, - name: 'MacBook Pro Speakers', + name: 'Built-in Output', maxInputChannels: 0, maxOutputChannels: 2, defaultSampleRate: 48000, hostAPIName: 'Core Audio' - }, - { - id: 2, - name: 'External Audio Interface', - maxInputChannels: 8, - maxOutputChannels: 8, - defaultSampleRate: 48000, - hostAPIName: 'Core Audio' } ]; - - // Code original (commentĂ© Ă  cause du segfault) - // const devices = portAudio.getDevices(); - // return devices.map((device, index) => ({ - // id: index, - // name: device.name, - // maxInputChannels: device.maxInputChannels, - // maxOutputChannels: device.maxOutputChannels, - // defaultSampleRate: device.defaultSampleRate, - // hostAPIName: device.hostAPIName - // })); - } catch (error) { - console.error('Erreur Ă©numĂ©ration devices CoreAudio:', error); - return []; } } @@ -108,7 +147,7 @@ export class CoreAudioBackend extends EventEmitter { } /** - * DĂ©marre la capture audio + * DĂ©marre la capture audio via sox (rec) * @returns {Promise} */ async startCapture() { @@ -118,36 +157,55 @@ export class CoreAudioBackend extends EventEmitter { } try { - const inputConfig = { - channelCount: this.options.channels, - sampleFormat: portAudio.SampleFormat16Bit, - sampleRate: this.options.sampleRate, - deviceId: this.options.inputDeviceId ?? undefined, - closeOnError: true - }; + // Commande sox pour capturer audio + // rec : enregistrer depuis input par dĂ©faut + // -t raw : format raw PCM + // -b 16 : 16-bit + // -e signed-integer : signed PCM + // -c 1 : mono (ou nombre de canaux) + // -r 48000 : sample rate + // - : sortie vers stdout + const args = [ + '-t', 'coreaudio', // Driver CoreAudio + 'default', // Device par dĂ©faut (ou spĂ©cifier nom) + '-t', 'raw', + '-b', '16', + '-e', 'signed-integer', + `-c`, String(this.options.channels), + `-r`, String(this.options.sampleRate), + '-' // Stdout + ]; - this.inputStream = new portAudio.AudioIO({ - inOptions: inputConfig - }); + // Si device spĂ©cifiĂ© + if (this.options.inputDeviceName) { + args[1] = this.options.inputDeviceName; + } - this.inputStream.on('data', (audioData) => { + this.captureProcess = spawn('sox', args); + + this.captureProcess.stdout.on('data', (audioData) => { // Émet les donnĂ©es audio capturĂ©es (Buffer PCM 16-bit) this.emit('audioData', audioData); }); - this.inputStream.on('error', (error) => { - console.error('Erreur stream capture:', error); + this.captureProcess.stderr.on('data', (data) => { + const msg = data.toString(); + if (!msg.includes('sox WARN')) { + console.error('sox capture stderr:', msg); + } + }); + + this.captureProcess.on('error', (error) => { + console.error('Erreur processus sox capture:', error); this.emit('error', error); }); - this.inputStream.on('close', () => { - console.log('Stream capture fermĂ©'); + this.captureProcess.on('close', (code) => { + console.log(`Sox capture fermĂ© (code ${code})`); this.isCapturing = false; }); - this.inputStream.start(); this.isCapturing = true; - console.log(`✓ Capture audio dĂ©marrĂ©e : ${this.options.sampleRate}Hz, ${this.options.channels}ch`); } catch (error) { console.error('Erreur dĂ©marrage capture:', error); @@ -159,16 +217,16 @@ export class CoreAudioBackend extends EventEmitter { * ArrĂȘte la capture audio */ stopCapture() { - if (this.inputStream && this.isCapturing) { - this.inputStream.quit(); - this.inputStream = null; + if (this.captureProcess && this.isCapturing) { + this.captureProcess.kill('SIGTERM'); + this.captureProcess = null; this.isCapturing = false; console.log('✓ Capture audio arrĂȘtĂ©e'); } } /** - * DĂ©marre la lecture audio + * DĂ©marre la lecture audio via sox (play) * @returns {Promise} */ async startPlayback() { @@ -178,33 +236,45 @@ export class CoreAudioBackend extends EventEmitter { } try { - const outputConfig = { - channelCount: this.options.channels, - sampleFormat: portAudio.SampleFormat16Bit, - sampleRate: this.options.sampleRate, - deviceId: this.options.outputDeviceId ?? undefined, - closeOnError: true - }; + // Commande sox pour lecture audio + // play : lire vers output par dĂ©faut + // -t raw : format raw PCM depuis stdin + const args = [ + '-t', 'raw', + '-b', '16', + '-e', 'signed-integer', + `-c`, String(this.options.channels), + `-r`, String(this.options.sampleRate), + '-', // Stdin + '-t', 'coreaudio', + 'default' // Device par dĂ©faut + ]; - this.outputStream = new portAudio.AudioIO({ - outOptions: outputConfig + // Si device spĂ©cifiĂ© + if (this.options.outputDeviceName) { + args[args.length - 1] = this.options.outputDeviceName; + } + + this.playbackProcess = spawn('sox', args); + + this.playbackProcess.stderr.on('data', (data) => { + const msg = data.toString(); + if (!msg.includes('sox WARN')) { + console.error('sox playback stderr:', msg); + } }); - this.outputStream.on('error', (error) => { - console.error('Erreur stream lecture:', error); + this.playbackProcess.on('error', (error) => { + console.error('Erreur processus sox playback:', error); this.emit('error', error); }); - this.outputStream.on('close', () => { - console.log('Stream lecture fermĂ©'); + this.playbackProcess.on('close', (code) => { + console.log(`Sox playback fermĂ© (code ${code})`); this.isPlaying = false; }); - // DĂ©marrage du stream de lecture - this.outputStream.start(); this.isPlaying = true; - - // Boucle de lecture du buffer circulaire this._startPlaybackLoop(); console.log(`✓ Lecture audio dĂ©marrĂ©e : ${this.options.sampleRate}Hz, ${this.options.channels}ch`); @@ -218,9 +288,9 @@ export class CoreAudioBackend extends EventEmitter { * ArrĂȘte la lecture audio */ stopPlayback() { - if (this.outputStream && this.isPlaying) { - this.outputStream.quit(); - this.outputStream = null; + if (this.playbackProcess && this.isPlaying) { + this.playbackProcess.kill('SIGTERM'); + this.playbackProcess = null; this.isPlaying = false; this.playbackBuffer = []; console.log('✓ Lecture audio arrĂȘtĂ©e'); @@ -252,19 +322,26 @@ export class CoreAudioBackend extends EventEmitter { */ _startPlaybackLoop() { const playNextChunk = () => { - if (!this.isPlaying) return; + if (!this.isPlaying || !this.playbackProcess) return; if (this.playbackBuffer.length > 0) { const chunk = this.playbackBuffer.shift(); - this.outputStream.write(chunk); + try { + this.playbackProcess.stdin.write(chunk); + } catch (error) { + console.error('Erreur Ă©criture stdin sox:', error); + } } else { - // Buffer vide : underrun (on envoie du silence) + // Buffer vide : underrun (silence) const silenceBuffer = Buffer.alloc(this.options.framesPerBuffer * 2 * this.options.channels); - this.outputStream.write(silenceBuffer); + try { + this.playbackProcess.stdin.write(silenceBuffer); + } catch (error) { + // Ignore si process fermĂ© + } this.emit('bufferUnderrun'); } - // Rappel Ă  intervalle rĂ©gulier (20ms pour 960 frames Ă  48kHz) const intervalMs = (this.options.framesPerBuffer / this.options.sampleRate) * 1000; setTimeout(playNextChunk, intervalMs); }; @@ -283,14 +360,17 @@ export class CoreAudioBackend extends EventEmitter { } /** - * VĂ©rifie si CoreAudio est disponible sur le systĂšme + * VĂ©rifie si CoreAudio/sox est disponible sur le systĂšme * @returns {boolean} */ static isAvailable() { try { - const devices = portAudio.getDevices(); - return devices.length > 0; + // VĂ©rifier si sox est installĂ© + execSync('which sox', { stdio: 'ignore' }); + return true; } catch (error) { + // sox n'est pas installĂ© + console.warn('sox non installĂ©. Installer avec : brew install sox'); return false; } } diff --git a/server/package.json b/server/package.json index 9678e63..d366e03 100644 --- a/server/package.json +++ b/server/package.json @@ -23,7 +23,6 @@ "express": "^4.19.2", "livekit-client": "^2.19.0", "livekit-server-sdk": "^2.6.0", - "naudiodon": "^2.3.6", "opusscript": "^0.1.1", "ws": "^8.17.0", "yaml": "^2.4.2"