Skip to content

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:

  1. Underbed: AI background music or hold tones at low volume during the call.
  2. Translation: original-language audio ducks under TTS in the listener's language.
  3. 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:

  1. The listener loses awareness of the speaker's prosody, stress, and emotion.
  2. Conversational turn-taking breaks (the listener cannot hear the other speaker preparing to interrupt).
  3. 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:

  • baseDb 0 (full volume) is the default for primary.
  • duckExtraDb 12-18 is typical. Below 8, TTS becomes hard to hear over the original. Above 24, the original is effectively silent.
  • echoVolume 0.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.

See also