fix: audio haché depuis carte son par chunks sox de taille variable

- Ajout buffer d'accumulation dans CoreAudioBackend pour garantir frames fixes
- sox envoie des chunks de taille variable → accumuler jusqu'à frameSize complet
- Émission de frames exactement 960 samples × 2ch × 2 bytes = 3840 bytes
- Adaptation automatique mono/stéréo selon config channels
- Audio fluide sans hachage/robotique vers clients LiveKit
This commit is contained in:
2026-06-02 01:14:49 +02:00
parent 9aff58c528
commit 2b88ea0ad5
6 changed files with 92 additions and 51 deletions
+31 -8
View File
@@ -32,6 +32,11 @@ export class CoreAudioBackend extends EventEmitter {
this.playbackProcess = null;
this.isCapturing = false;
this.isPlaying = false;
this.shuttingDown = false;
// Buffer d'accumulation pour la capture (sox envoie des chunks de taille variable)
this.captureAccumulator = Buffer.alloc(0);
this.targetCaptureBytes = this.options.framesPerBuffer * 2 * this.options.channels; // 2 bytes per sample
// Buffer circulaire pour la lecture
this.playbackBuffer = [];
@@ -212,8 +217,17 @@ export class CoreAudioBackend extends EventEmitter {
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);
// Accumuler les données jusqu'à avoir un frame complet
this.captureAccumulator = Buffer.concat([this.captureAccumulator, audioData]);
// Émettre des frames de taille fixe
while (this.captureAccumulator.length >= this.targetCaptureBytes) {
const frame = this.captureAccumulator.subarray(0, this.targetCaptureBytes);
this.emit('audioData', Buffer.from(frame)); // Copier pour éviter les références
// Garder le reste pour la prochaine frame
this.captureAccumulator = this.captureAccumulator.subarray(this.targetCaptureBytes);
}
});
this.captureProcess.stderr.on('data', (data) => {
@@ -249,6 +263,7 @@ export class CoreAudioBackend extends EventEmitter {
this.captureProcess.kill('SIGTERM');
this.captureProcess = null;
this.isCapturing = false;
this.captureAccumulator = Buffer.alloc(0); // Reset accumulator
console.log('✓ Capture audio arrêtée');
}
}
@@ -272,7 +287,7 @@ export class CoreAudioBackend extends EventEmitter {
// Output = -d (default) OU -t coreaudio "Device Name"
const args = [
'--buffer', '8192', // Buffer interne sox
'--buffer', '65536', // Buffer 64k (évite EOF prématuré)
'-t', 'raw',
'-b', '16',
'-e', 'signed-integer',
@@ -318,13 +333,20 @@ export class CoreAudioBackend extends EventEmitter {
});
this.playbackProcess.on('close', (code) => {
console.log(`⚠️ Sox playback fermé (code ${code}) après ${((Date.now() - this.playbackStartTime) / 1000).toFixed(1)}s`);
const uptime = ((Date.now() - this.playbackStartTime) / 1000).toFixed(1);
console.log(`⚠️ Sox playback fermé (code ${code}) après ${uptime}s`);
this.isPlaying = false;
// Tenter de redémarrer si c'était inattendu
if (code !== 0) {
console.log('🔄 Tentative de redémarrage du playback...');
setTimeout(() => this.startPlayback(), 1000);
// Redémarrer automatiquement (sox se ferme quand le buffer stdin se vide)
if (!this.shuttingDown) {
console.log('🔄 Redémarrage automatique du playback...');
setTimeout(() => {
if (!this.shuttingDown) {
this.startPlayback().catch(err => {
console.error('Erreur redémarrage playback:', err);
});
}
}, 100);
}
});
@@ -443,6 +465,7 @@ export class CoreAudioBackend extends EventEmitter {
* Arrête tous les streams
*/
destroy() {
this.shuttingDown = true;
this.stopCapture();
this.stopPlayback();
this.removeAllListeners();