Browser voice AI fails on audio routing: the echo canceller cannot see Web Audio playback
One developer's catalogue of 16 traps: browser echo cancellation only references audio-element and WebRTC playback, so TTS routed through Web Audio guarantees the bot hears itself.
The Engineer · Build desk
What happened
- A developer's guide, republished on dev.to from a Japanese original, catalogues 16 browser voice AI traps in symptom, cause, solution form.
- The author's fix routes TTS back as a WebRTC remote track played in an audio element, reporting 99 seconds of speech over open speakers with zero false user turns.
- A separate trap: the first 10 to 20 seconds go unanswered because AEC only learns during playback and autoGainControl ramps up from quiet.
Why it matters
- What it limits constraint Anything that needs the TTS waveform inside a Web Audio graph, lip sync included, is paying for it with the echo canceller. The playback route cannot serve both.
- Who has to decide decision Stacks doing browser-side synthesis over a websocket now have to choose between that design and usable speaker mode, because the working fix assumes a server media path.
- What it costs cost Curing the loop relocates the error into recognition, and the user pays it as mangled word beginnings whenever they interrupt, with an LLM guess as the last line of defence.
- Who's exposed exposure Agents that open media in other tabs are unprotectable in software; what is left is user hardware and OS settings that a product team cannot guarantee are present.
Echo cancellation is a subtraction job, and it needs a copy of the thing being subtracted. In a browser that copy arrives only from paths the browser itself owns: an `<audio>` element, or a WebRTC receiver track. A Web Audio graph does not reliably supply one [4]. So when TTS leaves through a buffer source node because someone wanted sample-accurate amplitude for a mouth shape, the canceller has nothing to work against. The mic hears the synthetic voice cleanly and the recognizer files it as a user turn [5]. No threshold downstream separates that from a person, because without a reference signal a speaker's output and a human voice look the same at the microphone [6].
The usual patch is text matching: hold what TTS just said and drop transcripts that resemble it. The author's fix skips that layer, sending TTS from the server as a WebRTC remote track played through an `<audio>` element, reported as 99 seconds of continuous speech over open speakers with zero false user turns [1]. Read what that sentence requires: a server-side media path, a remote track, and an element to consume it. Client-side synthesis and direct buffer access are what you hand back.
Convergence is the same mechanism one order down. The canceller learns only while audio is actually playing, so a session that opens in silence begins with an untrained filter and the user's first 10 to 20 seconds get suppressed along with the echo, with autoGainControl ramping up from quiet making that same window worse [8]. The remedy is to make noise on purpose: a greeting or sound effect during the loading screen, AGC set to false, echoCancellation left alone [9]. The spinner is now part of the audio stack.
The media element turns out to be mandatory in the other direction too. Chromium will not hand a remote MediaStream to Web Audio until some media element starts consuming it, so even an analysis-only graph needs a muted `<audio>` with play() called on it, plus a held reference, since Chrome will collect unreferenced nodes and stop the graph without raising an error [12][13].
What remains after the routing is corrected is a genuine trade. AEC suppresses and distorts near-end audio during simultaneous speech, which is why proper nouns come apart at word beginnings when a user talks over the avatar [10]. The mitigations run three deep and the last of them is instructing the LLM to treat its input as error-prone transcription and reach for phonetically similar terms [11]. Audio from another process, including a video the agent opened itself, has no reference at all and so has no fix; headphones, macOS voice isolation that unsupported systems quietly ignore, and downstream filtering are the entire toolkit [7].
Four of the eight traps reproduced in this excerpt come back to where the reference signal comes from or whether it has converged [16]. The instrumentation advice follows from that: server-side energy logging at 10 Hz, one sample every 100 ms, tells distorted audio from true silence, and a meter on the transmitted stream earns its place because a muted track and an empty room are identical on the wire [2][15][14].
What to watch
- Independent reproduction of the 99-second, zero-false-turn result, which is currently one developer's bench test on one stack.
- Any move by Chromium to expose a Web Audio playback path as a valid AEC reference, which would remove the routing constraint entirely.
- Whether voiceIsolation gains support beyond macOS, since other-process audio has no software answer today.
Clarity's read
What the record supports and how the coverage leans. The claims behind it follow.
Reality
- Evidence38
- Adoption12
- Hype gap+8
- Incentives28
- Confidence42
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
Solution: return TTS audio from the server as a WebRTC remote track and play it via an <audio> element. This eliminates echoes without text-matching workarounds; tested with 99 seconds of continuous speech with speakers on and zero false user turn detections.
- [2]
Recommended observability: server-side logging of input audio energy or probability at 10 Hz to distinguish complete silence from distorted audio, plus a UI level meter on the actual transmitted stream read via createMediaStreamSource, which does not affect transmission.
- [3]
A developer writeup published on dev.to, originally in Japanese at forge.workstyle.tech, compiles 16 traps encountered while building browser voice AI (avatars, voice bots, streaming AI), organised as symptom, cause, solution.
- [4]
AEC (acoustic echo cancellation) requires a reference signal, the sound to cancel. Only the browser's official playback paths (<audio> elements and WebRTC receiver tracks) serve as references; custom playback via the Web Audio API does not reliably function as a reference.
- [5]
Symptom: TTS audio is picked up by the mic and STT recognises it as user speech, creating a self-response loop.
- [6]
Browser AEC can only reference audio played by the same tab or app; audio from other processes is indistinguishable from human speech to the mic, so audio and lyrics from a YouTube video the agent opened keep getting transcribed by STT.
- [7]
For audio from other processes there is no technical silver bullet: combine OS speaker separation (for example macOS Voice Isolation, requested via voiceIsolation: true, which is ignored on unsupported systems), headphones, and downstream noise rejection by LLM or tool-based filtering.
- [8]
Symptom: the first 10 to 20 seconds of speech go unanswered, then it works normally. Causes are AEC convergence, because AEC only learns during actual playback so unconverged dual speech suppresses the user's voice, and autoGainControl ramp-up, which makes initial speech too quiet.
- [9]
Solution: play short greetings or sound effects before the main session to train AEC, with loading screens ideal for this; set autoGainControl to false, which has minimal downsides when STT is server-side; never disable echoCancellation.
- [10]
Fundamental AEC trade-off: to cancel echoes, AEC suppresses and distorts near-end (user) audio during dual speech, so proper nouns get mangled during simultaneous speech, especially at word beginnings.
- [11]
Three-layer mitigation for dual-speech misrecognition: raise mic Opus bitrate and enable FEC; give STT vocabulary hints using recent avatar speech as initial_prompt rather than a dictionary; instruct the LLM that input is STT transcription with potential errors, to interpret unnatural words as phonetically similar terms and add confirmation prompts.
- [12]
Chromium does not send a remote MediaStream to Web Audio until a media element starts consuming it, long-standing behaviour; the workaround is to attach srcObject to a muted <audio> element and call play(), holding the reference to prevent garbage collection.
- [13]
Chrome may garbage collect unreferenced audio nodes, silently stopping input to the processing graph with no errors after it has worked for a while.
- [14]
A silent track looks indistinguishable from a quiet room to the server; implementations that keep the mic off until a button press clash with user expectations of always listening, and the fix is to align defaults with the product promise, auto-enabling on initialisation completion rather than during connection and keeping the button as a mute toggle.
- [15]
Logging input audio energy at 10 Hz is one sample every 100 milliseconds.
- [16]
Four of the eight symptom entries reproduced in the source excerpt trace to how AEC obtains or converges its reference signal: the self-response loop, dual-speech distortion, other-process audio, and the unanswered first 10 to 20 seconds.
Get this ledger inside ChatGPT. Ask about any of our stories and the answer brings its figure, each claim’s verdict and the sources. Free, no account.
Sources
1 independent publisher whose own reporting we read for this story.
Topics and entities
Follow any of these and your For You feed starts watching them — no settings page required.