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
44 changes: 32 additions & 12 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,48 +5,68 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [Unreleased] (v0.19)

### Added

- `StreamTrait::stop` ends a stream gracefully, draining buffered audio before halting (blocking up to a caller-supplied timeout). Dropping a stream still halts immediately without draining.
- **AAudio**: Xruns are now reported as `ErrorKind::Xrun`.
- **CoreAudio**: Xruns are now reported as `ErrorKind::Xrun`.
- **PipeWire**: Xruns are now reported as `ErrorKind::Xrun`.
- **WASAPI**: Capture xruns are now reported as `ErrorKind::Xrun`.
- `CallbackInfo::xrun()` reports buffer over/underruns via the data callback.

### Changed

- Migrated to Rust 2024.
- `DeviceTrait` and `StreamTrait` now require `Send + Sync` as supertrait bounds.
- `StreamTrait::play` is renamed to `start`.
- `InputCallbackInfo`/`OutputCallbackInfo` merged into `CallbackInfo`.
- `InputStreamTimestamp`/`OutputStreamTimestamp` merged into `StreamTimestamp`; `capture`/`playback` renamed `device`.
- **ALSA**: Update `alsa` dependency to 0.12.
- **WASAPI**: The `windows` and `windows-core` dependencies are now both pinned to 0.62.

### Deprecated

- `StreamTrait::play` is deprecated in favor of `start`.
- `assert_stream_send!` and `assert_stream_sync!` are deprecated; `StreamTrait: Send + Sync` makes them redundant.

### Removed

- Breaking: `ErrorKind::Xrun` (see UPGRADING.md).

### Fixed

- Timestamps now stay monotonic across device and graph changes.
- **ALSA**: A nonzero but sub-millisecond stream timeout is no longer treated as a non-blocking poll.
- **AudioWorklet**: Fix `Stream` operations to work when called from any thread.
- **CoreAudio**: Bump `objc2-core-foundation` dependency lower bound to 0.3.1.
- **CoreAudio**: Default-output streams now report xrun status.
- **CoreAudio**: Fix stale audio output when a data callback wrote a partial buffer.
- **PipeWire**: Fix streams starting audio before `start()` is called.
- **PulseAudio**: `NoData` errors are no longer misreported as buffer xruns.
- **WebAudio**: Fix unsound `Send + Sync` on `Stream` when compiled with `+atomics`.
- **WebAudio**: Fix `Host::is_available()` always returning `true`, even in non-window contexts.

## [Unreleased] (v0.18.2)

### Added

- **AAudio**: Xruns are now reported as `ErrorKind::Xrun`.
- **CoreAudio**: Xruns are now reported as `ErrorKind::Xrun`.
- **PipeWire**: Xruns are now reported as `ErrorKind::Xrun`.
- **WASAPI**: Capture xruns are now reported as `ErrorKind::Xrun`.

### Changed

- **CoreAudio**: Bump `objc2-core-foundation` dependency lower bound to 0.3.1.
- **iOS**: Timestamps now include hardware latency and update when the audio route changes.
- **JACK**: Timestamps now include port latency.
- **WASAPI**: The `windows` and `windows-core` dependencies are now both pinned to 0.62.

### Fixed

- Timestamps now stay monotonic across device and graph changes.
- **ALSA**: A nonzero but sub-millisecond stream timeout is no longer treated as a non-blocking poll.
- **JACK**: Streams with more channels than physical system ports no longer fail to build.
- **JACK**: Channel enumeration is no longer capped at the physical system port count.
- **PipeWire**: Fix streams starting audio before `start()` is called.
- **PipeWire**: Streams for a specific device no longer auto-reroute if it disappears.
- **visionOS**: The CoreAudio backend now builds.
- **WASAPI**: Default device changes no longer report `DeviceChanged`, which wrongly implied the stream had rerouted automatically.
- **WASAPI**: Reported buffer sizes are no longer off by one frame.
- **WASAPI**: `Stream::drop`, `play`, and `pause` no longer panic when the device is lost.
- **WebAudio**: Fix unsound `Send + Sync` on `Stream` when compiled with `+atomics`.
- **WebAudio**: Fix `Host::is_available()` always returning `true`, even in non-window contexts.

