Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4b804d4
feat(realtime): rename feature, make default, and report RT denials
roderickvd May 2, 2026
7e0fd58
fix(realtime): address review points
roderickvd May 2, 2026
1ad2915
feat: add error delivery helpers
roderickvd May 3, 2026
bfdb902
feat: default-enable log and add realtime-dbus features
roderickvd May 3, 2026
360dc69
fix(aaudio): defer realtime denial check until first callback
roderickvd May 3, 2026
22f2a94
fix(alsa): prevent worker from firing callbacks early
roderickvd May 3, 2026
52f726e
feat(asio): report sample rate errors to callback
roderickvd May 3, 2026
79f67cc
refactor(coreaudio): use ErrorCallbackArc and emit_error_or_warn
roderickvd May 3, 2026
6221068
feat(jack): propagate port connect errors
roderickvd May 3, 2026
0ffabf8
refactor(pipewire): improve error handling and reporting
roderickvd May 3, 2026
501f59c
fix(pulseaudio): synchronize worker startup to avoid early callbacks
roderickvd May 3, 2026
5a27695
refactor(wasapi): use ErrorCallbackArc and emit_error
roderickvd May 3, 2026
9816182
refactor(webaudio): use dyn callback Arcs
roderickvd May 3, 2026
603251b
fix: address review points
roderickvd May 3, 2026
99a22ba
fix: address review points
roderickvd May 5, 2026
cab196e
fix(pipewire): clippy lints
roderickvd May 5, 2026
7d7581e
refactor: remove log feature and use error callbacks
roderickvd May 6, 2026
73a351a
fix: address review points
roderickvd May 6, 2026
8011ae2
fix: address review comment
roderickvd May 6, 2026
a60459e
fix(pipewire): wait for caller before running mainloop
roderickvd May 7, 2026
b7dfd26
fix(pipewire): clippy lints
roderickvd May 7, 2026
e57a970
fix(pipewire): track default-device with AtomicBool
roderickvd May 7, 2026
808d294
fix: address review points
roderickvd May 7, 2026
5b0d07a
fix(pipewire): promote to RT on the mainloop thread
roderickvd May 7, 2026
f879f49
fix: clarify eprintln string for audio stream errors
roderickvd May 7, 2026
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
32 changes: 23 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `ErrorKind::DeviceBusy` for retryable device access errors (e.g. EBUSY, EAGAIN).
- `ErrorKind::DeviceChanged` signals that the audio route changed to another device.
- `ErrorKind::PermissionDenied` for OS-level access denials.
- `ErrorKind::ThreadPriorityUnavailable` for when a thread priority request is not granted.
- `StreamConfig` now implements `Copy`.
- `StreamTrait::buffer_size()` to query the stream's current buffer size in frames per callback.
- `HostTrait::device_by_id()` is now dispatched to each backend's implementation, allowing
Expand All @@ -22,6 +21,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `SupportedStreamConfigRange::contains_rate()` to test whether a sample rate falls within a range.
- `SupportedStreamConfigRange::try_with_standard_sample_rate()` and `with_standard_sample_rate()`
to select 48 kHz or 44.1 kHz from a range.
- `realtime` feature for high-priority audio scheduling without a D-Bus build dependency.
- **AAudio**: Streams now request `PERFORMANCE_MODE_LOW_LATENCY` when the `realtime` feature is
enabled; stream error callback receives `ErrorKind::RealtimeDenied` if not granted.
- **ALSA**: `device_by_id()` now accepts PCM shorthand names such as `hw:0,0` and `plughw:foo`.
- **CoreAudio**: tvOS target support (Tier 3, requires nightly).
- **PipeWire**: New host for Linux and some BSDs using the PipeWire API.
Expand All @@ -37,6 +39,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[UPGRADING.md](UPGRADING.md) for migration details.
- `SupportedStreamConfigRange::cmp_default_heuristics` now ranks all `SampleFormat` variants.
See [UPGRADING.md](UPGRADING.md) for migration details.
- `audio_thread_priority` feature renamed to `realtime-dbus` and enabled by default.
- `audio_thread_priority` dependency bumped to 0.35.
- `ErrorKind::ThreadPriorityUnavailable` renamed to `ErrorKind::RealtimeDenied`.
- **AAudio**: Device names now include the device type suffix (e.g. "Speaker (Builtin Speaker)")
for easier identification when enumerating devices.
- **AAudio**: `supported_input_configs()` and `supported_output_configs()` now return an error for
Expand All @@ -47,7 +52,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **AAudio**: `SupportedBufferSize` now reports `min: 1`.
- **AAudio**: `default_input_config()` and `default_output_config()` now prefer 48 kHz, then
44.1 kHz, then the maximum supported sample rate, instead of always taking the maximum.
- **ALSA**: Device disconnection now stops the stream with `ErrorKind::DeviceNotAvailable`.
- **ALSA**: Stream error callback now receives `ErrorKind::DeviceNotAvailable` on device
disconnection.
- **ALSA**: Polling errors trigger underrun recovery instead of looping.
- **ALSA**: Try to resume from hardware after a system suspend.
- **ALSA**: Loop partial reads and writes to completion.
Expand Down Expand Up @@ -75,24 +81,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
when media services are lost.
- **CoreAudio**: Stream error callback now receives `ErrorKind::DeviceChanged` when the system
default output device changes.
- **CoreAudio**: On iOS, unplugging headphones now emits `ErrorKind::DeviceChanged` (stream
rerouted to speaker).
- **CoreAudio**: Stream error callback now receives `ErrorKind::DeviceChanged` on iOS when
headphones are unplugged.
- **CoreAudio**: User timeouts are now respected when building a stream.
- **CoreAudio (iOS)**: `default_input_config()` and `default_output_config()` now prefer 48 kHz,
then 44.1 kHz, then the maximum supported sample rate, instead of always taking the maximum.
- **JACK**: Timestamps now use the precise hardware deadline.
- **JACK**: Buffer size change no longer fires an error callback; internal buffers are resized
- **JACK**: Buffer size change no longer invokes the error callback; internal buffers are resized
without error.
- **JACK**: Server shutdown now fires `ErrorKind::DeviceNotAvailable`.
- **JACK**: Stream error callback now receives `ErrorKind::DeviceNotAvailable` on server shutdown.
- **JACK**: Default client name now includes the process PID.
- **JACK**: User timeouts are now respected when building a stream.
- **JACK**: `Stream::connect_to_system_outputs()` and `Stream::connect_to_system_inputs()` now
return `Result<(), Error>` and roll back the graph instead of silently discarding
port-connection failures.
- **JACK**: Stream error callback now receives `ErrorKind::RealtimeDenied` once if the process
callback is not running at real-time scheduling priority.
- **Linux/BSD**: Default host in order from first to last available now is: PipeWire, PulseAudio,
ALSA.
- **WASAPI**: Raise `windows` dependency lower bound to 0.61.
- **WASAPI**: Timestamps now include hardware pipeline latency.
- **WASAPI**: `FriendlyName` is now preferred as device name over `DeviceDesc`.
- **WASAPI**: Default output and input streams now automatically reroute when the system default
device changes, and fire `ErrorKind::DeviceChanged` on the stream error callback.
device changes; stream error callback now receives `ErrorKind::DeviceChanged`.
- **WASAPI**: `Device::immdevice()` now returns `Option<Audio::IMMDevice>` instead of
`&Audio::IMMDevice`.
- **WebAudio**: Bump MSRV to 1.85.
Comment thread
roderickvd marked this conversation as resolved.
Expand All @@ -108,14 +119,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Reintroduce `audio_thread_priority` feature.
- Fix numeric overflows in calls to create `StreamInstant` in ASIO, CoreAudio and JACK.
- **AAudio**: Fix thread lock when a stream is dropped before it fully starts.
- **AAudio**: Fix capture and playback timestamps falling back to time-zero on error.
- **AAudio**: Fix capture and playback timestamp not accounting for audio pipeline buffer depth.
- **AAudio**: Fix overflow in `buffer_capacity_in_frames` for large fixed buffer sizes.
- **AAudio**: Poisoned stream locks now return `ErrorKind::StreamInvalidated` instead of panicking.
- **AAudio**: Output buffers are now zero-filled before the callback runs.
- **AAudio**: Stream errors are now forwarded to `error_callback`.
- **ALSA**: Fix capture stream hanging or spinning on overruns.
- **ALSA**: Fix timestamps stepping backward during stream startup or after xrun recovery.
- **ALSA**: Fix spurious timestamp errors during stream startup.
Expand All @@ -125,6 +136,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **ALSA**: Fix overflow in `buffer_capacity_in_frames` for large fixed buffer sizes.
- **ALSA**: Fix silence template not being applied for DSD.
- **ALSA**: Fix stream corruption on certain drivers with spurious wakeups.
- **ALSA**: Fix callbacks firing before `build_*_stream` returns the `Stream` handle.
- **ASIO**: Fix enumeration returning only the first device when using `collect()`.
- **ASIO**: Fix device enumeration and stream creation failing when called from spawned threads.
- **ASIO**: Fix buffer size not resizing when the driver reports `kAsioBufferSizeChange`.
Expand All @@ -135,6 +147,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
update.
- **ASIO**: Poisoned stream locks now return `ErrorKind::StreamInvalidated` instead of panicking.
- **ASIO**: Output buffers are now zero-filled before the callback runs.
- **ASIO**: Fix `driver.sample_rate()` failures at stream creation being silently ignored.
- **CoreAudio**: Fix default output streams silently stopping when the system default output
device is unplugged; they now reroute automatically or report `ErrorKind::DeviceNotAvailable`.
- **CoreAudio**: Fix undefined behaviour and silent failure in loopback device creation.
Expand All @@ -143,14 +156,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **CoreAudio**: Fix crashes on certain drivers due to early initialization.
- **JACK**: Fix input capture timestamp using callback execution time instead of cycle start.
- **JACK**: Poisoned error callback mutex no longer silently drops subsequent error notifications.
- **PulseAudio**: Poisoned locks now exit the thread gracefully instead of panicking.
- **JACK**: Port registration failure now fails stream creation instead of silently failing.
- **JACK**: `activate_async()` failure now returns an error instead of panicking.
- **JACK**: Sample rate is now validated against the live JACK server at stream creation time.
- **JACK**: Underrun notification no longer blocks the notification thread.
- **JACK**: Output buffers are now zero-filled before the callback runs.
- **WASAPI**: Poisoned locks now returns an error instead of panicking.
- **WASAPI**: Output buffers are now zero-filled before the callback runs.
- **WASAPI**: Fix audio worker thread spawn failure panicking instead of returning an error.
- **WASAPI**: Fix callbacks firing before `build_*_stream` returns the `Stream` handle.
- **WebAudio**: Fix duplicated callbacks on repeated `play()` calls.
- **WebAudio**: Report errors through the callback instead of panicking.

