A voice-guided app in the browser asks the user questions out loud and streams the answers to a hosted realtime speech-to-text service over a websocket. The same session also warns when the room is too loud to hear them clearly. People in cafes, open offices, and busy homes hit that combination often, and the team was deciding whether to turn on a neural denoiser in front of the speech pipeline.
If you speak in a noisy room, you can get no answer back and no loud-room banner, so it feels like the app simply did not hear you. During evaluation with the denoiser enabled, that banner stopped showing in exactly the rooms we wrote it for. One level meter still reported plausible numbers, and nothing in the test suite turned red.
One analyser, two jobs
The mic path uses the Web Audio API: an AudioContext, a MediaStream from getUserMedia, optionally an AudioWorklet running DeepFilterNet3 at 48 kHz, then downsampled audio to ElevenLabs Scribe realtime at 16 kHz. Voice activity detection (VAD) is the gate that decides whether someone is speaking before audio is sent. WER is the share of words the transcript gets wrong, the metric we used to judge the denoiser.
An AnalyserNode is the Web Audio node you read volume from. The trap is wiring one analyser on the cleaned stream and feeding both VAD and the loud-room notice from that single reading. A denoiser is a model that strips non-speech sound before anything else hears the signal. It feels like a drop-in win at one node in the graph. Every downstream reader silently changes meaning.
In the lab I wired getUserMedia through a denoise worklet into one AnalyserNode that drove both VAD and showNoiseNotice(level >= LOUD). Play pink noise at a level that used to trigger the notice. With the denoiser on, the level stays low and the banner never shows.
Turn the denoiser on, drag the noise slider all the way up, and watch the notice tile. Then switch the notice to the raw level and drag again.
Simulation
Denoiser vs room noise notice
One AnalyserNode feeding both voice activity detection and a loud-room banner. Denoiser strength is illustrative.
Levels in the graph
Room notice
Notice reads level
19
Value passed to showNoiseNotice
Loud-room notice
Hidden
Threshold 70
Simulated pink noise at the mic, no real audio.
Split meters before you split flags
Speech decisions should keep the cleaned signal. Anything that reports on the environment needs the raw mic.
Before:
const source = ctx.createMediaStreamSource(denoised ?? mic)
source.connect(analyser)
onLevel(readLevel(analyser)) // VAD and the noise notice both read thisAfter:
const source = ctx.createMediaStreamSource(denoised ?? mic)
source.connect(analyser)
const rawAnalyser = denoised ? ctx.createAnalyser() : null
if (rawAnalyser) ctx.createMediaStreamSource(mic).connect(rawAnalyser)
onLevel(readLevel(analyser), rawAnalyser ? readLevel(rawAnalyser) : undefined)
function noticePeak(cleaned: number, raw?: number) {
return raw !== undefined ? raw : cleaned
}
showNoiseNotice(noticePeak(cleaned, raw) >= LOUD)Keep any recording on the raw stream. Enable the denoiser only when the feature flag is exactly the string "true". If the model fails to load, the browser lacks AudioWorklet, or the worklet reports overload, fall back to the browser's built-in noise suppression instead of pretending the neural path is live.
When nothing in the app reports on the environment (no loud-room copy, no gain or mic-health UI), one cleaned analyser is enough. Skip the raw analyser when the denoiser is off, because both would read the same number.
Measure with a win rule first
Before we ran clips we agreed a win rule, lower WER in noise without hurting clean speech and no junk transcripts on noise-only takes. On 58 test clips the denoiser raised WER in a noisy room from 7.4% to 10.3% and on clean speech from 5.1% to 5.8%. Noise-only clips picked up stray one-word transcripts where the existing setup produced none. Neither option kept a second speaker's words out of the stream. The flags stayed off. Scribe's own background filter cannot be combined with word timestamps, which mattered for alignment work elsewhere.
You get the silent banner if you ship the denoiser without the raw meter. We left the flags off after those numbers, so production never saw that wiring.
When you clean a signal, inventory every reader of it. Environment copy and health UI read raw input. A denoiser earns its place only against a win rule written before the test.
Happy coding!
Sander