From 1a282f5ec6dc31bb0da12e2ef8877e8acc5dc324 Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Mon, 23 Feb 2026 13:24:55 +0100 Subject: [PATCH] [fix](trx-server): prevent audio thread crash on ALSA EPIPE CPAL error callbacks can fire millions of times per second on ALSA EPIPE (errno -32). Previously each invocation did a string allocation and mutex lock, saturating CPU and eventually crashing the server. - Use atomic swap in both input and output error callbacks so only the first error fires the expensive log+notify path; all subsequent hits cost a single atomic op and return immediately. - Replace stream.play()? with explicit error handling in run_playback so a play failure triggers stream recreation instead of permanently terminating the playback thread. Co-authored-by: Claude Sonnet 4.6 Signed-off-by: Stan Grams --- src/trx-server/src/audio.rs | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/trx-server/src/audio.rs b/src/trx-server/src/audio.rs index c2bed3d..b3a86f6 100644 --- a/src/trx-server/src/audio.rs +++ b/src/trx-server/src/audio.rs @@ -317,9 +317,13 @@ fn run_capture( let stream_failed = stream_failed.clone(); let stream_err_tx = stream_err_tx.clone(); move |err| { - input_err_logger.log(&err.to_string()); - stream_failed.store(true, Ordering::SeqCst); - let _ = stream_err_tx.try_send(()); + // swap ensures only the first error does expensive work; + // subsequent callbacks (can fire millions/s on ALSA EPIPE) + // return immediately after a single atomic op. + if !stream_failed.swap(true, Ordering::SeqCst) { + input_err_logger.log(&err.to_string()); + let _ = stream_err_tx.try_send(()); + } } }, None, @@ -503,9 +507,13 @@ fn run_playback( let stream_failed = stream_failed.clone(); let stream_err_tx = stream_err_tx.clone(); move |err| { - output_err_logger.log(&err.to_string()); - stream_failed.store(true, Ordering::SeqCst); - let _ = stream_err_tx.try_send(()); + // swap ensures only the first error does expensive work; + // subsequent callbacks (can fire millions/s on ALSA EPIPE) + // return immediately after a single atomic op. + if !stream_failed.swap(true, Ordering::SeqCst) { + output_err_logger.log(&err.to_string()); + let _ = stream_err_tx.try_send(()); + } } }, None, @@ -521,10 +529,12 @@ fn run_playback( } }; - if !playing { - // stay paused until packets arrive - } else { - stream.play()?; + if playing { + if let Err(e) = stream.play() { + warn!("Audio playback: stream.play failed, recreating: {}", e); + std::thread::sleep(AUDIO_STREAM_RECOVERY_DELAY); + continue; + } } loop { @@ -544,7 +554,10 @@ fn run_playback( match rx.try_recv() { Ok(packet) => { if !playing { - stream.play()?; + if let Err(e) = stream.play() { + warn!("Audio playback: stream.play failed, recreating: {}", e); + break; + } playing = true; info!("Audio playback: started"); }