From 33d1c8c84458ddbe7b68b7e57ee6e85759f1dc8e Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 9 May 2026 23:22:39 +0200 Subject: [PATCH 1/5] fix(alsa): check PCM RT-safety before promotion --- Cargo.toml | 1 + src/host/alsa/mod.rs | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index f334a9736..ea69fa9fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,6 +115,7 @@ jack = { version = "0.13", optional = true } [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))'.dependencies] alsa = "0.11" +alsa-sys = "0.4" libc = "0.2" audio_thread_priority = { version = "0.35", optional = true, default-features = false } jack = { version = "0.13", optional = true } diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index 1cce43e66..f92613445 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -3,6 +3,7 @@ //! Default backend on Linux and BSD systems. extern crate alsa; +extern crate alsa_sys; extern crate libc; use std::{ @@ -995,6 +996,45 @@ fn output_stream_worker( fn boost_current_thread_priority( stream: &StreamInner, ) -> Result { + use alsa_sys::*; + let raw = unsafe { + (&stream.handle as *const alsa::pcm::PCM) + .cast::<*mut snd_pcm_t>() + .read() + }; + let pcm_type = unsafe { snd_pcm_type(raw) }; + + // Only promote to RT for kernel-backed and pure-computation plugins. Others can exhaust + // RLIMIT_RTTIME when they block or coordinate with non-RT servers and trigger SIGXCPU + // on an RT thread. + if !matches!( + pcm_type, + SND_PCM_TYPE_HW + | SND_PCM_TYPE_HOOKS + | SND_PCM_TYPE_NULL + | SND_PCM_TYPE_COPY + | SND_PCM_TYPE_LINEAR + | SND_PCM_TYPE_ALAW + | SND_PCM_TYPE_MULAW + | SND_PCM_TYPE_ADPCM + | SND_PCM_TYPE_RATE + | SND_PCM_TYPE_ROUTE + | SND_PCM_TYPE_PLUG + | SND_PCM_TYPE_LINEAR_FLOAT + | SND_PCM_TYPE_IEC958 + | SND_PCM_TYPE_SOFTVOL + ) { + let type_name = unsafe { + std::ffi::CStr::from_ptr(snd_pcm_type_name(pcm_type)) + .to_str() + .unwrap_or("unknown") + }; + return Err(Error::new( + ErrorKind::RealtimeDenied, + format!("PCM type '{type_name}' is not safe for RT promotion"), + )); + } + let period_frames = u32::try_from(stream.period_size).unwrap_or(0); audio_thread_priority::promote_current_thread_to_real_time(period_frames, stream.sample_rate) .map_err(Error::from) From b272ee7b55457900d4e326088cd4bd6cf618c300 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 9 May 2026 23:52:03 +0200 Subject: [PATCH 2/5] fix(alsa): address review points --- Cargo.toml | 4 ++-- src/host/alsa/mod.rs | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ea69fa9fb..15bd056f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ default = ["realtime-dbus"] # Promotes audio callback threads to real-time or high-priority scheduling for lower latency # Requires: (Linux/BSD) `rtprio` granted in `limits.conf` (e.g. `@audio - rtprio 95`) # Platform: Linux, DragonFly BSD, FreeBSD, NetBSD, Windows, Android -realtime = ["dep:audio_thread_priority"] +realtime = ["dep:audio_thread_priority", "dep:alsa-sys"] # D-Bus/rtkit support on top of `realtime` for RT scheduling on Linux/BSD desktop systems # Requires: D-Bus development libraries (libdbus-1-dev or equivalent) @@ -115,7 +115,7 @@ jack = { version = "0.13", optional = true } [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))'.dependencies] alsa = "0.11" -alsa-sys = "0.4" +alsa-sys = { version = "0.4", optional = true } libc = "0.2" audio_thread_priority = { version = "0.35", optional = true, default-features = false } jack = { version = "0.13", optional = true } diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index f92613445..c06cf8d30 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -3,6 +3,7 @@ //! Default backend on Linux and BSD systems. extern crate alsa; +#[cfg(feature = "realtime")] extern crate alsa_sys; extern crate libc; @@ -997,6 +998,9 @@ fn boost_current_thread_priority( stream: &StreamInner, ) -> Result { use alsa_sys::*; + // SAFETY: `alsa::pcm::PCM` is `pub struct PCM(*mut snd_pcm_t, Cell)`. The crate + // does not expose a public `as_ptr()`, but we can cast and read from it. + // TODO: replace with `stream.handle.as_ptr()` once alsa-rs exposes it publicly. let raw = unsafe { (&stream.handle as *const alsa::pcm::PCM) .cast::<*mut snd_pcm_t>() @@ -1029,9 +1033,9 @@ fn boost_current_thread_priority( .to_str() .unwrap_or("unknown") }; - return Err(Error::new( + return Err(Error::with_message( ErrorKind::RealtimeDenied, - format!("PCM type '{type_name}' is not safe for RT promotion"), + format!("PCM type '{type_name}' is not eligible for real-time promotion"), )); } From e51efed0d10f0fbe6c9b53cfa562adb625d93a9e Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 12 May 2026 00:00:35 +0200 Subject: [PATCH 3/5] refactor(alsa): name PCM in error messages --- src/host/alsa/mod.rs | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index c06cf8d30..3bff1fac9 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -398,7 +398,10 @@ impl Device { if buffer_size == 0 { return Err(Error::with_message( ErrorKind::DeviceNotAvailable, - "initialization resulted in a null buffer", + format!( + "PCM '{}': initialization resulted in a null buffer", + self.pcm_id + ), )); } @@ -407,7 +410,7 @@ impl Device { if handle.count() == 0 { return Err(Error::with_message( ErrorKind::DeviceNotAvailable, - "poll descriptor count for stream was 0", + format!("PCM '{}': poll descriptor count is 0", self.pcm_id), )); } @@ -433,6 +436,7 @@ impl Device { let stream_inner = StreamInner { dropping: AtomicBool::new(false), handle, + pcm_id: self.pcm_id.clone(), sample_format, sample_rate: conf.sample_rate, frame_size, @@ -624,7 +628,10 @@ impl Device { Err(err) if err.kind() == ErrorKind::InvalidInput => { return Err(Error::with_message( ErrorKind::UnsupportedOperation, - "device does not support the requested direction", + format!( + "PCM '{}' does not support the requested direction", + self.pcm_id + ), )); } Err(err) => return Err(err), @@ -640,7 +647,7 @@ impl Device { .unwrap_or_else(|| f.with_max_sample_rate())), None => Err(Error::with_message( ErrorKind::UnsupportedConfig, - "no supported configuration for this device", + format!("PCM '{}': no supported configuration", self.pcm_id), )), } } @@ -733,6 +740,9 @@ struct StreamInner { // The ALSA handle. handle: alsa::pcm::PCM, + // ALSA PCM identifier used to open this stream. + pcm_id: String, + // Format of the samples. sample_format: SampleFormat, @@ -1010,7 +1020,8 @@ fn boost_current_thread_priority( // Only promote to RT for kernel-backed and pure-computation plugins. Others can exhaust // RLIMIT_RTTIME when they block or coordinate with non-RT servers and trigger SIGXCPU - // on an RT thread. + // on an RT thread. IOPLUG and EXTPLUG are excluded: no reliable way to distinguish + // RT-safe drivers (e.g. pipewire-alsa) from server-backed ones (e.g. pcm_pulse). if !matches!( pcm_type, SND_PCM_TYPE_HW @@ -1035,7 +1046,10 @@ fn boost_current_thread_priority( }; return Err(Error::with_message( ErrorKind::RealtimeDenied, - format!("PCM type '{type_name}' is not eligible for real-time promotion"), + format!( + "PCM '{}' ({type_name}) cannot be promoted to real-time priority", + stream.pcm_id, + ), )); } @@ -1124,7 +1138,7 @@ fn poll_for_period( if revents.intersects(alsa::poll::Flags::HUP | alsa::poll::Flags::NVAL) { return Err(Error::with_message( ErrorKind::DeviceNotAvailable, - "device disconnected", + format!("PCM '{}' disconnected", stream.pcm_id), )); } // POLLERR signals an xrun or suspend; avail_delay() below returns EPIPE/ESTRPIPE accordingly. @@ -1420,7 +1434,7 @@ impl StreamTrait for Stream { if !hw_params.can_pause() { return Err(Error::with_message( ErrorKind::UnsupportedOperation, - "hardware does not support pausing this stream", + format!("PCM '{}' does not support pausing", self.inner.pcm_id), )); } if self.inner.handle.state() != alsa::pcm::State::Paused { From 10e00ffbeca3dedc0d2c025425775b9691fa6975 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 15 May 2026 23:01:27 +0200 Subject: [PATCH 4/5] refactor: rename PCM to device in messages --- src/host/alsa/mod.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index 3bff1fac9..a70a62b99 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -399,7 +399,7 @@ impl Device { return Err(Error::with_message( ErrorKind::DeviceNotAvailable, format!( - "PCM '{}': initialization resulted in a null buffer", + "device '{}': initialization resulted in a null buffer", self.pcm_id ), )); @@ -410,7 +410,7 @@ impl Device { if handle.count() == 0 { return Err(Error::with_message( ErrorKind::DeviceNotAvailable, - format!("PCM '{}': poll descriptor count is 0", self.pcm_id), + format!("device '{}': poll descriptor count is 0", self.pcm_id), )); } @@ -629,7 +629,7 @@ impl Device { return Err(Error::with_message( ErrorKind::UnsupportedOperation, format!( - "PCM '{}' does not support the requested direction", + "device '{}' does not support the requested direction", self.pcm_id ), )); @@ -647,7 +647,7 @@ impl Device { .unwrap_or_else(|| f.with_max_sample_rate())), None => Err(Error::with_message( ErrorKind::UnsupportedConfig, - format!("PCM '{}': no supported configuration", self.pcm_id), + format!("device '{}': no supported configuration", self.pcm_id), )), } } @@ -1047,7 +1047,7 @@ fn boost_current_thread_priority( return Err(Error::with_message( ErrorKind::RealtimeDenied, format!( - "PCM '{}' ({type_name}) cannot be promoted to real-time priority", + "device '{}' ({type_name}) cannot be promoted to real-time priority", stream.pcm_id, ), )); @@ -1138,7 +1138,7 @@ fn poll_for_period( if revents.intersects(alsa::poll::Flags::HUP | alsa::poll::Flags::NVAL) { return Err(Error::with_message( ErrorKind::DeviceNotAvailable, - format!("PCM '{}' disconnected", stream.pcm_id), + format!("device '{}' disconnected", stream.pcm_id), )); } // POLLERR signals an xrun or suspend; avail_delay() below returns EPIPE/ESTRPIPE accordingly. @@ -1434,7 +1434,7 @@ impl StreamTrait for Stream { if !hw_params.can_pause() { return Err(Error::with_message( ErrorKind::UnsupportedOperation, - format!("PCM '{}' does not support pausing", self.inner.pcm_id), + format!("device '{}' does not support pausing", self.inner.pcm_id), )); } if self.inner.handle.state() != alsa::pcm::State::Paused { From 65d24e20ba3aa822a20875ae7c638ae0a8c5842c Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 15 May 2026 23:43:07 +0200 Subject: [PATCH 5/5] doc: troubleshooting ALSA RT priority errors --- README.md | 49 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 756f83d59..508191faf 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,17 @@ Low-level library for audio input and output in pure Rust. - WebAssembly (via Web Audio API or Audio Worklet) - Windows (via WASAPI by default, ASIO or JACK optionally) -Note that on Linux, the ALSA development files are required for building (even when using JACK, PipeWire or PulseAudio). These are provided as part of the `libasound2-dev` package on Debian and Ubuntu distributions and `alsa-lib-devel` on Fedora. +## Linux Build Dependencies + +On Linux, building cpal requires the ALSA and D-Bus development files: `libasound2-dev` and +`libdbus-1-dev` on Debian and Ubuntu, `alsa-lib-devel` and `dbus-devel` on Fedora. + +ALSA is needed even when using JACK, PipeWire, or PulseAudio. + +D-Bus is pulled in by the default `realtime-dbus` feature for `rtkit`-based RT scheduling, typical +for desktop systems. For systems without D-Bus, disable default features and enable the plain +`realtime` feature instead. See [Real-Time Priority Promotion](#real-time-priority-promotion). +Disable both features to disable RT scheduling entirely. ## Minimum Supported Rust Version (MSRV) @@ -51,7 +61,7 @@ If you are interested in using CPAL with WebAssembly, please see [this guide](ht |---------|----------|-------------| | `asio` | Windows | ASIO backend for low-latency audio, bypassing the Windows audio stack. Requires ASIO drivers and LLVM/Clang. See the [ASIO setup guide](#asio). | | `audioworklet` | WebAssembly (`wasm32-unknown-unknown`) | Audio Worklet backend for lower-latency web audio than the default Web Audio API, running audio on a dedicated thread. Requires atomics support (`RUSTFLAGS="-C target-feature=+atomics,+bulk-memory,+mutable-globals"`) and `Cross-Origin` headers for `SharedArrayBuffer`. See the `audioworklet-beep` example. | -| `custom` | All | User-defined host implementations for audio systems not natively supported by CPAL. See `examples/custom.rs`. | +| `custom` | All | User-defined backend implementations for audio systems not natively supported by CPAL. See `examples/custom.rs`. | | `jack` | Linux, BSD, macOS, Windows | JACK Audio Connection Kit backend for pro-audio routing and inter-application connectivity. Requires `libjack-jackd2-dev` (Debian/Ubuntu) or `jack-devel` (Fedora). | | `pipewire` | Linux, BSD | PipeWire media server backend. Requires `libpipewire-0.3-dev` (Debian/Ubuntu) or `pipewire-devel` (Fedora). | | `pulseaudio` | Linux, BSD | PulseAudio sound server backend. Requires `libpulse-dev` (Debian/Ubuntu) or `pulseaudio-libs-devel` (Fedora). | @@ -59,7 +69,7 @@ If you are interested in using CPAL with WebAssembly, please see [this guide](ht | `realtime-dbus` | Linux, BSD, Windows, Android | Uses `rtkit` via D-Bus for RT scheduling on Linux/BSD desktop systems, removing the need for manual `limits.conf` setup. Implies `realtime` on all platforms. Enabled by default. | | `wasm-bindgen` | WebAssembly (`wasm32-unknown-unknown`) | Web Audio API backend for browser-based audio; required for any WebAssembly audio support. See the `wasm-beep` example. | -See the [beep example](examples/beep.rs) for selecting the host at runtime. +See the [beep example](examples/beep.rs) for selecting the backend at runtime. ## ASIO @@ -99,7 +109,7 @@ In an ideal situation you don't need to worry about this step. cpal = { version = "*", features = ["asio"] } ``` -2. **Select the ASIO host** in your code: +2. **Select the ASIO backend** in your code: ```rust let host = cpal::host_from_id(cpal::HostId::Asio) .expect("failed to initialise ASIO host"); @@ -141,9 +151,12 @@ If you receive errors about no default input or output device: ## ALSA, PipeWire, and PulseAudio -When PipeWire or PulseAudio is running, it holds the ALSA `default` device exclusively. A second stream attempting to open it via the ALSA backend will fail with a `DeviceBusy` error. To route audio through the sound server via ALSA, use the bridge devices `pipewire` or `pulse` instead of `default`. Better yet, use the `pipewire` or `pulseaudio` cpal features for native integration. +When PipeWire or PulseAudio is running, it holds the ALSA `default` device exclusively. A second +stream attempting to open it via the ALSA host will fail with a `DeviceBusy` error. To route +audio through the sound server via ALSA, use the bridge devices `pipewire` or `pulse` instead of +`default`. Better yet, use the `pipewire` or `pulseaudio` cpal features for native integration. -Reserve `hw:` and `plughw:` device names for targets that have no sound server. On those targets, ensure the user is a member of the `audio` group if the system does not grant audio device access automatically via `logind`. +On targets without a sound server, address devices directly as `hw:` or `plughw:`. ### Buffer Size Issues @@ -163,6 +176,30 @@ config.buffer_size = cpal::BufferSize::Fixed(1024); Query `device.default_output_config()?.buffer_size()` for valid ranges. Smaller buffers reduce latency but increase CPU load and the risk of glitches. +### ALSA Real-Time Priority Promotion + +The ALSA backend refuses to promote the audio thread to RT priority for plugins such as `pcm.pulse` +and `pcm.pipewire`, notifying `RealtimeDenied` after stream creation on the error callback. +Consider using the `pulseaudio` or `pipewire` cpal features to open the device through the native +backend instead. While RT priority is desirable for low latency, the stream will continue to play +at the default scheduling priority. + +Kernel-backed PCMs (`hw`, `plughw`) and pure-computation plugins are unaffected. + +`RealtimeDenied` is also received when the process lacks the resource limits to acquire +`SCHED_FIFO`. With the default `realtime-dbus` feature, `rtkit` arranges this over D-Bus on +typical desktop systems. With the plain `realtime` feature, you must ensure that `rtprio` is +granted yourself. Add to `/etc/security/limits.d/audio.conf` and ensure the user is member of the +`audio` group: + +``` +@audio - rtprio 95 +``` + +then add the user to the `audio` group (`usermod -aG audio "$USER"`) and re-login. The same group +may anyway be needed to grant access to ALSA device files via `udev` on systems that do not +arrange this automatically via `logind`. + ### Build Errors If you are unable to build the library: