This commit is contained in:
parent
4d1bea502a
commit
72a10ad32b
2 changed files with 78 additions and 28 deletions
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue