Audio: stop blocking the real-time playback callback - #1803
Open
Mariusz Białończyk (manio) wants to merge 3 commits into
Open
Audio: stop blocking the real-time playback callback#1803Mariusz Białończyk (manio) wants to merge 3 commits into
Mariusz Białończyk (manio) wants to merge 3 commits into
Conversation
Mariusz Białończyk (manio)
deployed
to
llm-providers
August 26, 2026 06:27 — with
GitHub Actions
Active
`RxBuffer::fill()` ran on the cpal real-time audio thread and called `Receiver::recv_timeout(Duration::from_millis(4000))` whenever it ran out of buffered PCM. Any pause in incoming Wave PDUs longer than one callback period — completely normal whenever nothing is currently playing remotely — blocked the audio device's callback thread for up to 4 seconds, starving its ring buffer and triggering a cpal "Buffer underrun/overrun occurred" error plus a "timed out waiting on channel" warning, repeating every ~4s for as long as playback stayed idle. Replace the `mpsc::Receiver<Vec<u8>>` between the producer (network/ decode) and the consumer (cpal callback) with a lock-free SPSC byte ring buffer (`ringbuf`). `RxBuffer::fill()` now calls `pop_slice`, which is non-blocking and returns immediately with whatever is available; any shortfall is filled with correct silence (0x80 for unsigned 8-bit PCM, 0x00 otherwise) instead of blocking or leaving stale bytes. Volume is still applied at pop time, right before playback, preserving the existing responsiveness of VolumePdu changes. This also drops the old `last`/`idx` partial-block bookkeeping in `RxBuffer` (the ring buffer handles partial reads natively), and moves Opus decoding off the `stream_handle` thread into its own thread spawned directly from `wave()`, pushing decoded PCM into the same ring buffer non-blockingly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Occasional small drops (a few tens to ~100 bytes, roughly every 10-20s) are expected: the server's audio clock and the local playback device never run at exactly the same rate, and with no resampling in this path, that drift is absorbed by periodically dropping a sub-millisecond sliver of audio instead of letting the buffer grow unbounded. This is harmless and inaudible, but was logged at WARN every time, which is needlessly noisy. Only log at WARN when a drop is large enough to suggest an actual stall on the audio thread (e.g. the machine briefly starving the real-time callback); log everything smaller at trace instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mariusz Białończyk (manio)
force-pushed
the
audio-ringbufer
branch
from
August 26, 2026 06:29
484c570 to
4bb537a
Compare
Contributor
Author
Mariusz Białończyk (manio)
deployed
to
llm-providers
August 26, 2026 06:30 — with
GitHub Actions
Active
Marc-André Moreau (mamoreau-devolutions)
requested changes
Aug 26, 2026
Marc-André Moreau (mamoreau-devolutions)
left a comment
Contributor
There was a problem hiding this comment.
Requesting changes for the example and workspace-lint build failures, plus the feature-gating and public-dependency issues noted inline.
Note
LLM-assisted content (no human feedback).
- Update examples/cpal.rs to build/split a HeapRb instead of an mpsc channel, matching DecodeStream::new's HeapCons<u8> signature. - Use a checked usize::try_from conversion for the drop-size threshold instead of an 'as usize' cast, matching ring_buffer_capacity and satisfying clippy::as_conversions. - Mark the ringbuf dependency '# public' since HeapCons<u8> is part of DecodeStream::new's public signature. - Silent warning when compiling without `opus` feature
Contributor
Author
|
Addressed, thanks! |
Mariusz Białończyk (manio)
deployed
to
llm-providers
August 26, 2026 16:51 — with
GitHub Actions
Active
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
RxBuffer::fill()ran on the cpal real-time audio thread and calledReceiver::recv_timeout(Duration::from_millis(4000))whenever it ran out of buffered PCM. Any pause in incoming Wave PDUs longer than one callback period — completely normal whenever nothing is currently playing remotely — blocked the audio device's callback thread for upto 4 seconds, starving its ring buffer and triggering a cpal "Buffer underrun/overrun occurred" error plus a "timed out waiting on channel" warning, repeating every ~4s for as long as playback stayed idle.
Replace the
mpsc::Receiver<Vec<u8>>between the producer (network/decode) and the consumer (cpal callback) with a lock-free SPSC byte ring buffer (ringbuf).RxBuffer::fill()now callspop_slice, which is non-blocking and returns immediately with whatever is available; any shortfall is filled with correct silence (0x80 for unsigned 8-bit PCM, 0x00 otherwise) instead of blocking or leaving stale bytes. Volume is still applied at pop time, right before playback, preserving the existing responsiveness of VolumePdu changes.This also drops the old
last/idxpartial-block bookkeeping inRxBuffer(the ring buffer handles partial reads natively), and moves Opus decoding off thestream_handlethread into its own thread spawned directly fromwave(), pushing decoded PCM into the same ring buffer non-blockingly.Fixes for this kind of errors: