diff --git a/server/bridge/AudioBridge.js b/server/bridge/AudioBridge.js index fe71172..7321291 100644 --- a/server/bridge/AudioBridge.js +++ b/server/bridge/AudioBridge.js @@ -399,10 +399,30 @@ export class AudioBridge extends EventEmitter { // Convertir PCM Buffer → Float32Array (pour GroupAudioRouter) const float32Data = this._bufferToFloat32(pcmData); - // Pour l'instant, on assume que l'audio vient du canal 0 - // TODO: Supporter multi-canaux depuis la carte son - const channelId = this.options.inputDeviceChannel || 0; - this.inputChannelBuffers.set(channelId, float32Data); + // Séparer les canaux si audio multi-canaux (entrelacé) + const numChannels = this.options.channels || 1; + + if (numChannels === 1) { + // Mono : un seul canal + const channelId = this.options.inputDeviceChannel || 0; + this.inputChannelBuffers.set(channelId, float32Data); + } else { + // Multi-canaux : dé-entrelacer les samples + // Format entrelacé : [L0, R0, L1, R1, ...] → [L0, L1, ...] et [R0, R1, ...] + const samplesPerChannel = float32Data.length / numChannels; + + for (let ch = 0; ch < numChannels; ch++) { + const channelBuffer = new Float32Array(samplesPerChannel); + + for (let i = 0; i < samplesPerChannel; i++) { + channelBuffer[i] = float32Data[i * numChannels + ch]; + } + + // Mapper canal hardware → canal logique (peut être configuré) + const logicalChannelId = this.options.channelMapping?.[ch] ?? ch; + this.inputChannelBuffers.set(logicalChannelId, channelBuffer); + } + } // ÉTAPE 1 : Inputs physiques → Groupes (via GroupAudioRouter) const groupBuffers = this.groupAudioRouter.processInputsToGroups( @@ -415,10 +435,23 @@ export class AudioBridge extends EventEmitter { // ÉTAPE 2 : Pour chaque groupe, envoyer vers le LiveKitClient correspondant groupBuffers.forEach((groupBuffer, groupName) => { - // Convertir Float32Array → PCM Buffer - const pcmBuffer = this._float32ToBuffer(groupBuffer); + // Les groupes sont MONO (Float32Array de N samples) + // Mais LiveKit attend du STÉRÉO (2 canaux) + // → Dupliquer le canal mono pour créer du faux stéréo - // Encoder en Opus + const samplesPerChannel = groupBuffer.length; + const stereoBuffer = new Float32Array(samplesPerChannel * 2); + + // Entrelacer : [M0, M1, M2, ...] → [M0, M0, M1, M1, M2, M2, ...] + for (let i = 0; i < samplesPerChannel; i++) { + stereoBuffer[i * 2] = groupBuffer[i]; // Canal gauche + stereoBuffer[i * 2 + 1] = groupBuffer[i]; // Canal droit (dupliqué) + } + + // Convertir Float32Array stéréo → PCM Buffer + const pcmBuffer = this._float32ToBuffer(stereoBuffer); + + // Encoder en Opus (maintenant en stéréo) const opusData = this.opusEncoder.encode(pcmBuffer); if (opusData) { @@ -432,7 +465,7 @@ export class AudioBridge extends EventEmitter { if (client && client.isConnected) { client.sendAudioData(pcmBuffer); if (this.stats.framesCapture % 100 === 0) { - console.log(`[AudioBridge] → LiveKit groupe "${groupName}": ${pcmBuffer.length} bytes`); + console.log(`[AudioBridge] → LiveKit groupe "${groupName}": ${pcmBuffer.length} bytes (mono→stéréo)`); } } else { if (this.stats.framesCapture % 100 === 0) { @@ -453,16 +486,53 @@ export class AudioBridge extends EventEmitter { } // ÉTAPE 4 : Envoyer chaque output à la carte son - outputBuffers.forEach((outputBuffer, channelId) => { - const pcmBuffer = this._float32ToBuffer(outputBuffer); + const numOutputChannels = this.options.channels || 1; - // Envoyer à la carte son + if (numOutputChannels === 1) { + // Mono : un seul output + if (outputBuffers.size > 0) { + const [firstChannelId, outputBuffer] = outputBuffers.entries().next().value; + const pcmBuffer = this._float32ToBuffer(outputBuffer); + this.audioBackend.queueAudio(pcmBuffer); + + if (this.stats.framesCapture % 100 === 0) { + console.log(`[AudioBridge] → Output mono (canal ${firstChannelId}): ${pcmBuffer.length} bytes`); + } + } + } else { + // Multi-canaux : entrelacer les samples + // Récupérer les buffers dans l'ordre des canaux hardware + const channelBuffers = []; + const samplesPerChannel = this.options.frameSize; + + for (let ch = 0; ch < numOutputChannels; ch++) { + const logicalChannelId = this.options.channelMapping?.[ch] ?? ch; + const buffer = outputBuffers.get(logicalChannelId); + + if (buffer && buffer.length === samplesPerChannel) { + channelBuffers.push(buffer); + } else { + // Canal absent ou taille incorrecte : silence + channelBuffers.push(new Float32Array(samplesPerChannel)); + } + } + + // Entrelacer : [L0, L1, ...] et [R0, R1, ...] → [L0, R0, L1, R1, ...] + const interleavedBuffer = new Float32Array(samplesPerChannel * numOutputChannels); + + for (let i = 0; i < samplesPerChannel; i++) { + for (let ch = 0; ch < numOutputChannels; ch++) { + interleavedBuffer[i * numOutputChannels + ch] = channelBuffers[ch][i]; + } + } + + const pcmBuffer = this._float32ToBuffer(interleavedBuffer); this.audioBackend.queueAudio(pcmBuffer); if (this.stats.framesCapture % 100 === 0) { - console.log(`[AudioBridge] → Output ${channelId}: ${pcmBuffer.length} bytes`); + console.log(`[AudioBridge] → Output multi-canaux (${numOutputChannels}ch): ${pcmBuffer.length} bytes`); } - }); + } this.stats.framesCapture++; this.stats.framesPlayback++; diff --git a/server/bridge/backends/CoreAudioBackend.js b/server/bridge/backends/CoreAudioBackend.js index 5983895..50d7cad 100644 --- a/server/bridge/backends/CoreAudioBackend.js +++ b/server/bridge/backends/CoreAudioBackend.js @@ -184,30 +184,33 @@ export class CoreAudioBackend extends EventEmitter { } try { - // 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) + // Commande sox pour capturer audio sur macOS + // Sur macOS, sox utilise CoreAudio par défaut via 'rec' (alias de sox -d) + // Format: sox -d [options] output + // -d = default input device OU -t coreaudio "Device Name" + + const args = []; + + // Spécifier le device d'entrée + if (this.options.inputDeviceName) { + // Utiliser le device spécifié par son nom + args.push('-t', 'coreaudio', this.options.inputDeviceName); + } else { + // Device par défaut + args.push('-d'); + } + + // Format de sortie (stdout) + args.push( '-t', 'raw', '-b', '16', '-e', 'signed-integer', - `-c`, String(this.options.channels), - `-r`, String(this.options.sampleRate), + '-c', String(this.options.channels), + '-r', String(this.options.sampleRate), '-' // Stdout - ]; - - // Si device spécifié - if (this.options.inputDeviceName) { - args[2] = this.options.inputDeviceName; // Index 2 = device name - } + ); + console.log(`🎤 Démarrage capture sox: ${args.join(' ')}`); this.captureProcess = spawn('sox', args); this.captureProcess.stdout.on('data', (audioData) => { @@ -265,27 +268,31 @@ export class CoreAudioBackend extends EventEmitter { } try { - // Commande sox pour lecture audio - // play : lire vers output par défaut - // -t raw : format raw PCM depuis stdin - // --buffer : taille du buffer interne sox (en bytes) + // Commande sox pour lecture audio sur macOS + // Format: sox [options] input output + // Input = stdin (-) + // Output = -d (default) OU -t coreaudio "Device Name" + const args = [ '--buffer', '8192', // Buffer interne sox '-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 + '-c', String(this.options.channels), + '-r', String(this.options.sampleRate), + '-' // Input = stdin ]; - // Si device spécifié + // Spécifier le device de sortie if (this.options.outputDeviceName) { - args[args.length - 1] = this.options.outputDeviceName; + // Utiliser le device spécifié par son nom + args.push('-t', 'coreaudio', this.options.outputDeviceName); + } else { + // Device par défaut + args.push('-d'); } + console.log(`🔊 Démarrage playback sox: ${args.join(' ')}`); this.playbackProcess = spawn('sox', args, { stdio: ['pipe', 'ignore', 'pipe'] // stdin=pipe, stdout=ignore, stderr=pipe }); diff --git a/server/config/config.yaml b/server/config/config.yaml index 6f8231d..10b456f 100644 --- a/server/config/config.yaml +++ b/server/config/config.yaml @@ -1,17 +1,20 @@ audio: sampleRate: 48000 + channels: 2 frameSize: 20 defaultBitrate: 96 jitterBufferMs: 40 device: - inputDeviceId: Microphone MacBook Pro + inputDeviceId: Loopback Audio 4 outputDeviceId: Haut-parleurs MacBook Pro sampleRate: 48000 routing: inputToGroup: "0": - production - "1": [] + - default + "1": + - default "2": [] "4": - technique @@ -23,29 +26,31 @@ audio: production: - "0" - "1" + default: + - "0" gains: {} channelNames: inputs: - "0": iphone + "0": Mac "1": Talkback FOH "2": Retour Console "3": Liaison Scène "4": Monitor Mix "5": Spare 1 outputs: - "0": Sortie Principale - "1": Retour Scène + "0": L + "1": R "2": Talkback Console groups: + - name: Default + audioBitrate: 96 + channels: [] - name: Production audioBitrate: 96 channels: [] - name: Technique audioBitrate: 96 channels: [] - - name: Sonorisation - audioBitrate: 128 - channels: [] server: host: 0.0.0.0 port: 3000