macos #1

Merged
benoit merged 8 commits from macos into main 2026-06-18 16:20:06 +02:00
3 changed files with 133 additions and 51 deletions
Showing only changes of commit a803250f9f - Show all commits
+81 -11
View File
@@ -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
// 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 ${channelId}: ${pcmBuffer.length} bytes`);
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 multi-canaux (${numOutputChannels}ch): ${pcmBuffer.length} bytes`);
}
}
});
this.stats.framesCapture++;
this.stats.framesPlayback++;
+37 -30
View File
@@ -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
});
+13 -8
View File
@@ -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