Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e97f6c4
fix: make CoreAudio and JACK streams start manually
roderickvd May 10, 2026
7abfcd8
fix(pulseaudio): synchronize latency thread in new_record
roderickvd May 10, 2026
af0fec9
refactor(pipewire): unify init handshake and error reporting
roderickvd May 11, 2026
87db3af
fix(asio): block events until Stream handle returned
roderickvd May 11, 2026
5e96c4b
chore: remove stale comments
roderickvd May 11, 2026
6a79ffd
refactor: replace Barrier with Condvar to gate stream start
roderickvd May 11, 2026
14fe3a4
fix(pipewire): address review points
roderickvd May 11, 2026
c813acf
style: fix clippy lintss
roderickvd May 11, 2026
b31f996
fix: address review points
roderickvd May 12, 2026
c0c177d
refactor(asio): improve event handling and state management
roderickvd May 13, 2026
552f39f
fix: address review points
roderickvd May 13, 2026
c7e2274
refactor: address review points
roderickvd May 13, 2026
ede286c
refactor: signal stream readiness with AtomicBool/park
roderickvd May 14, 2026
63bf488
fix: incorrect visibility
roderickvd May 14, 2026
b802f6d
refactor(jack): use AtomicU8 for stream state
roderickvd May 14, 2026
2300575
refactor: address review points
roderickvd May 14, 2026
4b53bcb
refactor: introduce Latch for stream readiness
roderickvd May 14, 2026
aa2694b
fix: derive Debug on latch, clippy lints, iOS/tvOS imports
roderickvd May 14, 2026
3f80095
refactor: clippy lints
roderickvd May 14, 2026
4c31293
doc: streams return paused and require play
roderickvd May 15, 2026
abc43a3
refactor: make Starting the initial StreamState
roderickvd May 15, 2026
0550703
refactor: add OS-specific cfgs to Latch methods
roderickvd May 15, 2026
131ac25
Merge branch 'master' into refactor/remaining-rt-fixes
roderickvd May 15, 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
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **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 `ErrorKind::Xrun` on `kAsioResyncRequest`.
- **ASIO**: Stream error callback now receives `ErrorKind::StreamInvalidated` 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`.
Expand All @@ -90,6 +91,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **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**: Streams no longer start automatically on creation; call `play()` manually.
Comment thread
roderickvd marked this conversation as resolved.
- **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.
Expand All @@ -103,6 +105,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
port-connection failures.
- **JACK**: Stream error callback now receives `ErrorKind::RealtimeDenied` once if the process
callback is not running at real-time scheduling priority.
- **JACK**: Streams no longer start automatically on creation; call `play()` manually.
- **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.
Expand Down Expand Up @@ -156,6 +159,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **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.
- **ASIO**: Fix callbacks firing before `build_*_stream` returns the `Stream` handle.
- **ASIO**: Fix overrun not being reported when the driver reports `kAsioOverload`.
- **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 Down
25 changes: 24 additions & 1 deletion UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ This guide covers breaking changes requiring code updates. See [CHANGELOG.md](CH
- [ ] 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()`.
- [ ] **CoreAudio, JACK**: Add an explicit `stream.play()` call after `build_*_stream()` if you
were relying these backends to auto-start streams.

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

Expand Down Expand Up @@ -237,7 +239,28 @@ explicitly, rename it to `realtime-dbus`. If you relied on the opt-out behavior,
**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
## 7. Streams are returned paused on every backend

**What changed:** `build_input_stream` and `build_output_stream` now return a paused `Stream` on
every backend. Previously, CoreAudio and JACK started the stream automatically.

```rust
// Before (v0.17): on CoreAudio/JACK the stream was already running
let stream = device.build_output_stream(config, data_fn, err_fn, None)?;

// After (v0.18): every backend requires play()
let stream = device.build_output_stream(config, data_fn, err_fn, None)?;
stream.play()?;
```

**Impact:** If you were targeting CoreAudio or JACK and never called `play()`, your callback will
never fire after upgrading. Add the `play()` call.

**Why:** Auto-starting before the caller has the `Stream` handle creates a window where data and
error callbacks can fire before the application can pause, stop, or drop the stream. Other hosts
already required `play()`. The behavior is now uniform.

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

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

Expand Down
4 changes: 0 additions & 4 deletions src/host/aaudio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,8 +327,6 @@ where
let error_callback: ErrorCallbackArc = Arc::new(Mutex::new(error_callback));
let error_callback_for_stream = error_callback.clone();

// RT check: run once on the first callback invocation to avoid delivering RealtimeDenied
// before the Stream handle is returned to the caller.
#[cfg(feature = "realtime")]
let mut rt_checked = false;
#[cfg(feature = "realtime")]
Expand Down Expand Up @@ -408,8 +406,6 @@ where
let error_callback: ErrorCallbackArc = Arc::new(Mutex::new(error_callback));
let error_callback_for_stream = error_callback.clone();

