Audio ducking¶
Status: Current | Last reviewed: 2026-05-28
How media-proxy mixes multiple audio streams without producing noise.
What ducking is¶
Lowering one audio stream's volume while another speaks. Three uses in TrueServe:
- Underbed: AI background music or hold tones at low volume during the call.
- Translation: original-language audio ducks under TTS in the listener's language.
- Background mix: arbitrary configured background sounds.
All three live in apps/media-proxy/internal/audio/mix.go.
The functions¶
MixUnderbed¶
MixUnderbed(primary, underbed []int16, baseDb, duckExtraDb float32) []int16
Plays underbed at baseDb while primary is silent. When primary has signal, underbed ducks an additional duckExtraDb lower over a smoothing ramp.
Used for hold music, on-call ambient sound, "AI is thinking" cues.
MixTranslationStyle¶
MixTranslationStyle(original, translation []int16, baseDb, duckExtraDb, echoVolume float32) []int16
Same shape as MixUnderbed plus echoVolume, which is a separate gain applied to original before mixing — used to suppress speaker self-echo.
The fade ramp is translationFadeTimeMs (default 20 ms), defined as a constant near the top of mix.go.
MixWithBackground¶
MixWithBackground(primary, background []int16, gainDb float32) []int16
Static mix without ducking. background always plays at gainDb, regardless of primary.
Why duck instead of cut¶
If you cut the original audio entirely while TTS plays, three things go wrong:
- The listener loses awareness of the speaker's prosody, stress, and emotion.
- Conversational turn-taking breaks (the listener cannot hear the other speaker preparing to interrupt).
- The audio sounds robotic and frame-by-frame switching causes clicks.
Ducking keeps the original audible at low volume and ramps smoothly. The listener stays oriented; the dominant audio is still clear.
Choosing duck depth¶
Tunable per pipeline via transfer or translation config:
baseDb0 (full volume) is the default forprimary.duckExtraDb12-18 is typical. Below 8, TTS becomes hard to hear over the original. Above 24, the original is effectively silent.echoVolume0.0-0.3 suppresses speaker echo without killing context. Translation pipelines usually set this to 0.1-0.2.
Where the values come from¶
media-proxy reads the active values from per-call session config sent over its gRPC control channel. ai-orchestrator populates them when building the pipeline, pulling from agent_versions.config.audio (per-pipeline overrides) or falling back to defaults in service_constants.py.
Signal-detection logic¶
The "primary has signal" check uses an RMS threshold over a windowed buffer. False positives (background hiss triggering ducking) are suppressed by:
- A noise floor measured at call start (first 300 ms).
- A minimum sustained signal duration (~50 ms) before the duck engages.
These are implementation details of MixUnderbed and MixTranslationStyle; see comments in mix.go for the exact constants.
Multi-source mixing¶
When more than two streams are active (e.g., translation TTS + underbed music + caller voice), mix functions chain:
out := MixUnderbed(callerVoice, underbedMusic, 0, 12)
out = MixTranslationStyle(out, translationTTS, 0, 18, 0.15)
Order matters. Mix the most-suppressible first; the final mix's "primary" is what governs the strongest duck.
Testing¶
Translation and underbed mixing have unit tests in apps/media-proxy/internal/audio/ that verify gain math against expected output buffers. Run:
cd apps/media-proxy && go test ./internal/audio/...
For end-to-end audio quality testing in Vanguard, use the in-browser phone emulator and compare recordings; there is no automated A/B harness yet.