From e75c4924346a002f9749517dc0e16efa3c303fa2 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 16 Sep 2026 21:19:26 +0200 Subject: [PATCH 1/6] fix(pipewire): honor the build timeout --- CHANGELOG.md | 2 ++ src/host/pipewire/device.rs | 49 ++++++++++++++++++++++++------------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d92d0520..40c9ca061 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **JACK**: Streams no longer panic when the server delivers a larger period than the negotiated buffer size. - **PipeWire**: Fix an empty chunk being emitted when a cycle requests no frames. - **PipeWire**: Fix capture reading from the wrong offset in the buffer on some devices. +- **PipeWire**: Building a stream without a timeout now waits indefinitely instead of failing after two seconds. +- **PipeWire**: A worker thread that stops before the stream is ready is now reported as a backend error instead of a timeout. - **WASAPI**: Device enumeration no longer panics if the COM enumerator fails to initialize. - **WASAPI**: Revert "Default device changes no longer report `DeviceChanged`" as it was misinformed. - **WASAPI**: Output streams now start with real audio immediately instead of undefined content in the render buffer. diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 2302373df..7efe7cba0 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -47,8 +47,6 @@ use crate::{ pub type Devices = std::vec::IntoIter; -const INIT_TIMEOUT: Duration = Duration::from_secs(2); - // This enum record whether it is created by human or just default device #[derive(Clone, Debug, Default, Copy)] pub(crate) enum Class { @@ -373,7 +371,6 @@ impl DeviceTrait for Device { let mut latch = Latch::new(); let waiter = latch.waiter(); let device = self.clone(); - let wait_timeout = timeout.unwrap_or(Duration::from_secs(2)); let initial_quantum = match config.buffer_size { BufferSize::Fixed(n) => n, BufferSize::Default => self.quantum, @@ -506,12 +503,7 @@ impl DeviceTrait for Device { ) })?; - let init_result = init_rx.recv_timeout(wait_timeout).unwrap_or_else(|_| { - Err(Error::with_message( - ErrorKind::DeviceNotAvailable, - "PipeWire timed out", - )) - }); + let init_result = recv_init(&init_rx, timeout); if let Err(e) = init_result { drop(latch); @@ -565,7 +557,6 @@ impl DeviceTrait for Device { let mut latch = Latch::new(); let waiter = latch.waiter(); let device = self.clone(); - let wait_timeout = timeout.unwrap_or(Duration::from_secs(2)); let initial_quantum = match config.buffer_size { BufferSize::Fixed(n) => n, BufferSize::Default => self.quantum, @@ -713,12 +704,7 @@ impl DeviceTrait for Device { ) })?; - let init_result = init_rx.recv_timeout(wait_timeout).unwrap_or_else(|_| { - Err(Error::with_message( - ErrorKind::DeviceNotAvailable, - "PipeWire timed out", - )) - }); + let init_result = recv_init(&init_rx, timeout); if let Err(e) = init_result { drop(latch); @@ -741,6 +727,32 @@ impl DeviceTrait for Device { } } +/// Waits for the worker thread to report whether the stream came up. +/// +/// Honors `build_*_stream`'s timeout contract: `None` waits indefinitely. +fn recv_init( + init_rx: &mpsc::Receiver>, + timeout: Option, +) -> Result<(), Error> { + let received = match timeout { + None => init_rx + .recv() + .map_err(|_| mpsc::RecvTimeoutError::Disconnected), + Some(timeout) => init_rx.recv_timeout(timeout), + }; + received.unwrap_or_else(|err| { + Err(match err { + mpsc::RecvTimeoutError::Timeout => { + Error::with_message(ErrorKind::DeviceNotAvailable, "PipeWire timed out") + } + mpsc::RecvTimeoutError::Disconnected => Error::with_message( + ErrorKind::BackendError, + "PipeWire worker stopped before reporting the stream state", + ), + }) + }) +} + #[derive(Clone, Default)] struct Settings { rate: SampleRate, @@ -790,6 +802,9 @@ fn remote_props() -> Option { Some(props) } +// Enumeration does not take a configurable timeout. +const ENUMERATION_TIMEOUT: Duration = Duration::from_secs(2); + pub fn init_devices(connect_automatically: Arc) -> Option> { let _pw = PwInitGuard::new(); let mainloop = MainLoopRc::new(None).ok()?; @@ -1077,7 +1092,7 @@ pub fn init_devices(connect_automatically: Arc) -> Option Date: Wed, 16 Sep 2026 21:33:18 +0200 Subject: [PATCH 2/6] fix(pipewire): don't report a disconnect when a stream is dropped --- CHANGELOG.md | 1 + src/host/pipewire/device.rs | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40c9ca061..af3ac9c10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **PipeWire**: Fix capture reading from the wrong offset in the buffer on some devices. - **PipeWire**: Building a stream without a timeout now waits indefinitely instead of failing after two seconds. - **PipeWire**: A worker thread that stops before the stream is ready is now reported as a backend error instead of a timeout. +- **PipeWire**: Dropping a stream for a specific device no longer reports it as disconnected. - **WASAPI**: Device enumeration no longer panics if the COM enumerator fails to initialize. - **WASAPI**: Revert "Default device changes no longer report `DeviceChanged`" as it was misinformed. - **WASAPI**: Output streams now start with real audio immediately instead of undefined content in the render buffer. diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 7efe7cba0..1bd297e93 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -429,6 +429,7 @@ impl DeviceTrait for Device { invalidated, } = stream_data; + let invalidated_cmd = invalidated.clone(); let default_monitor = if let Some(key) = device.default_metadata_key() { match core.get_registry_rc() { Ok(registry) => Some(DefaultDeviceMonitor::new( @@ -466,6 +467,8 @@ impl DeviceTrait for Device { } StreamCommand::Drain => {} StreamCommand::Stop => { + // Flag an intentional stop to prevent the stream reporting a disconnected event. + invalidated_cmd.store(true, Ordering::Relaxed); if let Err(e) = stream_clone.disconnect() { emit_error( &error_callback_cmd, @@ -618,6 +621,7 @@ impl DeviceTrait for Device { invalidated, } = stream_data; + let invalidated_cmd = invalidated.clone(); let default_monitor = if let Some(key) = device.default_metadata_key() { match core.get_registry_rc() { Ok(registry) => Some(DefaultDeviceMonitor::new( @@ -668,6 +672,8 @@ impl DeviceTrait for Device { } } StreamCommand::Stop => { + // Flag an intentional stop to prevent the stream reporting a disconnected event. + invalidated_cmd.store(true, Ordering::Relaxed); if let Err(e) = stream_clone.disconnect() { emit_error( &error_callback_cmd, From 7df00e922ec0aaf421f99f68dda4a2c2180d0be4 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 17 Sep 2026 18:52:14 +0200 Subject: [PATCH 3/6] fix(pipewire): report a disconnect when a device is removed --- CHANGELOG.md | 1 + src/host/pipewire/device.rs | 91 ++++++++------- src/host/pipewire/stream.rs | 216 ++++++++++++++++++++---------------- 3 files changed, 176 insertions(+), 132 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af3ac9c10..f0d25fa90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **PipeWire**: Building a stream without a timeout now waits indefinitely instead of failing after two seconds. - **PipeWire**: A worker thread that stops before the stream is ready is now reported as a backend error instead of a timeout. - **PipeWire**: Dropping a stream for a specific device no longer reports it as disconnected. +- **PipeWire**: Removing a device now reports `DeviceNotAvailable` on streams using it. - **WASAPI**: Device enumeration no longer panics if the COM enumerator fails to initialize. - **WASAPI**: Revert "Default device changes no longer report `DeviceChanged`" as it was misinformed. - **WASAPI**: Output streams now start with real audio immediately instead of undefined content in the render buffer. diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 1bd297e93..a57503f78 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -36,7 +36,8 @@ use crate::{ latch::Latch, pipewire::{ stream::{ - DefaultDeviceMonitor, PwInitGuard, SUPPORTED_FORMATS, StreamCommand, StreamData, + PwInitGuard, SUPPORTED_FORMATS, StreamCommand, StreamData, watch_default_device, + watch_target_node, }, utils::{DEVICE_ICON_NAME, METADATA_NAME, audio, clock, default}, }, @@ -430,25 +431,30 @@ impl DeviceTrait for Device { } = stream_data; let invalidated_cmd = invalidated.clone(); - let default_monitor = if let Some(key) = device.default_metadata_key() { - match core.get_registry_rc() { - Ok(registry) => Some(DefaultDeviceMonitor::new( - registry, - key, - error_callback.clone(), - invalidated, - pending_device_changed, - )), - Err(e) => { - let _ = init_tx.send(Err(Error::with_message( - ErrorKind::BackendError, - format!("Could not acquire registry: {e}"), - ))); - return; - } + let registry = match core.get_registry_rc() { + Ok(registry) => registry, + Err(e) => { + let _ = init_tx.send(Err(Error::with_message( + ErrorKind::BackendError, + format!("Could not acquire registry: {e}"), + ))); + return; } - } else { - None + }; + let device_listener = match device.default_metadata_key() { + Some(key) => watch_default_device( + ®istry, + key, + error_callback.clone(), + invalidated, + pending_device_changed, + ), + None => watch_target_node( + ®istry, + device.object_serial, + error_callback.clone(), + invalidated, + ), }; let stream_clone = stream.clone(); let mainloop_rc1 = mainloop.clone(); @@ -494,7 +500,8 @@ impl DeviceTrait for Device { mainloop.run(); drop(listener); - drop(default_monitor); + drop(device_listener); + drop(registry); drop(core_monitor); drop(core); drop(context); @@ -622,25 +629,30 @@ impl DeviceTrait for Device { } = stream_data; let invalidated_cmd = invalidated.clone(); - let default_monitor = if let Some(key) = device.default_metadata_key() { - match core.get_registry_rc() { - Ok(registry) => Some(DefaultDeviceMonitor::new( - registry, - key, - error_callback.clone(), - invalidated, - pending_device_changed, - )), - Err(e) => { - let _ = init_tx.send(Err(Error::with_message( - ErrorKind::BackendError, - format!("Could not acquire registry: {e}"), - ))); - return; - } + let registry = match core.get_registry_rc() { + Ok(registry) => registry, + Err(e) => { + let _ = init_tx.send(Err(Error::with_message( + ErrorKind::BackendError, + format!("Could not acquire registry: {e}"), + ))); + return; } - } else { - None + }; + let device_listener = match device.default_metadata_key() { + Some(key) => watch_default_device( + ®istry, + key, + error_callback.clone(), + invalidated, + pending_device_changed, + ), + None => watch_target_node( + ®istry, + device.object_serial, + error_callback.clone(), + invalidated, + ), }; let stream_clone = stream.clone(); let mainloop_rc1 = mainloop.clone(); @@ -698,7 +710,8 @@ impl DeviceTrait for Device { mainloop.run(); drop(listener); - drop(default_monitor); + drop(device_listener); + drop(registry); drop(core_monitor); drop(core); drop(context); diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index 1e0d4a834..3946d1e61 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -1,5 +1,5 @@ use std::{ - cell::RefCell, + cell::{Cell, RefCell}, rc::Rc, sync::{ Arc, Mutex, @@ -521,105 +521,135 @@ struct MetadataObjects { _metadata: Metadata, } -pub struct DefaultDeviceMonitor { - _registry: RegistryRc, - _registry_listener: RegistryListener, - _meta_objects: Rc>>, -} - -impl DefaultDeviceMonitor { - /// Subscribe to the `"default"` metadata object and fire `error_callback` with - /// [`ErrorKind::DeviceChanged`] whenever its key changes. - pub(super) fn new( - registry: RegistryRc, - key: &'static str, - error_callback: ErrorCallbackArc, - invalidated: Arc, - pending_device_changed: Arc, - ) -> Self { - let meta_objects: Rc>> = Rc::new(RefCell::new(None)); - let meta_objects_ref = meta_objects.clone(); - let registry_ref = registry.clone(); - - let registry_listener = registry - .add_listener_local() - .global(move |global| { - if global.type_ != ObjectType::Metadata { - return; - } - if !global.props.is_some_and(|props| { - props - .get(super::utils::METADATA_NAME) - .is_some_and(|v| v == super::utils::default::NAME) - }) { +/// Subscribes to the `"default"` metadata object and fires `error_callback` with +/// [`ErrorKind::DeviceChanged`] whenever `key` changes. +/// +/// The watch lasts as long as the returned listener is held. +pub(super) fn watch_default_device( + registry: &RegistryRc, + key: &'static str, + error_callback: ErrorCallbackArc, + invalidated: Arc, + pending_device_changed: Arc, +) -> RegistryListener { + // Both of these stay alive through the closure that captures them. + let meta_objects: Rc>> = Rc::new(RefCell::new(None)); + let registry_ref = registry.clone(); + + registry + .add_listener_local() + .global(move |global| { + if global.type_ != ObjectType::Metadata { + return; + } + if !global.props.is_some_and(|props| { + props + .get(super::utils::METADATA_NAME) + .is_some_and(|v| v == super::utils::default::NAME) + }) { + return; + } + let metadata: Metadata = match registry_ref.bind(global) { + Ok(m) => m, + Err(e) => { + emit_error( + &error_callback, + Error::with_message( + ErrorKind::BackendError, + format!("Failed to bind metadata object; device change notifications may be incomplete: {e}"), + ), + ); return; } - let metadata: Metadata = match registry_ref.bind(global) { - Ok(m) => m, - Err(e) => { - emit_error( - &error_callback, - Error::with_message( - ErrorKind::BackendError, - format!("Failed to bind metadata object; device change notifications may be incomplete: {e}"), - ), - ); - return; - } - }; - let error_callback_cb = error_callback.clone(); - let invalidated_cb = invalidated.clone(); - let pending_device_changed_cb = pending_device_changed.clone(); - - let last_value: RefCell>> = RefCell::new(None); - let listener = metadata - .add_listener_local() - .property(move |_subject, prop_key, _type, value| { - if prop_key == Some(key) { - let prev = last_value.borrow_mut().replace(value.map(str::to_owned)); - if let Some(old) = prev { - if old.as_deref() != value { - if value.is_some() { - if try_emit_error( - &error_callback_cb, - Error::with_message( - ErrorKind::DeviceChanged, - "default device changed", - ), - ) - .is_err() - { - pending_device_changed_cb - .store(true, Ordering::Relaxed); - } - } else if !invalidated_cb.swap(true, Ordering::Relaxed) { - emit_error( - &error_callback_cb, - Error::with_message( - ErrorKind::DeviceNotAvailable, - "default device removed", - ), - ); + }; + let error_callback_cb = error_callback.clone(); + let invalidated_cb = invalidated.clone(); + let pending_device_changed_cb = pending_device_changed.clone(); + + let last_value: RefCell>> = RefCell::new(None); + let listener = metadata + .add_listener_local() + .property(move |_subject, prop_key, _type, value| { + if prop_key == Some(key) { + let prev = last_value.borrow_mut().replace(value.map(str::to_owned)); + if let Some(old) = prev { + if old.as_deref() != value { + if value.is_some() { + if try_emit_error( + &error_callback_cb, + Error::with_message( + ErrorKind::DeviceChanged, + "default device changed", + ), + ) + .is_err() + { + pending_device_changed_cb + .store(true, Ordering::Relaxed); } + } else if !invalidated_cb.swap(true, Ordering::Relaxed) { + emit_error( + &error_callback_cb, + Error::with_message( + ErrorKind::DeviceNotAvailable, + "default device removed", + ), + ); } } } - 0 - }) - .register(); - *meta_objects_ref.borrow_mut() = Some(MetadataObjects { - _listener: listener, - _metadata: metadata, - }); - }) - .register(); + } + 0 + }) + .register(); + *meta_objects.borrow_mut() = Some(MetadataObjects { + _listener: listener, + _metadata: metadata, + }); + }) + .register() +} - DefaultDeviceMonitor { - _registry: registry, - _registry_listener: registry_listener, - _meta_objects: meta_objects, - } - } +/// Watches the registry for the node going away, and fires `error_callback` with +/// [`ErrorKind::DeviceNotAvailable`] when it does. +/// +/// The watch lasts as long as the returned listener is held. +pub(super) fn watch_target_node( + registry: &RegistryRc, + object_serial: u32, + error_callback: ErrorCallbackArc, + invalidated: Arc, +) -> RegistryListener { + // Removals are reported by global id, while a device is pinned by serial, so learn the + // mapping from the announcement of the node itself. + let node_id = Rc::new(Cell::new(None)); + let node_id_global = node_id.clone(); + + registry + .add_listener_local() + .global(move |global| { + if global.type_ != ObjectType::Node { + return; + } + let is_target = global.props.is_some_and(|props| { + props + .get(*pw::keys::OBJECT_SERIAL) + .and_then(|serial| serial.parse::().ok()) + == Some(object_serial) + }); + if is_target { + node_id_global.set(Some(global.id)); + } + }) + .global_remove(move |id| { + if node_id.get() == Some(id) && !invalidated.swap(true, Ordering::Relaxed) { + emit_error( + &error_callback, + Error::with_message(ErrorKind::DeviceNotAvailable, "Device disconnected"), + ); + } + }) + .register() } pub struct ConnectParams { From a2415e9fd156151018ad237bd74880eb78721c82 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 17 Sep 2026 19:07:41 +0200 Subject: [PATCH 4/6] fix(pipewire): detect host via /run/pipewire fallback socket Closes #1365 Co-Authored-By: Marco Farruggio --- CHANGELOG.md | 1 + src/host/pipewire/utils.rs | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0d25fa90..e70d17c58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **PipeWire**: A worker thread that stops before the stream is ready is now reported as a backend error instead of a timeout. - **PipeWire**: Dropping a stream for a specific device no longer reports it as disconnected. - **PipeWire**: Removing a device now reports `DeviceNotAvailable` on streams using it. +- **PipeWire**: The host is now detected when `PIPEWIRE_RUNTIME_DIR`/`XDG_RUNTIME_DIR` are unset but the socket exists at `/run/pipewire`. - **WASAPI**: Device enumeration no longer panics if the COM enumerator fails to initialize. - **WASAPI**: Revert "Default device changes no longer report `DeviceChanged`" as it was misinformed. - **WASAPI**: Output streams now start with real audio immediately instead of undefined content in the render buffer. diff --git a/src/host/pipewire/utils.rs b/src/host/pipewire/utils.rs index aa9e324bc..b0311b0ba 100644 --- a/src/host/pipewire/utils.rs +++ b/src/host/pipewire/utils.rs @@ -41,6 +41,11 @@ pub mod audio { /// Returns the path of the PipeWire socket, checking the standard locations /// including the parent of `XDG_RUNTIME_DIR` for Snap sandboxes. +/// +/// Mirrors libpipewire's own lookup order (`PIPEWIRE_RUNTIME_DIR` -> +/// `XDG_RUNTIME_DIR` -> `/run/pipewire`) so cpal finds the same socket +/// `pw-cli` and other native clients do, even when neither runtime dir +/// env var is set. pub fn find_socket_path() -> Option<&'static PathBuf> { static SOCKET: OnceLock> = OnceLock::new(); SOCKET @@ -68,7 +73,7 @@ pub fn find_socket_path() -> Option<&'static PathBuf> { } } - None + socket_in(Path::new("/run/pipewire")) }) .as_ref() } From 83603433d6a0f7b7164844b05a364eb6a60c6f9b Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 17 Sep 2026 21:13:44 +0200 Subject: [PATCH 5/6] fix(ci): drop dbus/jack/pipewire from mipsel install --- .github/workflows/platforms.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/platforms.yml b/.github/workflows/platforms.yml index ea99d7d3b..e06021250 100644 --- a/.github/workflows/platforms.yml +++ b/.github/workflows/platforms.yml @@ -199,8 +199,9 @@ jobs: - name: Install mipsel cross-toolchain and audio libs run: | dpkg --add-architecture mipsel - apt-get update -qq - apt-get install -y --no-install-recommends libclang-dev crossbuild-essential-mipsel $(for p in ${{ env.PACKAGES_LINUX }}; do echo "$p:mipsel"; done) + apt-get update + # ALSA only: dbus/jack/pipewire need libsystemd0:mipsel, which apt 2.6's resolver can't add without breaking Essential packages + apt-get install -y --no-install-recommends libclang-dev crossbuild-essential-mipsel libasound2-dev:mipsel - name: Install Rust nightly (for build-std) uses: dtolnay/rust-toolchain@nightly @@ -215,8 +216,8 @@ jobs: - name: Check examples (default features) run: cargo +nightly check --examples --workspace --verbose --target ${{ env.TARGET }} -Z build-std - - name: Check examples (all features) - run: cargo +nightly check --examples --all-features --workspace --verbose --target ${{ env.TARGET }} -Z build-std + - name: Check examples (realtime, pulseaudio, custom) + run: cargo +nightly check --examples --features realtime,pulseaudio,custom --workspace --verbose --target ${{ env.TARGET }} -Z build-std # Windows (x86_64 and i686) windows: From 0cae9c0820f4eda587dbcc7eb45a42efbab74d83 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 17 Sep 2026 21:23:48 +0200 Subject: [PATCH 6/6] fix(pipewire): demote rt-promoted thread before teardown to avoid sigxcpu --- CHANGELOG.md | 1 + src/host/pipewire/rt_promote.rs | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e70d17c58..d3229e112 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **PipeWire**: Dropping a stream for a specific device no longer reports it as disconnected. - **PipeWire**: Removing a device now reports `DeviceNotAvailable` on streams using it. - **PipeWire**: The host is now detected when `PIPEWIRE_RUNTIME_DIR`/`XDG_RUNTIME_DIR` are unset but the socket exists at `/run/pipewire`. +- **PipeWire**: Dropping a stream no longer risks crashing the process with `SIGXCPU` if it was real-time promoted. - **WASAPI**: Device enumeration no longer panics if the COM enumerator fails to initialize. - **WASAPI**: Revert "Default device changes no longer report `DeviceChanged`" as it was misinformed. - **WASAPI**: Output streams now start with real audio immediately instead of undefined content in the render buffer. diff --git a/src/host/pipewire/rt_promote.rs b/src/host/pipewire/rt_promote.rs index 36efdd72e..43aeffab3 100644 --- a/src/host/pipewire/rt_promote.rs +++ b/src/host/pipewire/rt_promote.rs @@ -41,18 +41,28 @@ impl RtPromoter { let join = thread::Builder::new() .name("cpal_rt_promote".to_owned()) .spawn(move || { + let mut promoted = false; loop { if shutdown_bg.load(Ordering::Relaxed) { + // Demote before pw::deinit() to prevent spurious SIGXCPU signals. + if promoted { + if let Err(e) = + audio_thread_priority::demote_thread_from_real_time(thread_info) + { + emit_error(&error_callback_bg, Error::from(e)); + } + } return; } let frames = pending_frames_bg.swap(0, Ordering::Relaxed); if frames != 0 { - if let Err(e) = audio_thread_priority::promote_thread_to_real_time( + match audio_thread_priority::promote_thread_to_real_time( thread_info, frames, rate, ) { - emit_error(&error_callback_bg, Error::from(e)); + Ok(_) => promoted = true, + Err(e) => emit_error(&error_callback_bg, Error::from(e)), } continue; }