// RT check: run once on the first callback invocation to avoid delivering RealtimeDenied
// before the Stream handle is returned to the caller.
#[cfg(feature = "realtime")]
let mut rt_checked = false;
#[cfg(feature = "realtime")]
Expand Down
56 changes: 34 additions & 22 deletions src/host/alsa/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use crate::{
host::{
equilibrium::{fill_equilibrium, DSD_EQUILIBRIUM_BYTE, U8_EQUILIBRIUM_BYTE},
frames_to_duration,
latch::Latch,
},
iter::{SupportedInputConfigs, SupportedOutputConfigs},
traits::{DeviceTrait, HostTrait, StreamTrait},
Expand Down Expand Up @@ -236,6 +237,7 @@ impl DeviceTrait for Device {
error_callback,
timeout,
);
stream.signal_ready();
Comment thread
roderickvd marked this conversation as resolved.
Comment thread
roderickvd marked this conversation as resolved.
Comment thread
roderickvd marked this conversation as resolved.
Ok(stream)
}

Expand All @@ -259,6 +261,7 @@ impl DeviceTrait for Device {
error_callback,
timeout,
);
stream.signal_ready();
Ok(stream)
}
}
Expand Down Expand Up @@ -773,6 +776,10 @@ pub struct Stream {
/// Keeps the read end of the self-pipe alive for the lifetime of the Stream, so that
/// `trigger.wakeup()` never writes to a closed pipe, even if the worker exited early.
_rx: Arc<TriggerReceiver>,

/// Latch that prevents the worker thread from firing callbacks until the caller has received
/// the `Stream` handle.
latch: Latch,
}

// Compile-time assertion that Stream is Send and Sync
Expand Down Expand Up @@ -1251,6 +1258,11 @@ fn htstamp_elapsed(status: &alsa::pcm::Status, origin: libc::timespec) -> Stream
}

impl Stream {
/// Releases the latch so the worker thread can begin processing audio callbacks.
pub(crate) fn signal_ready(&self) {
self.latch.release();
}

fn new_input<D, E>(
inner: Arc<StreamInner>,
mut data_callback: D,
Expand All @@ -1265,16 +1277,15 @@ impl Stream {
let rx_thread = rx.clone();
let stream = inner.clone();

// The barrier prevents the worker from firing data callbacks before the caller has
// received the Stream handle. Without it, callbacks could arrive before the caller can
// pause, stop, or drop the stream.
let ready = std::sync::Arc::new(std::sync::Barrier::new(2));
let ready_worker = ready.clone();
// The latch is released just before the `Stream` is returned so the worker cannot fire any
// callbacks before the caller has the handle.
let mut latch = Latch::new();
let waiter = latch.waiter();

let thread = thread::Builder::new()
.name("cpal_alsa_in".to_owned())
.spawn(move || {
ready_worker.wait();
waiter.wait();
input_stream_worker(
rx_thread,
&stream,
Expand All @@ -1284,15 +1295,15 @@ impl Stream {
);
})
.unwrap();
let stream = Self {
latch.add_thread(thread.thread().clone());

Self {
thread: Some(thread),
inner,
trigger: tx,
_rx: rx,
};

ready.wait();
stream
latch,
}
}

fn new_output<D, E>(
Expand All @@ -1309,16 +1320,15 @@ impl Stream {
let rx_thread = rx.clone();
let stream = inner.clone();

// The barrier prevents the worker from firing data callbacks before the caller has
// received the Stream handle. Without it, callbacks could arrive before the caller can
// pause, stop, or drop the stream.
let ready = std::sync::Arc::new(std::sync::Barrier::new(2));
let ready_worker = ready.clone();
// The latch is released just before the `Stream` is returned so the worker cannot fire any
// callbacks before the caller has the handle.
let mut latch = Latch::new();
let waiter = latch.waiter();

let thread = thread::Builder::new()
.name("cpal_alsa_out".to_owned())
.spawn(move || {
ready_worker.wait();
waiter.wait();
output_stream_worker(
rx_thread,
&stream,
Expand All @@ -1328,21 +1338,23 @@ impl Stream {
);
})
.unwrap();
latch.add_thread(thread.thread().clone());

let stream = Self {
Self {
thread: Some(thread),
inner,
trigger: tx,
_rx: rx,
};

ready.wait();
stream
latch,
}
}
}

impl Drop for Stream {
fn drop(&mut self) {
// Unblock the worker in case the stream is dropped before signal_ready() was
// called. Idempotent: no effect if the worker is already running.
self.signal_ready();
self.inner.dropping.store(true, Ordering::Release);
self.trigger.wakeup();
if let Some(handle) = self.thread.take() {
Expand Down
Loading
Loading