Compare commits

..

3 Commits

Author SHA1 Message Date
benoit d908cf4ee6 fix: API /admin/devices/list compatible macOS avec CoreAudio
Avant : Utilisait sox (IDs numériques, incomplet)
Après : Utilise CoreAudioBackend.getDevices() (noms devices réels)

- Retourne device.name comme ID (compatible inputDeviceName)
- Affiche channels, sampleRate, isDefault
- Fallback sur built-in devices si erreur
- Cohérent avec résolution AudioBridge (ligne 206-216)

Interface /admin maintenant 100% compatible macOS et Linux.
2026-05-28 16:07:01 +02:00
benoit 522a6255fe fix: API /admin/devices/list retourne vrais IDs PulseAudio/PipeWire
Avant : IDs numériques (0, 1, 2...) incompatibles avec PipeWireBackend
Après : IDs réels (alsa_input.pci-..., alsa_output.pci-...)

- Extraction deviceId depuis pactl (colonne 2)
- Filtrage des monitors (.monitor)
- Descriptions lisibles (Input: pci-..., Output: pci-...)
- Compatible avec config.yaml existant
2026-05-28 16:04:57 +02:00
benoit 5784aa68e1 clean: suppression logs debug audio
L'audio fonctionne correctement maintenant :
- Client PWA → LiveKit → Serveur → Haut-parleurs ✓
- Latence acceptable
- Qualité audio bonne

