Discord: a call has no echo cancellation #232

Closed
opened 2026-09-23 15:16:24 -04:00 by Salastil · 1 comment
Owner

Anyone in a Discord call on speakers rather than headphones sends their own
output back into the channel, and nothing anywhere in the path breaks the loop.

The cause is an asymmetry between the two call stacks. A Matrix call is made in
the window, so its microphone comes from navigator.mediaDevices.getUserMedia
(src/renderer/src/lib/webrtc.ts:221) and arrives with Chromium's AEC3, noise
suppression and gain control already applied - they are on by default and cost
nothing to have. A Discord call is made in the daemon, so its microphone is a
raw cpal capture (nobilis/src/audio.rs:415, held by
backend/discord/voice.rs:92), and a raw capture has none of that. There is no
APM in the daemon at all: no AEC, no noise suppression, no AGC.

So the same person on the same hardware is clean on Matrix and echoes on
Discord, which is the confusing way round - it reads as a Discord problem
rather than a microphone one.

Found while testing a Go Live stream between two accounts on one machine, where
it is at its loudest, but the self-test only makes it obvious. It is there for
every real call too.

The fix is an APM in the daemon between the capture and the encoder. The
webrtc-audio-processing crate wraps the same AEC3 Chromium uses, and it needs
the playback stream as its reference signal - which voice.rs already holds
beside the capture, in playbacks next to captures. So the two signals it
has to line up are already in the same struct.

This lands under every Discord call, not just Go Live, so it is worth having
before #231 and before the viewing half of #14.

Anyone in a Discord call on speakers rather than headphones sends their own output back into the channel, and nothing anywhere in the path breaks the loop. The cause is an asymmetry between the two call stacks. A Matrix call is made in the window, so its microphone comes from `navigator.mediaDevices.getUserMedia` (`src/renderer/src/lib/webrtc.ts:221`) and arrives with Chromium's AEC3, noise suppression and gain control already applied - they are on by default and cost nothing to have. A Discord call is made in the daemon, so its microphone is a raw cpal capture (`nobilis/src/audio.rs:415`, held by `backend/discord/voice.rs:92`), and a raw capture has none of that. There is no APM in the daemon at all: no AEC, no noise suppression, no AGC. So the same person on the same hardware is clean on Matrix and echoes on Discord, which is the confusing way round - it reads as a Discord problem rather than a microphone one. Found while testing a Go Live stream between two accounts on one machine, where it is at its loudest, but the self-test only makes it obvious. It is there for every real call too. The fix is an APM in the daemon between the capture and the encoder. The `webrtc-audio-processing` crate wraps the same AEC3 Chromium uses, and it needs the playback stream as its reference signal - which `voice.rs` already holds beside the capture, in `playbacks` next to `captures`. So the two signals it has to line up are already in the same struct. This lands under every Discord call, not just Go Live, so it is worth having before #231 and before the viewing half of #14.
Salastil added the bug label 2026-09-23 15:16:24 -04:00
Author
Owner
Finished https://git.salastil.com/Salastil/nobilis/commit/72ef1b5 https://git.salastil.com/Salastil/moho/commit/ccd9936
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Salastil/moho#232