activity
All checks were successful
/ upload (release) Successful in 3m15s

This commit is contained in:
pavel 2026-02-25 18:46:24 +01:00
commit 72a10ad32b
2 changed files with 78 additions and 28 deletions

View file

@ -817,31 +817,56 @@ async function buildAudioPipeline(rawStream) {
state.voice.activeNoiseFilter = 'off'; state.voice.activeNoiseFilter = 'off';
} }
} }
// Metering/Speaking detection // Metering/Speaking detection with adaptive threshold and hysteresis.
const analyser = ctx.createAnalyser(); const analyser = ctx.createAnalyser();
analyser.fftSize = 512; analyser.fftSize = 1024;
processedSource.connect(analyser); processedSource.connect(analyser);
const dataArray = new Uint8Array(analyser.frequencyBinCount); const timeData = new Float32Array(analyser.fftSize);
let localIsSpeaking = false; let localIsSpeaking = false;
let speakCounter = 0; let speechFrames = 0;
let silenceFrames = 0;
let noiseFloor = 0.004;
let levelEma = 0;
const checkVolume = () => { const checkVolume = () => {
if (!state.voice.audioContext || state.voice.audioContext.state === 'closed') return; if (!state.voice.audioContext || state.voice.audioContext.state === 'closed') return;
analyser.getByteFrequencyData(dataArray);
let sum = 0;
for (let i = 0; i < dataArray.length; i++) sum += dataArray[i];
const avg = sum / dataArray.length;
const isCurrentlySpeaking = avg > 30; // Calibrated for normal speech analyser.getFloatTimeDomainData(timeData);
if (isCurrentlySpeaking) { let sumSquares = 0;
speakCounter = Math.min(speakCounter + 1, 5); let peak = 0;
} else { for (let i = 0; i < timeData.length; i++) {
speakCounter = Math.max(speakCounter - 1, 0); const v = timeData[i];
sumSquares += v * v;
const abs = Math.abs(v);
if (abs > peak) peak = abs;
} }
const newSpeakingState = speakCounter >= 2; const rms = Math.sqrt(sumSquares / timeData.length);
const level = Math.max(rms, peak * 0.5);
levelEma = levelEma * 0.75 + level * 0.25;
if (!localIsSpeaking) {
// Learn room noise slowly while idle.
noiseFloor = noiseFloor * 0.98 + levelEma * 0.02;
} else {
// Do not let noise floor jump up while speaking.
noiseFloor = Math.min(noiseFloor, levelEma);
}
const startThreshold = Math.max(noiseFloor * 2.2, 0.010);
const stopThreshold = Math.max(noiseFloor * 1.5, 0.006);
if (levelEma > startThreshold) {
speechFrames = Math.min(speechFrames + 1, 8);
silenceFrames = 0;
} else if (levelEma < stopThreshold) {
silenceFrames = Math.min(silenceFrames + 1, 8);
speechFrames = Math.max(speechFrames - 1, 0);
}
const newSpeakingState = localIsSpeaking ? silenceFrames < 3 : speechFrames >= 2;
if (newSpeakingState !== localIsSpeaking) { if (newSpeakingState !== localIsSpeaking) {
localIsSpeaking = newSpeakingState; localIsSpeaking = newSpeakingState;
@ -858,7 +883,7 @@ async function buildAudioPipeline(rawStream) {
myVideo.parentElement.classList.toggle('speaking', localIsSpeaking); myVideo.parentElement.classList.toggle('speaking', localIsSpeaking);
} }
} }
setTimeout(checkVolume, 100); setTimeout(checkVolume, 60);
}; };
checkVolume(); checkVolume();

View file

@ -780,31 +780,56 @@ async function buildAudioPipeline(rawStream) {
} }
} }
// Metering/Speaking detection // Metering/Speaking detection with adaptive threshold and hysteresis.
const analyser = ctx.createAnalyser(); const analyser = ctx.createAnalyser();
analyser.fftSize = 512; analyser.fftSize = 1024;
processedSource.connect(analyser); processedSource.connect(analyser);
const dataArray = new Uint8Array(analyser.frequencyBinCount); const timeData = new Float32Array(analyser.fftSize);
let localIsSpeaking = false; let localIsSpeaking = false;
let speakCounter = 0; let speechFrames = 0;
let silenceFrames = 0;
let noiseFloor = 0.004;
let levelEma = 0;
const checkVolume = () => { const checkVolume = () => {
if (!state.voice.audioContext || state.voice.audioContext.state === 'closed') return; if (!state.voice.audioContext || state.voice.audioContext.state === 'closed') return;
analyser.getByteFrequencyData(dataArray);
let sum = 0;
for (let i = 0; i < dataArray.length; i++) sum += dataArray[i];
const avg = sum / dataArray.length;
const isCurrentlySpeaking = avg > 30; // Calibrated for normal speech analyser.getFloatTimeDomainData(timeData);
if (isCurrentlySpeaking) { let sumSquares = 0;
speakCounter = Math.min(speakCounter + 1, 5); let peak = 0;
} else { for (let i = 0; i < timeData.length; i++) {
speakCounter = Math.max(speakCounter - 1, 0); const v = timeData[i];
sumSquares += v * v;
const abs = Math.abs(v);
if (abs > peak) peak = abs;
} }
const newSpeakingState = speakCounter >= 2; const rms = Math.sqrt(sumSquares / timeData.length);
const level = Math.max(rms, peak * 0.5);
levelEma = levelEma * 0.75 + level * 0.25;
if (!localIsSpeaking) {
// Learn room noise slowly while idle.
noiseFloor = noiseFloor * 0.98 + levelEma * 0.02;
} else {
// Do not let noise floor jump up while speaking.
noiseFloor = Math.min(noiseFloor, levelEma);
}
const startThreshold = Math.max(noiseFloor * 2.2, 0.010);
const stopThreshold = Math.max(noiseFloor * 1.5, 0.006);
if (levelEma > startThreshold) {
speechFrames = Math.min(speechFrames + 1, 8);
silenceFrames = 0;
} else if (levelEma < stopThreshold) {
silenceFrames = Math.min(silenceFrames + 1, 8);
speechFrames = Math.max(speechFrames - 1, 0);
}
const newSpeakingState = localIsSpeaking ? silenceFrames < 3 : speechFrames >= 2;
if (newSpeakingState !== localIsSpeaking) { if (newSpeakingState !== localIsSpeaking) {
localIsSpeaking = newSpeakingState; localIsSpeaking = newSpeakingState;
@ -821,7 +846,7 @@ async function buildAudioPipeline(rawStream) {
myVideo.parentElement.classList.toggle('speaking', localIsSpeaking); myVideo.parentElement.classList.toggle('speaking', localIsSpeaking);
} }
} }
setTimeout(checkVolume, 100); setTimeout(checkVolume, 60);
}; };
checkVolume(); checkVolume();