Fixes appliqués :
- Support Int16Array (LiveKit Node SDK format)
- Accumulation frames 240→960 samples
- Conversion directe Int16Array vers Float32
2026-05-28 15:52:23 +02:00
5 changed files with 311 additions and 122 deletions
+1 -1
View File
@@ -81,7 +81,7 @@ define(['./workbox-290dd570'], (function (workbox) { 'use strict';
"revision": "3ca0b8505b4bec776b69afdba2768812" "revision": "3ca0b8505b4bec776b69afdba2768812"
}, { }, {
"url": "index.html", "url": "index.html",
"revision": "0.lhgefe7plc8" "revision": "0.881sreuemg"
}], {}); }], {});
workbox.cleanupOutdatedCaches(); workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
File diff suppressed because one or more lines are too long
+56 -29
View File
@@ -679,39 +679,46 @@ router.get('/devices/list', async (req, res) => {
// Détection selon la plateforme // Détection selon la plateforme
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
// macOS : utiliser CoreAudio via sox // macOS : utiliser CoreAudioBackend.getDevices()
const { exec } = await import('child_process');
const { promisify } = await import('util');
const execPromise = promisify(exec);
try { try {
// Utiliser sox pour lister les devices audio const coreAudioDevices = CoreAudioBackend.getDevices();
const { stdout } = await execPromise('sox -V6 2>&1');
// Parser la sortie sox pour extraire les devices // Séparer inputs et outputs
// Format typique : "Input Device [0]: MacBook Pro Microphone" coreAudioDevices.forEach(device => {
const inputMatches = stdout.matchAll(/Input Device \[(\d+)\]: (.+)/g); if (device.maxInputChannels > 0) {
const outputMatches = stdout.matchAll(/Output Device \[(\d+)\]: (.+)/g);
for (const match of inputMatches) {
devices.inputs.push({ devices.inputs.push({
id: parseInt(match[1], 10), id: device.name, // Utiliser le nom comme ID (compatible avec inputDeviceName)
name: match[2].trim() name: device.name,
channels: device.maxInputChannels,
sampleRate: device.defaultSampleRate,
isDefault: device.isDefault?.input || false
}); });
} }
for (const match of outputMatches) { if (device.maxOutputChannels > 0) {
devices.outputs.push({ devices.outputs.push({
id: parseInt(match[1], 10), id: device.name, // Utiliser le nom comme ID (compatible avec outputDeviceName)
name: match[2].trim() name: device.name,
channels: device.maxOutputChannels,
sampleRate: device.defaultSampleRate,
isDefault: device.isDefault?.output || false
}); });
} }
} catch (soxError) { });
console.warn('⚠️ sox non disponible, devices limités:', soxError.message);
// Fallback si aucun device trouvé
if (devices.inputs.length === 0) {
devices.inputs.push({ id: 'builtin-mic', name: 'Built-in Microphone', isDefault: true });
}
if (devices.outputs.length === 0) {
devices.outputs.push({ id: 'builtin-output', name: 'Built-in Output', isDefault: true });
}
} catch (error) {
console.warn('⚠️ Détection CoreAudio échouée:', error.message);
// Fallback : devices par défaut macOS // Fallback : devices par défaut macOS
devices.inputs.push({ id: 0, name: 'Default Input (Built-in Microphone)', isDefault: true }); devices.inputs.push({ id: 'builtin-mic', name: 'Built-in Microphone', isDefault: true });
devices.outputs.push({ id: 0, name: 'Default Output (Built-in Speakers)', isDefault: true }); devices.outputs.push({ id: 'builtin-output', name: 'Built-in Output', isDefault: true });
} }
} else if (process.platform === 'linux') { } else if (process.platform === 'linux') {
@@ -736,21 +743,41 @@ router.get('/devices/list', async (req, res) => {
} }
}); });
} else { } else {
// Fallback : PipeWire via pactl // Fallback : PipeWire/PulseAudio via pactl
const { stdout: paDevices } = await execPromise('pactl list short sources 2>/dev/null || echo ""'); const { stdout: paDevices } = await execPromise('pactl list short sources 2>/dev/null || echo ""');
const { stdout: paSinks } = await execPromise('pactl list short sinks 2>/dev/null || echo ""'); const { stdout: paSinks } = await execPromise('pactl list short sinks 2>/dev/null || echo ""');
// Helper pour obtenir une description lisible
const getDeviceDescription = (deviceId) => {
// Extraire une description plus lisible du nom technique
if (deviceId.includes('alsa_input')) return deviceId.replace('alsa_input.', 'Input: ');
if (deviceId.includes('alsa_output')) return deviceId.replace('alsa_output.', 'Output: ');
return deviceId;
};
if (paDevices.trim()) { if (paDevices.trim()) {
paDevices.split('\n').filter(Boolean).forEach((line, idx) => { paDevices.split('\n').filter(Boolean).forEach((line) => {
const name = line.split('\t')[1] || `Device ${idx}`; const parts = line.split('\t');
devices.inputs.push({ id: idx, name }); const deviceId = parts[1]; // Nom du device (ex: alsa_input.pci-...)
if (deviceId && !deviceId.includes('.monitor')) { // Ignorer les monitors
devices.inputs.push({
id: deviceId,
name: getDeviceDescription(deviceId)
});
}
}); });
} }
if (paSinks.trim()) { if (paSinks.trim()) {
paSinks.split('\n').filter(Boolean).forEach((line, idx) => { paSinks.split('\n').filter(Boolean).forEach((line) => {
const name = line.split('\t')[1] || `Device ${idx}`; const parts = line.split('\t');
devices.outputs.push({ id: idx, name }); const deviceId = parts[1]; // Nom du device (ex: alsa_output.pci-...)
if (deviceId) {
devices.outputs.push({
id: deviceId,
name: getDeviceDescription(deviceId)
});
}
}); });
} }
} }
-20
View File
@@ -372,26 +372,6 @@ export class AudioBridge extends EventEmitter {
// Réception audio depuis les clients LiveKit de ce groupe // Réception audio depuis les clients LiveKit de ce groupe
client.on('audioData', ({ participantName, pcmData, sampleRate, channels }) => { client.on('audioData', ({ participantName, pcmData, sampleRate, channels }) => {
// Log premier frame pour diagnostic
if (!this._firstFrameLogged) {
// Calculer RMS pour détecter silence
let sumSquares = 0;
for (let i = 0; i < Math.min(240, pcmData.length); i++) {
sumSquares += pcmData[i] * pcmData[i];
}
const rms = Math.sqrt(sumSquares / Math.min(240, pcmData.length));
const dbFS = 20 * Math.log10(rms / 32768.0);
console.log(`🔍 Diagnostic audio LiveKit:
sampleRate: ${sampleRate}
channels: ${channels || 1} (défaut: 1 si undefined)
buffer size: ${pcmData.length} samples (${pcmData.length * 2} bytes)
buffer type: ${pcmData.constructor.name}
first 10 samples: [${Array.from(pcmData.slice(0, 10)).join(', ')}]
RMS level: ${rms.toFixed(0)} (${dbFS.toFixed(1)} dBFS)`);
this._firstFrameLogged = true;
}
// Router vers le bon groupe // Router vers le bon groupe
this.emit('groupAudioIn', { groupName: groupId, pcmBuffer: pcmData }); this.emit('groupAudioIn', { groupName: groupId, pcmBuffer: pcmData });
}); });
File diff suppressed because one or more lines are too long