Skip to content

VAD needs an echo reference on the SIP/PCMU path (agent barges on its own voice) #48

Description

@akoyasoft

Filed after porting the turn-taking layer onto a telephony path (8 kHz µ-law, no
WebRTC). Raising it here because sip-server puts StreamCore on the same path.

Context: https://x.com/jasonshen_/status/2089925943385366962

Symptom

The energy VAD fires barge-in on the agent's own voice. It shows up as an
interruption with no caller turn behind it: the reply is cut mid-sentence,
nothing arrives to replace it, and the call sits in dead air until some other
recovery path unsticks it.

Why WebRTC hides this

A browser runs AEC before the audio ever reaches the server, so internal/vad
never sees the agent's own output coming back. Over a carrier there is no AEC
anywhere in the path. The returning audio is attenuated but structurally
identical to speech, and an RMS-vs-noise-floor test cannot separate the two —
adaptiveMinThreshold and noiseFloorMultiplier are both tuned against
background noise, which this isn't.

What worked for us

Not a higher threshold. That trades self-barging for being deaf to quiet
callers, and the adaptive floor is already doing as much as a single-signal
test can.

Instead, use the one reference the server always has: what it just sent.

  • keep a rolling window (~400 ms) of the RMS of outbound audio
  • floor = recent_tx_rms × gain (gain ≈ 0.6 — echo can't exceed what produced it)
  • treat inbound as an interruption only when inbound ≥ floor × margin (margin ≈ 1.8)

The margin is what separates double-talk from echo. Sensitivity to a quiet
caller on a clean line is untouched, because when the agent is silent the floor
is zero and the ordinary adaptive threshold governs.

One wrinkle if you keep ducking

Outbound RMS is naturally recorded at enqueue, but the duck is applied at send
(encodeAndSend). While ducked, what is actually on the wire is ~12 dB quieter,
so an un-scaled echo floor holds the confirm threshold high exactly when you
need to hear whether the caller is still talking. Scale the floor by the duck
gain while audioMuted is set.

Unrelated, same path, and it sent us down the wrong layer

Inbound went completely silent during agent playback and looked like half-duplex
media. We chased our own reader first, then transcode, then the caller's handset,
and had the PBX vendor confirm the endpoint was full-duplex by design before we
found it. Root cause was DTX — G.729 Annex B / OPUS DTX suppressing "silent" RTP,
which on that path meant all inbound during playback. Forcing G.711-only
(PCMU/PCMA) end to end fixed it.

Worth knowing before someone debugs their VAD for it: no amount of threshold
tuning helps when the frames aren't being sent.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions