Skip to content
29 changes: 16 additions & 13 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `DeviceBusy` error variant to `SupportedStreamConfigsError`, `DefaultStreamConfigError`, and
`BuildStreamError` for retryable device access errors (EBUSY, EAGAIN).
- `ErrorKind::DeviceBusy` for retryable device access errors (e.g. EBUSY, EAGAIN).
- `ErrorKind::PermissionDenied` for OS-level access denials.
- `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 @@ -23,8 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Public error enums are now marked `#[non_exhaustive]` to allow adding variants without
SemVer-breaking changes.
- Changed per-operation error types (`DevicesError`, `SupportedStreamConfigsError`, etc.) and
`HostUnavailable` into a unfied `Error`/`ErrorKind`. See [UPGRADING.md](UPGRADING.md).
- `DeviceTrait::build_*_stream()` now takes `StreamConfig` by value instead of `&StreamConfig`
- `HostId::name()` now returns a more human-friendly name instead of the raw backend identifier.
- `StreamInstant` API changed and extended to mirror `std::time::Instant`/`Duration`. See
Expand All @@ -37,34 +37,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **AAudio**: Bump MSRV to 1.85.
- **AAudio**: Buffers with default sizes are now dynamically tuned.
- **AAudio**: `SupportedBufferSize` now reports `min: 1`.
- **ALSA**: Device disconnection now stops the stream with `StreamError::DeviceNotAvailable`
instead of looping.
- **ALSA**: Device disconnection now stops the stream with `ErrorKind::DeviceNotAvailable`.
- **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.
- **ALSA**: Prevent reentrancy issues with non-reentrant plugins and devices.
- **ASIO**: `Device::driver`, `asio_streams`, and `current_callback_flag` are no longer `pub`.
- **ASIO**: Timestamps now include driver-reported hardware latency.
- **ASIO**: Hardware latency is now re-queried when the driver reports `kAsioLatenciesChanged`.
- **ASIO**: Stream error callback now receives `StreamError::BufferUnderrun` on
`kAsioResyncRequest`.
- **ASIO**: Stream error callback now receives `StreamError::StreamInvalidated` when the driver
- **ASIO**: Stream error callback now receives `ErrorKind::Xrun` on `kAsioResyncRequest`.
- **ASIO**: Stream error callback now receives `ErrorKind::StreamInvalidated` when the driver
reports a sample rate change (`sampleRateDidChange`) of 1 Hz or more from the configured rate.
- **AudioWorklet**: `BufferSize::Fixed` now sets `renderSizeHint` on the `AudioContext`.
- **CoreAudio**: Bump MSRV to 1.85.
- **CoreAudio**: Bump `mach2` to 0.6 (uses `core::ffi` instead of `libc`, enables tvOS builds).
- **CoreAudio**: Timestamps now include device latency and safety offset.
- **CoreAudio**: Poisoned stream mutex in stream functions now propagate panics.
- **CoreAudio**: Physical stream format is now set directly on the hardware device.
- **CoreAudio**: Stream error callback now receives `StreamError::StreamInvalidated` on any sample
- **CoreAudio**: Stream error callback now receives `ErrorKind::StreamInvalidated` on any sample
rate change on macOS, and on iOS on route changes that require a stream rebuild.
- **CoreAudio**: Stream error callback now receives `StreamError::DeviceNotAvailable` on iOS
- **CoreAudio**: Stream error callback now receives `ErrorKind::DeviceNotAvailable` on iOS
when media services are lost.
- **CoreAudio**: User timeouts are now respected when building a stream.
- **JACK**: Timestamps now use the precise hardware deadline.
- **JACK**: Buffer size change no longer fires an error callback; internal buffers are resized
without error.
- **JACK**: Server shutdown now fires `StreamError::DeviceNotAvailable`.
- **JACK**: Server shutdown now fires `ErrorKind::DeviceNotAvailable`.
- **JACK**: Default client name now includes the process PID.
- **JACK**: User timeouts are now respected when building a stream.
- **Linux/BSD**: Default host in order from first to last available now is: PipeWire, PulseAudio,
Expand All @@ -88,6 +85,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **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.
- **ALSA**: Fix capture stream hanging or spinning on overruns.
- **ALSA**: Fix non-monotonic `StreamInstant` during stream startup.
- **ALSA**: Fix spurious timestamp errors during stream startup.
Expand All @@ -103,13 +101,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **ASIO**: Poisoned error callback mutex no longer silently drops subsequent error notifications.
- **ASIO**: Poisoned stream mutex in the buffer-size change handler no longer silently skips the
update.
- **ASIO**: Poisoned stream locks now return `ErrorKind::StreamInvalidated` instead of panicking.
- **CoreAudio**: Fix undefined behaviour and silent failure in loopback device creation.
- **CoreAudio**: Poisoned stream locks now return `ErrorKind::StreamInvalidated` instead of
panicking.
- **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.
- **WASAPI**: Poisoned locks now returns an error instead of panicking.
- **WebAudio**: Fix duplicated callbacks on repeated `play()` calls.
- **WebAudio**: Report errors through the callback instead of panicking.