Expand Down
24 changes: 16 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,19 @@ edition = "2021"
rust-version = "1.78"

[features]
# Audio thread priority elevation
# Raises the audio callback thread to real-time priority for lower latency and fewer glitches
# Requires: On Linux, either rtkit or appropriate user permissions (e.g. limits.conf or capabilities)
# Platform: Linux, DragonFly BSD, FreeBSD, NetBSD, Windows
audio_thread_priority = ["dep:audio_thread_priority"]
default = ["realtime-dbus"]

# Real-time audio thread scheduling
# 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"]

# 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)
# Platform: Linux, DragonFly BSD, FreeBSD, NetBSD (D-Bus/rtkit is a no-op elsewhere; thread
# priority promotion from `realtime` still applies on Windows and Android)
realtime-dbus = ["realtime", "audio_thread_priority/with_dbus"]
Comment thread
roderickvd marked this conversation as resolved.

# ASIO backend for Windows
# Provides low-latency audio I/O by bypassing the Windows audio stack
Expand All @@ -40,7 +48,6 @@ audioworklet = [
]

# Support for user-defined custom hosts, devices, and streams
# Allows integration with audio systems not natively supported by CPAL
# See examples/custom.rs for usage
# Platform: All platforms
custom = []
Expand Down Expand Up @@ -101,15 +108,15 @@ windows = { version = ">=0.61, <=0.62", features = [
"Win32_Media_Multimedia",
"Win32_UI_Shell_PropertiesSystem",
] }
audio_thread_priority = { version = "0.34", optional = true }
audio_thread_priority = { version = "0.35", optional = true, default-features = false }
asio-sys = { version = "0.3.0", path = "asio-sys", optional = true }
num-traits = { version = "0.2", optional = true }
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"
libc = "0.2"
audio_thread_priority = { version = "0.34", optional = true }
audio_thread_priority = { version = "0.35", optional = true, default-features = false }
jack = { version = "0.13", optional = true }
pulseaudio = { version = "0.3", optional = true }
futures = { version = "0.3", optional = true }
Expand Down Expand Up @@ -179,6 +186,7 @@ web-sys = { version = "0.3", features = [
] }

