Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 = { 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 }
Expand Down
49 changes: 43 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -51,15 +61,15 @@ 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). |
| `realtime` | Linux, BSD, Windows, Android | Raises the audio callback thread to real-time or high-priority scheduling for lower latency. On Linux/BSD, requires `rtprio` granted in `limits.conf` (e.g. `@audio - rtprio 95`) unless `realtime-dbus` is also enabled. |
| `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

Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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

Expand All @@ -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:
Expand Down
70 changes: 64 additions & 6 deletions src/host/alsa/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
//! Default backend on Linux and BSD systems.

extern crate alsa;
#[cfg(feature = "realtime")]
extern crate alsa_sys;
extern crate libc;

use std::{
Expand Down Expand Up @@ -396,7 +398,10 @@ impl Device {
if buffer_size == 0 {
return Err(Error::with_message(
ErrorKind::DeviceNotAvailable,
"initialization resulted in a null buffer",
format!(
"device '{}': initialization resulted in a null buffer",
self.pcm_id
),
));
}

Expand All @@ -405,7 +410,7 @@ impl Device {
if handle.count() == 0 {
return Err(Error::with_message(
ErrorKind::DeviceNotAvailable,
"poll descriptor count for stream was 0",
format!("device '{}': poll descriptor count is 0", self.pcm_id),
));
}

Expand All @@ -431,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,
Expand Down Expand Up @@ -622,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!(
"device '{}' does not support the requested direction",
self.pcm_id
),
));
}
Err(err) => return Err(err),
Expand All @@ -638,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!("device '{}': no supported configuration", self.pcm_id),
)),
}
}
Expand Down Expand Up @@ -731,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,

Expand Down Expand Up @@ -995,6 +1007,52 @@ fn output_stream_worker(
fn boost_current_thread_priority(
stream: &StreamInner,
) -> Result<audio_thread_priority::RtPriorityHandle, Error> {
use alsa_sys::*;
// SAFETY: `alsa::pcm::PCM` is `pub struct PCM(*mut snd_pcm_t, Cell<bool>)`. 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>()
.read()
};
Comment thread
roderickvd marked this conversation as resolved.
Comment thread
roderickvd marked this conversation as resolved.
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. 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
| 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::with_message(
ErrorKind::RealtimeDenied,
format!(
"device '{}' ({type_name}) cannot be promoted to real-time priority",
stream.pcm_id,
),
));
}

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)
Expand Down Expand Up @@ -1080,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!("device '{}' disconnected", stream.pcm_id),
));
}
// POLLERR signals an xrun or suspend; avail_delay() below returns EPIPE/ESTRPIPE accordingly.
Expand Down Expand Up @@ -1376,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!("device '{}' does not support pausing", self.inner.pcm_id),
));
}
if self.inner.handle.state() != alsa::pcm::State::Paused {
Expand Down
Loading