Expand Down
60 changes: 38 additions & 22 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ This guide covers breaking changes requiring code updates. See [CHANGELOG.md](CH

## Breaking Changes Checklist

- [ ] Add wildcard arms to exhaustive `match` expressions on cpal error enums
- [ ] Optionally handle the new `DeviceBusy` variant for retryable device errors
- [ ] Replace per-operation error type matches with `e.kind()` on `ErrorKind`
- [ ] Replace `HostUnavailable` error type with `ErrorKind::HostUnavailable` on `Error`
- [ ] Change `build_*_stream` call sites to pass `StreamConfig` by value (drop the `&`)
- [ ] For custom hosts, change `DeviceTrait` implementations to accept `StreamConfig` by value.
- [ ] Remove `instant.duration_since(e)` unwraps; it now returns `Duration` (saturating).
Expand All @@ -16,43 +16,59 @@ This guide covers breaking changes requiring code updates. See [CHANGELOG.md](CH
- [ ] Update `duration_since` call sites to pass by value (drop the `&`).
- [ ] Migrate `wasm32-unknown-emscripten` to `wasm32-unknown-unknown` if possible.

## 1. Error enums are now `#[non_exhaustive]`
## 1. Unified `Error` and `ErrorKind` type

**What changed:** Public error enums in `cpal` are now marked `#[non_exhaustive]`.
**What changed:** All per-operation error types (`DevicesError`, `SupportedStreamConfigsError`,
`DefaultStreamConfigError`, `BuildStreamError`, `StreamError`, `PlayStreamError`,
`PauseStreamError`) and the `HostUnavailable` struct are replaced by a single `cpal::Error` struct
with getters for its `kind()` and optional `message()`.

```rust
// Before (v0.17)
// Before (v0.17): each operation returned its own error type
match device.default_output_config() {
Ok(config) => config,
Err(DefaultStreamConfigError::DeviceNotAvailable) => panic!("device gone"),
Err(DefaultStreamConfigError::StreamTypeNotSupported) => panic!("unsupported"),
Err(DefaultStreamConfigError::BackendSpecific { err }) => panic!("{err}"),
}

// After (v0.18)
loop {
match device.default_output_config() {
Ok(config) => break config,
Err(DefaultStreamConfigError::DeviceBusy) => {
// After (v0.18): all operations return cpal::Error; match on e.kind()
match device.default_output_config() {
Ok(config) => config,
Err(e) => match e.kind() {
cpal::ErrorKind::DeviceNotAvailable => panic!("device gone"),
cpal::ErrorKind::UnsupportedConfig => panic!("unsupported"),
cpal::ErrorKind::DeviceBusy => {
std::thread::sleep(std::time::Duration::from_millis(100));
// retry
}
Err(DefaultStreamConfigError::DeviceNotAvailable) => panic!("device gone"),
Err(DefaultStreamConfigError::StreamTypeNotSupported) => panic!("unsupported"),
Err(DefaultStreamConfigError::BackendSpecific { err }) => panic!("{err}"),
Err(_) => panic!("unknown error"),
}
_ => panic!("{e}"),
},
}
```

**Why:** This lets cpal add new variants in future minor releases without a SemVer-breaking change.
The `ErrorKind` variants and their equivalents from v0.17:

## 2. New `DeviceBusy` variant
| `ErrorKind` | Former equivalent |
|------------------------|------------------------------------------------------|
| `HostUnavailable` | `HostUnavailable` (struct) |
| `DeviceNotAvailable` | `DeviceNotAvailable` in most enums |
| `DeviceBusy` | - (new; previously mapped to `DeviceNotAvailable`) |
| `UnsupportedConfig` | `StreamConfigNotSupported`, `StreamTypeNotSupported` |
| `UnsupportedOperation` | - (new) |
| `InvalidInput` | - (new) |
| `StreamInvalidated` | `StreamError::StreamInvalidated` |
| `Xrun` | `StreamError::BufferUnderrun` |
| `PermissionDenied` | - (new) |
| `Other` | `BackendSpecific` |

**What changed:** On ALSA, `EBUSY`/`EAGAIN` errors from device open calls now produce `DeviceBusy` instead of `DeviceNotAvailable`. This may be added to other hosts in the future.
The `message()` getter on `Error` returns human-readable context (formerly in
`BackendSpecific::err`).

**Why:** Unlike `DeviceNotAvailable` (device is gone), `DeviceBusy` signals a transient condition. Retrying after a short delay may succeed, as shown in the example above.
**Why:** A single type simplifies error handling across all cpal operations and allows new
`ErrorKind` variants to be added without changing any return types.

## 3. `StreamConfig` is now passed by value
## 2. `StreamConfig` is now passed by value

**What changed:** `StreamConfig` now implements `Copy`, and all `DeviceTrait` stream-building methods accept it by value.

Expand All @@ -68,7 +84,7 @@ let stream = device.build_output_stream(config, data_fn, err_fn, None)?;

If you implement `DeviceTrait` on your own type (via the `custom` feature), update your `build_input_stream_raw` and `build_output_stream_raw` signatures from `config: &StreamConfig` to `config: StreamConfig`. Any `config.clone()` calls before `move` closures can also be removed.

## 4. `StreamInstant` API overhaul
## 3. `StreamInstant` API overhaul

The `StreamInstant` API has been aligned with `std::time::Instant` and `std::time::Duration`.

Expand Down Expand Up @@ -135,7 +151,7 @@ StreamInstant::new(0_u64, 0);

**Why:** All audio host clocks are positive and monotonic; they are never negative.

## 5. `wasm32-unknown-emscripten` target removed
## 4. `wasm32-unknown-emscripten` target removed

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

Expand Down
16 changes: 8 additions & 8 deletions examples/beep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
use clap::Parser;
use cpal::{
traits::{DeviceTrait, HostTrait, StreamTrait},
FromSample, HostUnavailable, Sample, SizedSample, I24,
Error, ErrorKind, FromSample, HostId, Sample, SizedSample, I24,
};

#[derive(Parser, Debug)]
Expand All @@ -40,14 +40,14 @@ struct Opt {
fn main() -> anyhow::Result<()> {
let opt = Opt::parse();

// Jack/PulseAudio support must be enabled at compile time, and is
// JACK/PulseAudio support must be enabled at compile time, and is
// only available on some platforms.
#[allow(unused_mut, unused_assignments)]
let mut jack_host_id = Err(HostUnavailable);
let mut jack_host_id: Result<HostId, Error> = Err(ErrorKind::HostUnavailable.into());
#[allow(unused_mut, unused_assignments)]
let mut pulseaudio_host_id = Err(HostUnavailable);
let mut pulseaudio_host_id: Result<HostId, Error> = Err(ErrorKind::HostUnavailable.into());
#[allow(unused_mut, unused_assignments)]
let mut pipewire_host_id = Err(HostUnavailable);
let mut pipewire_host_id: Result<HostId, Error> = Err(ErrorKind::HostUnavailable.into());
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
Expand All @@ -57,16 +57,16 @@ fn main() -> anyhow::Result<()> {
{
#[cfg(feature = "jack")]
{
jack_host_id = Ok(cpal::HostId::Jack);
jack_host_id = Ok(HostId::Jack);
}

#[cfg(feature = "pulseaudio")]
{
pulseaudio_host_id = Ok(cpal::HostId::PulseAudio);
pulseaudio_host_id = Ok(HostId::PulseAudio);
}
#[cfg(feature = "pipewire")]
{
pipewire_host_id = Ok(cpal::HostId::PipeWire);
pipewire_host_id = Ok(HostId::PipeWire);
}
}

Expand Down
Loading
Loading