## [0.18.1] - 2026-06-07

Expand Down
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,10 @@ name = "record_wav"
[[example]]
name = "synth_tones"

[[example]]
name = "custom"
required-features = ["custom"]

[package.metadata.docs.rs]
# The docs.rs build environment does not have all the dependencies for all features
features = [
Expand Down
56 changes: 56 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ This guide covers breaking changes requiring code updates. See [CHANGELOG.md](CH
- [ ] Remove any calls to the deprecated `assert_stream_send!` and `assert_stream_sync!` macros.
- [ ] Rename `StreamTrait::play` calls to `start` (the old name still works but is deprecated).
- [ ] If you implement a custom host, add a `StreamTrait::stop` implementation.
- [ ] Replace `InputCallbackInfo`/`OutputCallbackInfo` with `CallbackInfo`.
- [ ] Replace `InputStreamTimestamp`/`OutputStreamTimestamp` with `StreamTimestamp`; `capture`/`playback` is now `device`.
- [ ] Remove `ErrorKind::Xrun` match arms; read `CallbackInfo::xrun()` instead.

## 1. `DeviceTrait` and `StreamTrait` require `Send + Sync`

Expand Down Expand Up @@ -36,6 +39,59 @@ Draining is best-effort and varies by backend: most built-in output backends res
[`start`]: https://docs.rs/cpal/latest/cpal/traits/trait.StreamTrait.html#tymethod.start
[`stop`]: https://docs.rs/cpal/latest/cpal/traits/trait.StreamTrait.html#tymethod.stop

## 3. `InputCallbackInfo`/`OutputCallbackInfo` and timestamp types merged

**What changed:** `InputCallbackInfo` and `OutputCallbackInfo` are replaced by a single [`CallbackInfo`]. `InputStreamTimestamp` and `OutputStreamTimestamp` are replaced by a single [`StreamTimestamp`]; its `capture`/`playback` field is renamed `device`.

```rust
// Before (v0.18)
let data_fn = move |data: &mut [f32], info: &OutputCallbackInfo| {
let playback = info.timestamp().playback;
// ...
};

// After (v0.19)
let data_fn = move |data: &mut [f32], info: &CallbackInfo| {
let playback = info.timestamp().device;
// ...
};
```

**Impact:** Update data callback signatures to `&CallbackInfo`, and `.playback`/`.capture` reads to `.device`. The type system still stops you from wiring an output closure to an input stream: `build_output_stream` requires `&mut Data`, `build_input_stream` requires `&Data`.

**Why:** The two structs were identical in shape, distinguished only by name.

[`CallbackInfo`]: https://docs.rs/cpal/latest/cpal/struct.CallbackInfo.html
[`StreamTimestamp`]: https://docs.rs/cpal/latest/cpal/struct.StreamTimestamp.html

## 4. Buffer over/underrun reported via `CallbackInfo::xrun()`, not `ErrorKind::Xrun`

**What changed:** `ErrorKind::Xrun` is removed. Buffer overruns and underruns are now reported through the data callback's info struct instead of `error_callback`, via [`CallbackInfo::xrun()`].

```rust
// Before (v0.18): delivered asynchronously through error_callback
let err_fn = |err: Error| match err.kind() {
ErrorKind::Xrun => eprintln!("xrun"),
_ => eprintln!("Stream error: {err}"),
};

// After (v0.19): delivered with the data callback, tied to the affected block of samples
let data_fn = move |data: &mut [f32], info: &CallbackInfo| {
if info.xrun() {
eprintln!("underrun");
}
// ...
};
```

**Impact:** Remove `ErrorKind::Xrun` from `error_callback` match arms. Read `info.xrun()` in the data callback instead.

**Why:** `error_callback` has no ordering guarantee relative to the data callback. On several hosts the xrun notification arrives through a separate system-level callback entirely. An engine that needs to know exactly which block of samples was affected by a glitch, e.g. to skip forward and stay in sync with something else running in real time, needs that information delivered alongside the data itself.

Ordering of `xrun()` relative to the glitch it reports varies by host; see [`CallbackInfo`]'s docs for the per-host table. WASAPI render and iOS have no xrun signal at all, so `xrun()` is always `false` there.

[`CallbackInfo::xrun()`]: https://docs.rs/cpal/latest/cpal/struct.CallbackInfo.html#method.xrun

---

# Upgrading from v0.17 to v0.18
Expand Down
10 changes: 4 additions & 6 deletions examples/android/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ extern crate anyhow;
extern crate cpal;

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

#[cfg_attr(target_os = "android", ndk_glue::main(backtrace = "full"))]
Expand Down Expand Up @@ -53,15 +53,13 @@ where
};

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

let stream = device.build_output_stream(
config,
move |data: &mut [T], _: &OutputCallbackInfo| {
write_data(data, channels, &mut next_value)
},
move |data: &mut [T], _: &CallbackInfo| write_data(data, channels, &mut next_value),
err_fn,
None,
)?;
Expand Down
2 changes: 1 addition & 1 deletion examples/audioworklet-beep/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ where
};

