Skip to content

Audio: stop blocking the real-time playback callback - #1803

Open
Mariusz Białończyk (manio) wants to merge 3 commits into
Devolutions:masterfrom
manio:audio-ringbufer
Open

Audio: stop blocking the real-time playback callback#1803
Mariusz Białończyk (manio) wants to merge 3 commits into
Devolutions:masterfrom
manio:audio-ringbufer

Conversation

@manio

Copy link
Copy Markdown
Contributor

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.

Fixes for this kind of errors:

2026-08-26T04:19:43.004416Z ERROR ThreadId(54) crates/ironrdp-rdpsnd-native/src/cpal.rs:473: error=Buffer underrun/overrun occurred.
2026-08-26T04:19:43.055013Z ERROR ThreadId(54) crates/ironrdp-rdpsnd-native/src/cpal.rs:473: error=Buffer underrun/overrun occurred.
2026-08-26T04:19:43.104671Z ERROR ThreadId(54) crates/ironrdp-rdpsnd-native/src/cpal.rs:473: error=Buffer underrun/overrun occurred.
2026-08-26T04:19:47.109771Z  WARN ThreadId(54) crates/ironrdp-rdpsnd-native/src/cpal.rs:540: error=timed out waiting on channel
2026-08-26T04:19:47.109844Z ERROR ThreadId(54) crates/ironrdp-rdpsnd-native/src/cpal.rs:473: error=Buffer underrun/overrun occurred.
2026-08-26T04:19:48.950520Z  WARN ThreadId(54) crates/ironrdp-rdpsnd-native/src/cpal.rs:540: error=channel is empty and sending half is closed
2026-08-26T04:19:49.375421Z ERROR ThreadId(56) crates/ironrdp-rdpsnd-native/src/cpal.rs:473: error=Buffer underrun/overrun occurred.
2026-08-26T04:19:49.489871Z ERROR ThreadId(56) crates/ironrdp-rdpsnd-native/src/cpal.rs:473: error=Buffer underrun/overrun occurred.
2026-08-26T04:19:49.593537Z ERROR ThreadId(56) crates/ironrdp-rdpsnd-native/src/cpal.rs:473: error=Buffer underrun/overrun occurred.
2026-08-26T04:19:49.679003Z ERROR ThreadId(56) crates/ironrdp-rdpsnd-native/src/cpal.rs:473: error=Buffer underrun/overrun occurred.

`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>
@manio

Copy link
Copy Markdown
Contributor Author

@github-actions github-actions Bot added maintainer-required Maintainer review or intervention is required risk/unknown Risk could not be determined automatically; needs maintainer-level scrutiny size/M Size: up to 449 counted lines and 10 files; exceeds S in either measure labels Aug 26, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread crates/ironrdp-rdpsnd-native/src/cpal.rs
Comment thread crates/ironrdp-rdpsnd-native/src/cpal.rs Outdated
Comment thread crates/ironrdp-rdpsnd-native/src/cpal.rs
Comment thread crates/ironrdp-rdpsnd-native/Cargo.toml Outdated
- 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
@manio

Copy link
Copy Markdown
Contributor Author

Addressed, thanks!

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

Labels

maintainer-required Maintainer review or intervention is required risk/unknown Risk could not be determined automatically; needs maintainer-level scrutiny size/M Size: up to 449 counted lines and 10 files; exceeds S in either measure

Development

Successfully merging this pull request may close these issues.

2 participants