From a798fbaeec07924e42bb97945c4d891ddf44731a Mon Sep 17 00:00:00 2001 From: "gulbrand.dev" Date: Wed, 14 Oct 2026 22:43:09 -0700 Subject: [PATCH] fix(alsa): drop PCM IO before underrun recovery An alsa IO created in a match scrutinee lives across the match arms, and alsa allows only one IO per PCM. On duplex playback underrun (EPIPE/EAGAIN or Suspended), recover_duplex() -> begin_duplex_playback() therefore panicked with 'No hw_params call or additional IO objects allowed' when creating the prefill IO on the same PCM. Bind the readi/writei result first so the IO drops before recovery, in both duplex loops and the single-direction input/output loops. --- src/host/alsa/duplex.rs | 17 +++++++++++------ src/host/alsa/worker.rs | 16 ++++++++++------ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/host/alsa/duplex.rs b/src/host/alsa/duplex.rs index ee8a24a8e..92d68c8ee 100644 --- a/src/host/alsa/duplex.rs +++ b/src/host/alsa/duplex.rs @@ -349,12 +349,15 @@ fn process_duplex( ) -> Result<(), Error> { let mut frames_read = 0; while frames_read < stream.period_size { - match stream + // Bind the result first: an IO in a match scrutinee lives across the arms, and + // alsa allows only one IO per PCM, so recover_duplex() -> begin_duplex_playback() + // would panic creating a second IO on the playback PCM. + let result = stream .capture .handle .io_bytes() - .readi(&mut capture_buffer[frames_read * stream.capture.frame_size..]) - { + .readi(&mut capture_buffer[frames_read * stream.capture.frame_size..]); + match result { Ok(n) => frames_read += n, Err(err) if err.errno() == libc::EAGAIN && frames_read == 0 => return Ok(()), Err(_) if matches!(stream.capture.handle.state(), alsa::pcm::State::Suspended) => { @@ -431,12 +434,14 @@ fn process_duplex( let mut frames_written = 0; while frames_written < stream.period_size { - match stream + // Bind the result first, as for capture above: recover_duplex() must not run + // while this PCM's IO is still alive. + let result = stream .playback .handle .io_bytes() - .writei(&playback_buffer[frames_written * stream.playback.frame_size..]) - { + .writei(&playback_buffer[frames_written * stream.playback.frame_size..]); + match result { Ok(n) => frames_written += n, Err(err) if err.errno() == libc::EAGAIN && frames_written == 0 => return Ok(()), Err(_) if matches!(stream.playback.handle.state(), alsa::pcm::State::Suspended) => { diff --git a/src/host/alsa/worker.rs b/src/host/alsa/worker.rs index e35795c7b..0f5b9cedd 100644 --- a/src/host/alsa/worker.rs +++ b/src/host/alsa/worker.rs @@ -334,11 +334,13 @@ fn process_input( ) -> Result<(), Error> { let mut frames_read = 0; while frames_read < stream.period_size { - match stream + // Bind the result first: an IO in a match scrutinee lives across the arms, and + // alsa allows only one IO per PCM, so recovery must not run while it is alive. + let result = stream .handle .io_bytes() - .readi(&mut buffer[frames_read * stream.frame_size..]) - { + .readi(&mut buffer[frames_read * stream.frame_size..]); + match result { Ok(n) => frames_read += n, // EAGAIN = no frames available: skip this cycle if no progress was made, // otherwise treat as an underrun (partial period cannot be delivered safely). @@ -419,11 +421,13 @@ fn process_output( let mut frames_written = 0; while frames_written < stream.period_size { - match stream + // Bind the result first, as for input above: recovery must not run while + // this PCM's IO is still alive. + let result = stream .handle .io_bytes() - .writei(&buffer[frames_written * stream.frame_size..]) - { + .writei(&buffer[frames_written * stream.frame_size..]); + match result { Ok(n) => frames_written += n, // EAGAIN = device cannot currently accept more frames: skip this cycle if no // progress was made, otherwise treat as an underrun (partial period cannot be