fix(audio): bound playout depth to stop voice latency ratcheting up
Some checks failed
Build Linux Binaries / linux/amd64 (push) Has been cancelled
Build Linux Binaries / linux/arm64 (push) Has been cancelled

Latency between speakers grew to multiple seconds and only reset on
rejoining voice. Root cause was the receiver playout logic, not the
codec settings: the playout clock free-ran in real time while the
sender omitted silence from its timestamps (and set no header flags),
and the only correction snapped the clock to the *oldest* buffered
frame — which could only ever add standing latency. target_depth_ms_
was computed but never enforced, so latency could only grow or reset.

Fix: bound playout against the stream's leading edge (newest frame).
(Re)seed to the leading edge on start/marker/starve (no prebuffer, so
latency stays low), and frame-skip catch-up trims any backlog beyond
target+hysteresis — the missing downward force.

Hardening: sender now stamps kFlagMarker (talkspurt start) and kFlagDtx,
consumed on recv for clean resync; adaptive late-drop window; EWMA
outlier rejection so silence gaps/stragglers don't poison the estimate;
duplicate counting and ring-underrun diagnostics.

New test_jitter_depth asserts depth stays bounded (<200ms) while
arrivals outrun playback. ctest --preset dev green (27/27).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 20:02:20 +02:00
parent e155e342f4
commit ce2035f271
10 changed files with 384 additions and 49 deletions

View File

@@ -110,11 +110,14 @@ int main() {
}
CHECK(energy == 0); // capped PLC = silence
// 5) Resumption: push a fresh real frame — PLC streak resets, audio returns.
// 5) Resumption: push a fresh real frame — PLC streak resets, audio returns. marker=true is
// what the real sender stamps on the first frame after a silence (talkspurt restart); it
// makes the playout clock reseed to this leading edge immediately (no prebuffer delay).
voicecat::audio::JitterBuffer::Frame f2;
f2.seq = 1;
f2.timestamp = 200000; // far ahead — playout-clock re-sync snaps to it
f2.timestamp = 200000; // far ahead — playout-clock reseeds to it
f2.fec_present = false;
f2.marker = true;
f2.payload.assign(opus_buf, opus_buf + opus_len);
engine.push_recv_frame(ssrc, std::move(f2));