fix(desktop): huddle playout drops good audio on ~half of all ticks - #2652
fix(desktop): huddle playout drops good audio on ~half of all ticks#2652sboily wants to merge 1 commit into
Conversation
|
For context: I found this while building a Python client library for Buzz (text + voice huddles): https://github.com/sboily/buzzkit. I was bridging an AI voice agent into a huddle, and its clean synthesized speech made the constant 10 ms cuts much easier to hear than regular mic audio — that's what led me to trace the receive path down to this queue. |
The playout queue high-water (4 frames) sits inside the queue's normal depth range: the device callback consumes in bursts that beat against the 10 ms producer tick, so Player::len() oscillates between 1 and 6 on a healthy machine (measured: len >= 4 on 49% of appends with a standalone 100 x 10 ms-buffers/s feed). skip_one() therefore fired constantly during normal playback, cutting 10 ms of good audio each time — heard as permanently choppy huddle voice on affected devices. Raise the high-water to 16 frames (160 ms, still under NetEq's max_delay_ms of 200 ms) and, when it trips, drain to 8 frames bounded by count — skip_one() only takes effect on the device callback thread, so a len()-polling loop spins while the output stream is starting. Signed-off-by: Sylvain Boily <sylvainboilydroid@gmail.com>
8ee841b to
5d62ecb
Compare
|
I did some test with other users and they also have a choppy sound. Could be great if you have time to review because sound is not useable currently. |
|
Closing this as superseded — the root cause is now fixed on b29c8cd ("feat(desktop): redesign the Huddle experience", #4281, merged 2026-08-04) replaced the whole drift-bounding mechanism in const PLAYOUT_QUEUE_RECOVERY_START: usize = 10;
const PLAYOUT_QUEUE_RECOVERY_END: usize = 4;
const PLAYOUT_QUEUE_EMERGENCY_HIGH_WATER: usize = 30;
const PLAYOUT_RECOVERY_SPEED: f32 = 1.02;The steady-state hard drop is gone. Producer-vs-device clock drift is now absorbed by playing 2% faster with hysteresis (engages at depth 10, disengages at 4), and Point by point against what this PR argued:
That makes #4281 strictly better than what's proposed here: this PR still shed 8 frames at once whenever the raised threshold of 16 tripped, whereas the current code discards no audio at all under normal conditions. Rebasing isn't worth it — both hunks collide head-on with the rewritten block, and carrying them forward would reintroduce the hard drop that #4281 deliberately removed. Thanks to whoever picked this up in the redesign. Leaving the original measurement here for the record, since it's what pinned the natural queue range: with a standalone 100 × 10 ms-buffers/s feeder on a default MacBook output, |
The 10 ms playout tick uses MissedTickBehavior::Delay, which never shortens the ticks that follow a missed one. Windows timers default to a 15.6 ms resolution and tokio intervals fire ~14.6 ms late there on average (tokio-rs/tokio#5021), so the loop settles at ~62 of the 100 pulls/s the pipeline needs. The per-peer rodio queues run dry (audible gaps, dropped words) while NetEq stays full and time-compresses playback (metallic, sped-up voices) - on every peer, regardless of network quality. Matches the choppy-audio reports in block#2652; block#4281 changed the drop threshold but not the tick rate. Measured on Windows 11 with a standalone reproduction of this loop: 62.4 ticks/s with Delay at default resolution, 100.2 ticks/s with timeBeginPeriod(1) raised for the loop lifetime and Burst making up missed ticks. Catch-up bursts are absorbed by the existing queue recovery (hysteresis 10-4, emergency trim at 30). Signed-off-by: kaalph <138721439+kaalph@users.noreply.github.com>
The 10 ms playout tick uses MissedTickBehavior::Delay, which never shortens the ticks that follow a missed one. Windows timers default to a 15.6 ms resolution and tokio intervals fire ~14.6 ms late there on average (tokio-rs/tokio#5021), so the loop settles at ~62 of the 100 pulls/s the pipeline needs. The per-peer rodio queues run dry (audible gaps, dropped words) while NetEq stays full and time-compresses playback (metallic, sped-up voices) - on every peer, regardless of network quality. Matches the choppy-audio reports in block#2652; block#4281 changed the drop threshold but not the tick rate. Measured on Windows 11 with a standalone reproduction of this loop: 62.4 ticks/s with Delay at default resolution, 100.2 ticks/s with timeBeginPeriod(1) raised for the loop lifetime and Burst making up missed ticks. Catch-up bursts are absorbed by the existing queue recovery (hysteresis 10-4, emergency trim at 30). Signed-off-by: kaalph <138721439+kaalph@users.noreply.github.com>
Problem
Huddle voice sounds constantly choppy on some machines. The playout queue high-water is 4 frames, but the rodio
Playerqueue naturally oscillates between 1 and 6 during perfectly healthy playback — the device callback consumes in bursts that beat against the 10 ms producer tick. Soskip_one()fires on roughly half of the ticks, and each hit cuts 10 ms out of the speech.Easy to see live: stderr spams
playout queue high-water … dropping oldest frameat depth 4-5 the whole time a peer is speaking. Measured with a standalone feeder (one 10 ms buffer appended every 10 ms, default MacBook output):Player::len() >= 4on 49% of appends.Fix
max_delay_msof 200 ms), well above the natural oscillation.skip_one()only takes effect on the device callback thread, so awhile len() > targetloop spins while the output stream is starting (hit this at huddle join).Testing
cargo fmt,cargo clippy, and the 92 huddle unit tests pass.