[target.'cfg(target_os = "android")'.dependencies]
audio_thread_priority = { version = "0.35", optional = true, default-features = false }
ndk = { version = "0.9", default-features = false, features = [
"audio",
"api-level-26",
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,13 @@ If you are interested in using CPAL with WebAssembly, please see [this guide](ht
| Feature | Platform | Description |
|---------|----------|-------------|
| `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). |
| `audio_thread_priority` | Linux, BSD, Windows | Raises the audio callback thread to real-time priority for lower latency and fewer glitches. On Linux, requires `rtkit` or appropriate user permissions (`limits.conf` or capabilities). |
| `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`. |
| `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.
Expand Down
40 changes: 39 additions & 1 deletion UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ This guide covers breaking changes requiring code updates. See [CHANGELOG.md](CH
- [ ] Raise your `windows` dependency to `>= 0.61` if you pin it below that.
- [ ] If you relied on the default config returning 44.1 kHz, pin the sample rate explicitly.
- [ ] If you relied on the default config returning `F32`, pin the sample format explicitly.
- [ ] **JACK**: Handle or discard the new `Result` from `Stream::connect_to_system_outputs()` and
`Stream::connect_to_system_inputs()`.

## 1. Unified `Error` and `ErrorKind` type

Expand Down Expand Up @@ -195,7 +197,43 @@ unpredictable for any other format the device reported. The new ordering is comp
consistent: floats before integers (F64 > F32 for maximum fidelity), integers by bit-depth
descending with signed above unsigned at each width, and DSD last.

## 6. `wasm32-unknown-emscripten` target removed
## 6. `audio_thread_priority` feature renamed to `realtime-dbus` and enabled by default

**What changed:** The `audio_thread_priority` feature has been renamed to `realtime-dbus` and is
now a default feature. If you did not previously enable it, real-time scheduling will now be
requested automatically for audio callback threads. A new `realtime` feature was also added,
providing the same scheduling promotion without a D-Bus build dependency.
Comment thread
roderickvd marked this conversation as resolved.

```toml
# Before (v0.17): opt-in required
cpal = { version = "0.17", features = ["audio_thread_priority"] }

# After (v0.18): on by default; rename the feature if you were opting in
cpal = { version = "0.18" }

# To opt out explicitly:
cpal = { version = "0.18", default-features = false }
Comment thread
roderickvd marked this conversation as resolved.
```

On Linux and BSD, `realtime-dbus` requires `libdbus-1-dev` (Debian/Ubuntu), `dbus-devel`
(Fedora/RHEL), or equivalent at build time. On headless or embedded targets without D-Bus, use
`realtime` instead:

```toml
cpal = { version = "0.18", default-features = false, features = ["realtime"] }
Comment thread
roderickvd marked this conversation as resolved.
```

For both features, promotion failures are non-fatal: the stream still starts and an
`ErrorKind::RealtimeDenied` error is delivered through `error_callback`.

**Impact:** In most cases no action is needed. If your `Cargo.toml` names `audio_thread_priority`
explicitly, rename it to `realtime-dbus`. If you relied on the opt-out behavior, pass
`default-features = false`.
Comment thread
roderickvd marked this conversation as resolved.
Comment thread
roderickvd marked this conversation as resolved.

**Why:** Real-time scheduling is the correct default for audio applications. The previous opt-in
made it easy to accidentally ship without it.

## 7. `wasm32-unknown-emscripten` target removed

**What changed:** The `emscripten` audio host and the `wasm32-unknown-emscripten` build target are no longer supported.

Expand Down
8 changes: 6 additions & 2 deletions examples/android/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ extern crate cpal;

use cpal::{
traits::{DeviceTrait, HostTrait, StreamTrait},
Device, FromSample, OutputCallbackInfo, Sample, SampleFormat, SizedSample, StreamConfig, I24,
Device, Error, ErrorKind, FromSample, OutputCallbackInfo, Sample, SampleFormat, SizedSample,
StreamConfig, I24,
};

#[cfg_attr(target_os = "android", ndk_glue::main(backtrace = "full"))]
Expand Down Expand Up @@ -51,7 +52,10 @@ where
(sample_clock * 440.0 * 2.0 * std::f32::consts::PI / sample_rate).sin()
};

let err_fn = |err| eprintln!("an error occurred on stream: {err}");
let err_fn = |err: Error| match err.kind() {
ErrorKind::DeviceChanged | ErrorKind::Xrun | ErrorKind::RealtimeDenied => eprintln!("{err}"),
_ => eprintln!("Stream error: {err}"),
};

let stream = device.build_output_stream(
config,
Expand Down
10 changes: 8 additions & 2 deletions examples/audioworklet-beep/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ use std::{cell::Cell, rc::Rc};

use cpal::{
traits::{DeviceTrait, HostTrait, StreamTrait},
Device, FromSample, HostId, Sample, SampleFormat, SizedSample, Stream, StreamConfig,
Device, Error, ErrorKind, FromSample, HostId, Sample, SampleFormat, SizedSample, Stream,
StreamConfig,
};
use wasm_bindgen::prelude::*;
use web_sys::console;
Expand Down Expand Up @@ -78,7 +79,12 @@ where
(sample_clock * 440.0 * 2.0 * std::f32::consts::PI / sample_rate).sin()
};

let err_fn = |err| console::error_1(&format!("an error occurred on stream: {err}").into());
let err_fn = |err: Error| match err.kind() {
ErrorKind::DeviceChanged | ErrorKind::Xrun | ErrorKind::RealtimeDenied => {
console::log_1(&format!("{err}").into())
}
_ => console::error_1(&format!("Stream error: {err}").into()),
};

let stream = device
.build_output_stream(
Expand Down
7 changes: 6 additions & 1 deletion examples/beep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,12 @@ where
(sample_clock * 440.0 * 2.0 * std::f32::consts::PI / sample_rate).sin()
};

let err_fn = |err| eprintln!("an error occurred on stream: {err}");
let err_fn = |err: Error| match err.kind() {
ErrorKind::DeviceChanged | ErrorKind::Xrun | ErrorKind::RealtimeDenied => {
eprintln!("{err}")
}
_ => eprintln!("Stream error: {err}"),
};

let stream = device.build_output_stream(
config,
Expand Down
7 changes: 6 additions & 1 deletion examples/custom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,12 @@ pub fn make_stream(device: &Device, config: StreamConfig) -> Result<Stream, anyh
current_sample_index: 0.0,
frequency_hz: 440.0,
};
let err_fn = |err| eprintln!("Error building output sound stream: {err}");
let err_fn = |err: Error| match err.kind() {
ErrorKind::DeviceChanged | ErrorKind::Xrun | ErrorKind::RealtimeDenied => {
eprintln!("{err}")
}
_ => eprintln!("Stream error: {err}"),
};

let time_at_start = std::time::Instant::now();
println!("Time at start: {time_at_start:?}");
Expand Down
7 changes: 6 additions & 1 deletion examples/feedback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,5 +172,10 @@ fn main() -> anyhow::Result<()> {
}

fn err_fn(err: Error) {
eprintln!("an error occurred on stream: {err}");
match err.kind() {
ErrorKind::DeviceChanged | ErrorKind::Xrun | ErrorKind::RealtimeDenied => {
eprintln!("{err}")
}
_ => eprintln!("Stream error: {err}"),
}
}
Loading
Loading