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

@@ -69,6 +69,15 @@ if(VOICECAT_USE_VCPKG_DEPS)
target_include_directories(test_plc_cap PRIVATE ${VC_TEST_INTERNAL_INCLUDES})
add_test(NAME plc_cap COMMAND test_plc_cap)
# Jitter-buffer bounded depth: across many talkspurt/silence cycles with a compressed sender
# timeline + reordered stragglers, playout latency must stay bounded (no backward drift /
# ratchet). White-box AudioEngine test, no server needed.
add_executable(test_jitter_depth test_jitter_depth.cpp)
target_link_libraries(test_jitter_depth PRIVATE voicecat::voicecat)
target_compile_features(test_jitter_depth PRIVATE cxx_std_20)
target_include_directories(test_jitter_depth PRIVATE ${VC_TEST_INTERNAL_INCLUDES})
add_test(NAME jitter_depth COMMAND test_jitter_depth)
# External playback (iOS VPIO): the mixer-timer thread drives decode+mix with NO hardware
# device and delivers the final mix to the mixed-output sink. White-box AudioEngine test.
add_executable(test_external_playback test_external_playback.cpp)