Skip to content

alsa: refactor and enhance worker loop - #1122

Merged
roderickvd merged 12 commits into
masterfrom
fix/alsa-capture-overruns
Mar 15, 2026
Merged

roderickvd merged 12 commits into
masterfrom
fix/alsa-capture-overruns

Conversation

@roderickvd

@roderickvd roderickvd commented Mar 3, 2026 •

Copy link
Copy Markdown
Member

Fixes

  • Capture overrun recovery (ALSA: invalid handling of capture overruns #730): snd_pcm_start() was not called after snd_pcm_prepare() when recovering a capture stream from an underrun.

  • POLLERR mishandled as error: POLLERR from snd_pcm_poll_descriptors_revents() signals a pending underrun. It was being returned as BackendSpecificError, causing spurious error callbacks and skipping recovery.

  • poll() timeout reported as error: poll() returning 0 (timeout or spurious wakeup) was fired as a BackendSpecificError on every timeout .

  • Device disconnection looping: POLLHUP/POLLNVAL were not handled. The worker now stops with StreamError::DeviceNotAvailable on device removal.

  • SIGPIPE on early worker exit: If the worker exited early (e.g. on DeviceNotAvailable) before Stream::drop() called wakeup(), the write to the self-pipe's closed read end raised SIGPIPE

  • EINTR panic in self-pipe paths: write()/read() on the self-pipe could return -1 with EINTR on signal delivery, causing assert_eq!(ret, 8) to panic.

Added

Suspend/resume support: ESTRPIPE from avail() or writei() (system suspend) is now handled.

Changed

The internal PollDescriptorsFlow enum and poll_descriptors_and_prepare_buffer function have
been refactored for simplicity and likeness to Ready/Pending vocabulary.

Fixes #730

POLLERR from snd_pcm_poll_descriptors_revents() signals an xrun but was
being returned as a BackendSpecificError, causing the poll loop to spin
indefinitely. It now falls through to avail(), which returns EPIPE and
triggers xrun recovery.

Capture streams also require an explicit snd_pcm_start() after
snd_pcm_prepare() to re-enter SND_PCM_STATE_RUNNING; without it the
stream stalled in PREPARED and poll() timed out repeatedly.

POLLHUP/POLLNVAL now stop the stream with StreamError::DeviceNotAvailable
instead of looping and poll() returning 0 (timeout/spurious) is now
treated as Continue rather than an error.

Fixes #730

This comment was marked as outdated.

@roderickvd

Copy link
Copy Markdown
Member Author

The Copilot review seems a valid concern and a pre-existing fragility. I'll think about how to address it.

TriggerReceiver is now wrapped in Arc and a clone is stored in Stream
alongside the sender. Worker threads take their own Arc clone, so
dropping the worker no longer closes the read end of the pipe.

This means wakeup() in Stream::drop() always writes to an open pipe,
even when the worker exited early due to a device error, eliminating
the SIGPIPE that would otherwise be raised.
@roderickvd roderickvd changed the title fix(alsa): correct handling of capture overruns alsa: refactor and enhance worker loop Mar 6, 2026
@roderickvd
roderickvd requested a review from Copilot March 6, 2026 20:20

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/host/alsa/mod.rs Outdated
@roderickvd

Copy link
Copy Markdown
Member Author

This PR has grown quite a bit beyond its original #730 scope so I updated the PR title and text.

@roderickvd
roderickvd merged commit 5931fa2 into master Mar 15, 2026
31 checks passed
@roderickvd
roderickvd deleted the fix/alsa-capture-overruns branch March 15, 2026 19:02
crmne added a commit to crmne/zapfast that referenced this pull request Oct 15, 2026
)

cpal 0.17's ALSA backend turns an underrun that leaves the device in its
XRUN state into "`alsa::poll()` returned POLLERR" and polls again without
calling prepare(), so the error comes straight back for as long as the
stream lives, and rodio's default error callback prints each one to
stderr: tens of thousands of lines a second and a core held until
restart. cpal 0.18 recovers it (RustAudio/cpal#1122).

Every output stream now reports to an audio::Health instead of rodio's
default callback. It logs the first error and then a count at most every
ten seconds; dozens of errors in a second, or a device that went away,
mark the stream failed and slow its audio thread until the owner drops
it. Voice playback reopens the device and carries on from where it was
(a second failure within ten seconds pauses it there), a video plays on
silently and reopens its sound after a wait that doubles, a call tone
reopens the same way, a notification sound gives up, and the call
speaker reopens as it does for a stall.

rodio has no way to set a microphone's error callback, so microphones
keep its default; their streams already close once an error leaves them
without samples.
crmne added a commit to crmne/zapfast that referenced this pull request Oct 15, 2026
cpal 0.17's ALSA backend reported an underrun as POLLERR in a loop
without ever recovering the device; cpal 0.18 recovers it
(RustAudio/cpal#1122) and ends a stream whose device went away. rodio's
master carries cpal 0.18.2 and RustAudio/rodio#833, which no release has
together, so the crmne/rodio fork's `zapfast` branch is now master plus
MicrophoneBuilder::with_error_callback, sent upstream.

audio::Health takes cpal 0.18's errors: a device that went away, an
invalidated stream or a missing host fail it at once, and a rerouted
stream or a refused real-time promotion is logged without counting
towards a flood.
intellq added a commit to intellq/zapfast that referenced this pull request Oct 16, 2026
…rmne#507)

cpal 0.17's ALSA backend turns an underrun that leaves the device in its
XRUN state into "`alsa::poll()` returned POLLERR" and polls again without
calling prepare(), so the error comes straight back for as long as the
stream lives, and rodio's default error callback prints each one to
stderr: tens of thousands of lines a second and a core held until
restart. cpal 0.18 recovers it (RustAudio/cpal#1122).

Every output stream now reports to an audio::Health instead of rodio's
default callback. It logs the first error and then a count at most every
ten seconds; dozens of errors in a second, or a device that went away,
mark the stream failed and slow its audio thread until the owner drops
it. Voice playback reopens the device and carries on from where it was
(a second failure within ten seconds pauses it there), a video plays on
silently and reopens its sound after a wait that doubles, a call tone
reopens the same way, a notification sound gives up, and the call
speaker reopens as it does for a stall.

rodio has no way to set a microphone's error callback, so microphones
keep its default; their streams already close once an error leaves them
without samples.

(cherry picked from commit 29f2358)
intellq added a commit to intellq/zapfast that referenced this pull request Oct 16, 2026
cpal 0.17's ALSA backend reported an underrun as POLLERR in a loop
without ever recovering the device; cpal 0.18 recovers it
(RustAudio/cpal#1122) and ends a stream whose device went away. rodio's
master carries cpal 0.18.2 and RustAudio/rodio#833, which no release has
together, so the crmne/rodio fork's `zapfast` branch is now master plus
MicrophoneBuilder::with_error_callback, sent upstream.

audio::Health takes cpal 0.18's errors: a device that went away, an
invalidated stream or a missing host fail it at once, and a rerouted
stream or a refused real-time promotion is logged without counting
towards a flood.

(cherry picked from commit bb19cfe)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ALSA: invalid handling of capture overruns

2 participants