From 31f3647aaabaf063e0f7807ada4c1c9aad531cc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Ma=CC=88nnlein?= Date: Sat, 7 Mar 2026 14:45:06 +0100 Subject: [PATCH 1/3] fix(alsa): skip drain() on non-Running PCM to prevent USB kernel deadlock After a write error, try_recover() leaves the PCM in the Prepared state. Calling drain() on a Prepared-state USB PCM can deadlock the kernel driver, causing a full system freeze that requires a physical power cycle to recover. This has been confirmed on Raspberry Pi 3 with the dwc_otg USB controller and a Focusrite Scarlett 2i4 USB audio interface. Certain tracks trigger an audio key error which causes an ALSA underrun during stop(); try_recover() succeeds but leaves the PCM in Prepared state, and the subsequent drain() call never returns. Dropping the PCM instead of draining is sufficient to release resources when the PCM is not actively running. --- playback/src/audio_backend/alsa.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index bd2b4bf5c..8cf70b44d 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -4,7 +4,7 @@ use crate::convert::Converter; use crate::decoder::AudioPacket; use crate::{NUM_CHANNELS, SAMPLE_RATE}; use alsa::device_name::HintIter; -use alsa::pcm::{Access, Format, Frames, HwParams, PCM}; +use alsa::pcm::{Access, Format, Frames, HwParams, State, PCM}; use alsa::{Direction, ValueOr}; use std::process::exit; use thiserror::Error; @@ -442,7 +442,14 @@ impl Sink for AlsaSink { let pcm = self.pcm.take().ok_or(AlsaError::NotConnected)?; - pcm.drain().map_err(AlsaError::DrainFailure)?; + // Only drain if the PCM is in Running state. After a write error, + // try_recover() leaves the PCM in Prepared state, and calling drain() + // on a Prepared-state USB PCM can deadlock the kernel driver + // (confirmed on Raspberry Pi 3 with the dwc_otg USB controller). + // In that case, dropping the PCM is sufficient to release resources. + if pcm.state() == State::Running { + pcm.drain().map_err(AlsaError::DrainFailure)?; + } } Ok(()) From b112f80625a529aab52299b3482d6073a2e2b6c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Ma=CC=88nnlein?= Date: Sat, 7 Mar 2026 14:51:39 +0100 Subject: [PATCH 2/3] style: fix import ordering for rustfmt --- playback/src/audio_backend/alsa.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 8cf70b44d..366534706 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -4,7 +4,7 @@ use crate::convert::Converter; use crate::decoder::AudioPacket; use crate::{NUM_CHANNELS, SAMPLE_RATE}; use alsa::device_name::HintIter; -use alsa::pcm::{Access, Format, Frames, HwParams, State, PCM}; +use alsa::pcm::{Access, Format, Frames, HwParams, PCM, State}; use alsa::{Direction, ValueOr}; use std::process::exit; use thiserror::Error; From 816a48813dec82d5ec15bd8ed845b1edee3128e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Ma=CC=88nnlein?= Date: Sat, 7 Mar 2026 14:56:27 +0100 Subject: [PATCH 3/3] fix: add debug log when drain is skipped due to non-Running PCM state --- playback/src/audio_backend/alsa.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 366534706..54abef823 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -447,8 +447,11 @@ impl Sink for AlsaSink { // on a Prepared-state USB PCM can deadlock the kernel driver // (confirmed on Raspberry Pi 3 with the dwc_otg USB controller). // In that case, dropping the PCM is sufficient to release resources. - if pcm.state() == State::Running { + let state = pcm.state(); + if state == State::Running { pcm.drain().map_err(AlsaError::DrainFailure)?; + } else { + debug!("PCM not in Running state ({state:?}), skipping drain and dropping PCM"); } }