mirror of
https://github.com/R0m1k3/LiveFlow.git
synced 2026-10-11 17:27:19 +02:00
Micro : suppression de la bascule automatique + gain auto renforcé et mémorisé
- L'app ne change plus jamais de micro toute seule : choix manuel dans la liste uniquement, simple alerte si aucun son n'est capté - Gain automatique : seuil d'amplification élargi (signal < 12000), montée plus rapide (x1.3/250 ms), plafond x80, et gain mémorisé entre réunions pour être efficace dès la première seconde https://claude.ai/code/session_01YHMp3EKzr4s6o8w1ygxuUe
This commit is contained in:
1 file changed
+28
-55
+28
-55
@@ -12,13 +12,11 @@ const state = {
|
|||||||
startedAt: null,
|
startedAt: null,
|
||||||
currentMeetingId: null,
|
currentMeetingId: null,
|
||||||
lastLoudAt: 0,
|
lastLoudAt: 0,
|
||||||
heardSound: false, // un micro a déjà capté du son : on ne change plus
|
heardSound: false,
|
||||||
silentWarned: false,
|
silentWarned: false,
|
||||||
sourceNode: null,
|
sourceNode: null,
|
||||||
gainNode: null,
|
gainNode: null,
|
||||||
lastGainAt: 0,
|
lastGainAt: 0,
|
||||||
autoMicQueue: null,
|
|
||||||
switching: false,
|
|
||||||
paused: false,
|
paused: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -81,37 +79,11 @@ async function switchMic(deviceId) {
|
|||||||
state.mediaStream = stream;
|
state.mediaStream = stream;
|
||||||
state.sourceNode = state.audioContext.createMediaStreamSource(stream);
|
state.sourceNode = state.audioContext.createMediaStreamSource(stream);
|
||||||
state.sourceNode.connect(state.gainNode);
|
state.sourceNode.connect(state.gainNode);
|
||||||
state.gainNode.gain.value = 1; // le nouveau micro repart d'un gain neutre
|
state.gainNode.gain.value = savedGain();
|
||||||
state.lastLoudAt = Date.now();
|
state.lastLoudAt = Date.now();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si le micro actif reste muet, essaie les autres micros un par un.
|
|
||||||
async function autoNextMic() {
|
|
||||||
if (state.switching || !state.recording) return;
|
|
||||||
state.switching = true;
|
|
||||||
try {
|
|
||||||
if (!state.autoMicQueue) {
|
|
||||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
||||||
const current = state.mediaStream?.getAudioTracks()[0]?.getSettings().deviceId;
|
|
||||||
state.autoMicQueue = devices.filter((d) =>
|
|
||||||
d.kind === 'audioinput' && d.deviceId &&
|
|
||||||
d.deviceId !== 'default' && d.deviceId !== 'communications' &&
|
|
||||||
d.deviceId !== current);
|
|
||||||
}
|
|
||||||
const next = state.autoMicQueue.shift();
|
|
||||||
if (!next) {
|
|
||||||
setStatus('error', 'Aucun micro ne capte de son — vérifiez côté système');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setStatus('busy', 'Micro muet — essai : ' + (next.label || 'micro sans nom'));
|
|
||||||
if (await switchMic(next.deviceId)) {
|
|
||||||
$('mic-select').value = next.deviceId;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
state.switching = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------- enregistrement
|
// ----------------------------------------------------------- enregistrement
|
||||||
|
|
||||||
@@ -155,7 +127,7 @@ async function startRecording() {
|
|||||||
state.workletNode.port.onmessage = (e) => queuePcm(new Int16Array(e.data));
|
state.workletNode.port.onmessage = (e) => queuePcm(new Int16Array(e.data));
|
||||||
// gain automatique : compense les micros trop faibles (casques...)
|
// gain automatique : compense les micros trop faibles (casques...)
|
||||||
state.gainNode = state.audioContext.createGain();
|
state.gainNode = state.audioContext.createGain();
|
||||||
state.gainNode.gain.value = 1;
|
state.gainNode.gain.value = savedGain(); // repart du gain mémorisé
|
||||||
state.gainNode.connect(state.workletNode);
|
state.gainNode.connect(state.workletNode);
|
||||||
state.sourceNode = state.audioContext.createMediaStreamSource(state.mediaStream);
|
state.sourceNode = state.audioContext.createMediaStreamSource(state.mediaStream);
|
||||||
state.sourceNode.connect(state.gainNode);
|
state.sourceNode.connect(state.gainNode);
|
||||||
@@ -169,7 +141,6 @@ async function startRecording() {
|
|||||||
state.lastLoudAt = Date.now();
|
state.lastLoudAt = Date.now();
|
||||||
state.heardSound = false;
|
state.heardSound = false;
|
||||||
state.silentWarned = false;
|
state.silentWarned = false;
|
||||||
state.autoMicQueue = null;
|
|
||||||
state.timerInterval = setInterval(updateTimer, 500);
|
state.timerInterval = setInterval(updateTimer, 500);
|
||||||
$('record-btn').textContent = '■ Arrêter';
|
$('record-btn').textContent = '■ Arrêter';
|
||||||
$('record-btn').classList.add('recording');
|
$('record-btn').classList.add('recording');
|
||||||
@@ -207,18 +178,28 @@ function updateVu(samples) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Gain automatique : monte progressivement le volume des micros faibles
|
// Gain automatique : monte progressivement le volume des micros faibles
|
||||||
// jusqu'à un niveau exploitable par la détection de parole, et redescend
|
// jusqu'à un niveau exploitable par la détection de parole, redescend en cas
|
||||||
// en cas de saturation. Ajusté au plus toutes les 250 ms.
|
// de saturation, et mémorise le gain trouvé pour les prochaines réunions.
|
||||||
|
function savedGain() {
|
||||||
|
const g = parseFloat(localStorage.getItem('liveflow-gain'));
|
||||||
|
return Number.isFinite(g) ? Math.min(80, Math.max(1, g)) : 1;
|
||||||
|
}
|
||||||
|
|
||||||
function autoGain(peak) {
|
function autoGain(peak) {
|
||||||
if (!state.gainNode || state.paused) return;
|
if (!state.gainNode || state.paused) return;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - state.lastGainAt < 250) return;
|
if (now - state.lastGainAt < 250) return;
|
||||||
state.lastGainAt = now;
|
state.lastGainAt = now;
|
||||||
const g = state.gainNode.gain.value;
|
const g = state.gainNode.gain.value;
|
||||||
if (peak > 26000) {
|
let next = g;
|
||||||
state.gainNode.gain.value = Math.max(1, g * 0.7); // proche saturation
|
if (peak > 27000) {
|
||||||
} else if (peak > 200 && peak < 8000) {
|
next = Math.max(1, g * 0.7); // proche saturation
|
||||||
state.gainNode.gain.value = Math.min(40, g * 1.25); // parole trop faible
|
} else if (peak > 60 && peak < 12000) {
|
||||||
|
next = Math.min(80, g * 1.3); // signal trop faible
|
||||||
|
}
|
||||||
|
if (next !== g) {
|
||||||
|
state.gainNode.gain.value = next;
|
||||||
|
localStorage.setItem('liveflow-gain', String(next));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,26 +290,19 @@ function updateTimer() {
|
|||||||
$('timer').textContent =
|
$('timer').textContent =
|
||||||
String(Math.floor(s / 60)).padStart(2, '0') + ':' + String(s % 60).padStart(2, '0');
|
String(Math.floor(s / 60)).padStart(2, '0') + ':' + String(s % 60).padStart(2, '0');
|
||||||
|
|
||||||
// Recherche automatique de micro : UNIQUEMENT tant qu'aucun son n'a
|
// Simple alerte tant qu'aucun son n'a jamais été capté : l'app ne change
|
||||||
// jamais été capté depuis le début (un silence en cours de réunion est
|
// JAMAIS de micro toute seule (le choix se fait dans la liste).
|
||||||
// normal et ne doit jamais déclencher un changement de micro).
|
if (!state.recording || state.paused) return;
|
||||||
if (!state.recording || state.paused || state.heardSound) {
|
if (state.heardSound) {
|
||||||
if (state.silentWarned && state.heardSound) {
|
if (state.silentWarned) {
|
||||||
// le micro fonctionne : on le mémorise et on arrête la recherche
|
|
||||||
state.silentWarned = false;
|
state.silentWarned = false;
|
||||||
state.autoMicQueue = null;
|
|
||||||
const id = state.mediaStream?.getAudioTracks()[0]?.getSettings().deviceId;
|
|
||||||
if (id) {
|
|
||||||
localStorage.setItem('liveflow-mic', id);
|
|
||||||
$('mic-select').value = id;
|
|
||||||
}
|
|
||||||
setStatus('rec', 'Enregistrement…');
|
setStatus('rec', 'Enregistrement…');
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Date.now() - state.lastLoudAt > 5000) {
|
if (!state.silentWarned && Date.now() - state.lastLoudAt > 5000) {
|
||||||
state.silentWarned = true;
|
state.silentWarned = true;
|
||||||
autoNextMic();
|
setStatus('error', 'Aucun son capté — choisissez un autre micro dans la liste');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,9 +403,8 @@ $('mic-select').onchange = async () => {
|
|||||||
const id = $('mic-select').value;
|
const id = $('mic-select').value;
|
||||||
localStorage.setItem('liveflow-mic', id);
|
localStorage.setItem('liveflow-mic', id);
|
||||||
if (state.recording) {
|
if (state.recording) {
|
||||||
await switchMic(id || undefined); // bascule à chaud
|
await switchMic(id || undefined); // bascule à chaud, à la demande
|
||||||
state.heardSound = false; // le nouveau micro doit faire ses preuves
|
state.heardSound = false;
|
||||||
state.autoMicQueue = null;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
listMics();
|
listMics();
|
||||||
|
|||||||
Reference in new issue
Block a user