let err_fn = |err: Error| match err.kind() {
ErrorKind::DeviceChanged | ErrorKind::Xrun | ErrorKind::RealtimeDenied => {
ErrorKind::DeviceChanged | ErrorKind::RealtimeDenied => {
console::log_1(&format!("{err}").into())
}
_ => console::error_1(&format!("Stream error: {err}").into()),
Expand Down
11 changes: 8 additions & 3 deletions examples/beep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

use clap::Parser;
use cpal::{
Device, Error, ErrorKind, FromSample, HostId, I24, OutputCallbackInfo, Sample, SampleFormat,
CallbackInfo, Device, Error, ErrorKind, FromSample, HostId, I24, Sample, SampleFormat,
SizedSample, StreamConfig,
traits::{DeviceTrait, HostTrait, StreamTrait},
};
Expand Down Expand Up @@ -135,15 +135,20 @@ where
};

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

let stream = device.build_output_stream(
config,
move |data: &mut [T], _: &OutputCallbackInfo| write_data(data, channels, &mut next_value),
move |data: &mut [T], info: &CallbackInfo| {
if info.xrun() {
eprintln!("output underrun");
}
write_data(data, channels, &mut next_value)
},
err_fn,
None,
)?;
Expand Down
29 changes: 11 additions & 18 deletions examples/custom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@ use std::{
};

use cpal::{
ChannelCount, Data, Device, DeviceDescription, DeviceDescriptionBuilder, DeviceId, Error,
ErrorKind, FrameCount, FromSample, InputCallbackInfo, OutputCallbackInfo,
OutputStreamTimestamp, Sample, SampleFormat, Stream, StreamConfig, StreamInstant,
SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange,
CallbackInfo, ChannelCount, Data, Device, DeviceDescription, DeviceDescriptionBuilder,
DeviceId, Error, ErrorKind, FrameCount, FromSample, Sample, SampleFormat, Stream, StreamConfig,
StreamInstant, StreamTimestamp, SupportedBufferSize, SupportedStreamConfig,
SupportedStreamConfigRange,
traits::{DeviceTrait, HostTrait, StreamTrait},
};

#[allow(dead_code)]
#[derive(Clone)] // Clone, Send+Sync are required
struct MyHost;

Expand Down Expand Up @@ -117,7 +116,7 @@ impl DeviceTrait for MyDevice {
_: Option<Duration>,
) -> Result<Self::Stream, Error>
where
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
D: FnMut(&Data, &CallbackInfo) + Send + 'static,
E: FnMut(Error) + Send + 'static,
{
Err(Error::new(ErrorKind::UnsupportedConfig))
Expand All @@ -136,7 +135,7 @@ impl DeviceTrait for MyDevice {
_: Option<Duration>,
) -> Result<Self::Stream, Error>
where
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
D: FnMut(&mut Data, &CallbackInfo) + Send + 'static,
E: FnMut(Error) + Send + 'static,
{
let controls = Arc::new(StreamControls {
Expand Down Expand Up @@ -167,11 +166,11 @@ impl DeviceTrait for MyDevice {
let secs = duration.as_nanos() / 1_000_000_000;
let subsec_nanos = duration.as_nanos() - secs * 1_000_000_000;
let stream_instant = StreamInstant::new(secs as _, subsec_nanos as _);
let timestamp = OutputStreamTimestamp {
let timestamp = StreamTimestamp {
callback: stream_instant,
playback: stream_instant,
device: stream_instant,
};
data_callback(&mut data, &OutputCallbackInfo::new(timestamp));
data_callback(&mut data, &CallbackInfo::new(timestamp, false));

let avg = buffer.iter().sum::<f32>() / buffer.len() as f32;
println!("avg: {avg}");
Expand Down Expand Up @@ -228,7 +227,6 @@ impl Drop for MyStream {
}
}

#[cfg(feature = "custom")]
fn main() {
let custom_host = cpal::platform::CustomHost::from_host(MyHost);
// alternatively, use cpal::platform::CustomDevice and skip enumerating devices
Expand All @@ -242,11 +240,6 @@ fn main() {
std::thread::sleep(std::time::Duration::from_millis(4000));
}

#[cfg(not(feature = "custom"))]
fn main() {
panic!("please run with -F custom to try this example")
}

// rest of this example is mostly based off of synth_tones.rs

pub enum Waveform {
Expand Down Expand Up @@ -329,7 +322,7 @@ pub fn make_stream(device: &Device, config: StreamConfig) -> Result<Stream, anyh
frequency_hz: 440.0,
};
let err_fn = |err: Error| match err.kind() {
ErrorKind::DeviceChanged | ErrorKind::Xrun | ErrorKind::RealtimeDenied => {
ErrorKind::DeviceChanged | ErrorKind::RealtimeDenied => {
eprintln!("{err}")
}
_ => eprintln!("Stream error: {err}"),
Expand All @@ -340,7 +333,7 @@ pub fn make_stream(device: &Device, config: StreamConfig) -> Result<Stream, anyh

let stream = device.build_output_stream(
config,
move |output: &mut [f32], _: &OutputCallbackInfo| {
move |output: &mut [f32], _: &CallbackInfo| {
// for 0-1s play sine, 1-2s play square, 2-3s play saw, 3-4s play triangle_wave
let time_since_start = Instant::now().duration_since(time_at_start).as_secs_f32();
if time_since_start < 1.0 {
Expand Down
8 changes: 4 additions & 4 deletions examples/feedback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

use clap::Parser;
use cpal::{
Error, ErrorKind, HostId, InputCallbackInfo, OutputCallbackInfo, Sample, StreamConfig,
CallbackInfo, Error, ErrorKind, HostId, Sample, StreamConfig,
traits::{DeviceTrait, HostTrait, StreamTrait},
};
use ringbuf::{
Expand Down Expand Up @@ -120,13 +120,13 @@ fn main() -> anyhow::Result<()> {
producer.try_push(f32::EQUILIBRIUM).unwrap();
}

let input_data_fn = move |data: &[f32], _: &InputCallbackInfo| {
let input_data_fn = move |data: &[f32], _: &CallbackInfo| {
if producer.push_slice(data) < data.len() {
eprintln!("output stream fell behind: try increasing latency");
}
};

let output_data_fn = move |data: &mut [f32], _: &OutputCallbackInfo| {
let output_data_fn = move |data: &mut [f32], _: &CallbackInfo| {
let read = consumer.pop_slice(data);
if read < data.len() {
data[read..].fill(f32::EQUILIBRIUM);
Expand Down Expand Up @@ -159,7 +159,7 @@ fn main() -> anyhow::Result<()> {

fn err_fn(err: Error) {
match err.kind() {
ErrorKind::DeviceChanged | ErrorKind::Xrun | ErrorKind::RealtimeDenied => {
ErrorKind::DeviceChanged | ErrorKind::RealtimeDenied => {
eprintln!("{err}")
}
_ => eprintln!("Stream error: {err}"),
Expand Down
Loading
Loading