diff --git a/CHANGELOG.md b/CHANGELOG.md index 04823973b..d3fa667d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.toml b/Cargo.toml index 138630579..0c7e2fe1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 = [ diff --git a/UPGRADING.md b/UPGRADING.md index 16786abe9..e9ff1f3f9 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -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` @@ -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 diff --git a/examples/android/src/lib.rs b/examples/android/src/lib.rs index 59b941454..775e1df05 100644 --- a/examples/android/src/lib.rs +++ b/examples/android/src/lib.rs @@ -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"))] @@ -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, )?; diff --git a/examples/audioworklet-beep/src/lib.rs b/examples/audioworklet-beep/src/lib.rs index 4df2a4e08..b4af1ce92 100644 --- a/examples/audioworklet-beep/src/lib.rs +++ b/examples/audioworklet-beep/src/lib.rs @@ -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()), diff --git a/examples/beep.rs b/examples/beep.rs index 840869699..2547ace65 100644 --- a/examples/beep.rs +++ b/examples/beep.rs @@ -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}, }; @@ -135,7 +135,7 @@ 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}"), @@ -143,7 +143,12 @@ where 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, )?; diff --git a/examples/custom.rs b/examples/custom.rs index f6df54984..2f0e73d14 100644 --- a/examples/custom.rs +++ b/examples/custom.rs @@ -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; @@ -117,7 +116,7 @@ impl DeviceTrait for MyDevice { _: Option, ) -> Result where - D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + D: FnMut(&Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { Err(Error::new(ErrorKind::UnsupportedConfig)) @@ -136,7 +135,7 @@ impl DeviceTrait for MyDevice { _: Option, ) -> Result 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 { @@ -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::() / buffer.len() as f32; println!("avg: {avg}"); @@ -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 @@ -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 { @@ -329,7 +322,7 @@ pub fn make_stream(device: &Device, config: StreamConfig) -> Result { + ErrorKind::DeviceChanged | ErrorKind::RealtimeDenied => { eprintln!("{err}") } _ => eprintln!("Stream error: {err}"), @@ -340,7 +333,7 @@ pub fn make_stream(device: &Device, config: StreamConfig) -> Result 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); @@ -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}"), diff --git a/examples/ios-feedback/src/feedback.rs b/examples/ios-feedback/src/feedback.rs index 4d1105261..af9118668 100644 --- a/examples/ios-feedback/src/feedback.rs +++ b/examples/ios-feedback/src/feedback.rs @@ -11,12 +11,12 @@ extern crate cpal; extern crate ringbuf; use cpal::{ + CallbackInfo, Error, ErrorKind, Sample, StreamConfig, traits::{DeviceTrait, HostTrait, StreamTrait}, - Error, ErrorKind, InputCallbackInfo, OutputCallbackInfo, Sample, StreamConfig, }; use ringbuf::{ - traits::{Consumer, Producer, Split}, HeapRb, + traits::{Consumer, Producer, Split}, }; const LATENCY_MS: f32 = 1000.0; @@ -31,8 +31,14 @@ pub fn run_example() -> Result<(), anyhow::Error> { let output_device = host .default_output_device() .expect("failed to get default output device"); - println!("Using default input device: \"{}\"", input_device.description()?.name()); - println!("Using default output device: \"{}\"", output_device.description()?.name()); + println!( + "Using default input device: \"{}\"", + input_device.description()?.name() + ); + println!( + "Using default output device: \"{}\"", + output_device.description()?.name() + ); // We'll try and use the same configuration between streams to keep it simple. let config: StreamConfig = input_device.default_input_config()?.into(); @@ -52,13 +58,13 @@ pub fn run_example() -> Result<(), anyhow::Error> { 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); @@ -88,7 +94,7 @@ pub fn run_example() -> Result<(), anyhow::Error> { fn 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}"), } } diff --git a/examples/record_wav.rs b/examples/record_wav.rs index 24af5838f..6946eb321 100644 --- a/examples/record_wav.rs +++ b/examples/record_wav.rs @@ -10,7 +10,8 @@ use std::{ use clap::Parser; use cpal::{ - Error, ErrorKind, FromSample, HostId, Sample, SampleFormat, SupportedStreamConfig, + CallbackInfo, Error, ErrorKind, FromSample, HostId, Sample, SampleFormat, + SupportedStreamConfig, traits::{DeviceTrait, HostTrait, StreamTrait}, }; @@ -121,7 +122,7 @@ fn main() -> Result<(), anyhow::Error> { let writer_2 = writer.clone(); let err_fn = move |err: Error| match err.kind() { - ErrorKind::DeviceChanged | ErrorKind::Xrun | ErrorKind::RealtimeDenied => { + ErrorKind::DeviceChanged | ErrorKind::RealtimeDenied => { eprintln!("{err}") } _ => eprintln!("Stream error: {err}"), @@ -130,25 +131,37 @@ fn main() -> Result<(), anyhow::Error> { let stream = match config.sample_format() { SampleFormat::I8 => device.build_input_stream( config.into(), - move |data, _: &_| write_input_data::(data, &writer_2), + move |data, info: &CallbackInfo| { + warn_on_xrun(info); + write_input_data::(data, &writer_2) + }, err_fn, None, )?, SampleFormat::I16 => device.build_input_stream( config.into(), - move |data, _: &_| write_input_data::(data, &writer_2), + move |data, info: &CallbackInfo| { + warn_on_xrun(info); + write_input_data::(data, &writer_2) + }, err_fn, None, )?, SampleFormat::I32 => device.build_input_stream( config.into(), - move |data, _: &_| write_input_data::(data, &writer_2), + move |data, info: &CallbackInfo| { + warn_on_xrun(info); + write_input_data::(data, &writer_2) + }, err_fn, None, )?, SampleFormat::F32 => device.build_input_stream( config.into(), - move |data, _: &_| write_input_data::(data, &writer_2), + move |data, info: &CallbackInfo| { + warn_on_xrun(info); + write_input_data::(data, &writer_2) + }, err_fn, None, )?, @@ -190,6 +203,12 @@ fn wav_spec_from_config(config: &SupportedStreamConfig) -> hound::WavSpec { type WavWriterHandle = Arc>>>>; +fn warn_on_xrun(info: &CallbackInfo) { + if info.xrun() { + eprintln!("input overrun: recording has a gap"); + } +} + fn write_input_data(input: &[T], writer: &WavWriterHandle) where T: Sample, diff --git a/examples/synth_tones.rs b/examples/synth_tones.rs index b369a36af..472692546 100644 --- a/examples/synth_tones.rs +++ b/examples/synth_tones.rs @@ -7,7 +7,7 @@ extern crate clap; extern crate cpal; use cpal::{ - Device, Error, ErrorKind, FromSample, Host, I24, OutputCallbackInfo, Sample, SampleFormat, + CallbackInfo, Device, Error, ErrorKind, FromSample, Host, I24, Sample, SampleFormat, SizedSample, Stream, StreamConfig, SupportedStreamConfig, U24, traits::{DeviceTrait, HostTrait, StreamTrait}, }; @@ -140,7 +140,7 @@ where 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}"), @@ -151,7 +151,7 @@ where let stream = device.build_output_stream( config, - move |output: &mut [T], _: &OutputCallbackInfo| { + move |output: &mut [T], _: &CallbackInfo| { // for 0-1s play sine, 1-2s play square, 2-3s play saw, 3-4s play triangle_wave let time_since_start = std::time::Instant::now() .duration_since(time_at_start) diff --git a/examples/wasm-beep/src/lib.rs b/examples/wasm-beep/src/lib.rs index efe11c8cc..f4281753d 100644 --- a/examples/wasm-beep/src/lib.rs +++ b/examples/wasm-beep/src/lib.rs @@ -77,7 +77,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()), diff --git a/src/duplex.rs b/src/duplex.rs index fb8b13084..3800172ff 100644 --- a/src/duplex.rs +++ b/src/duplex.rs @@ -1,35 +1,31 @@ -use crate::{BufferSize, ChannelCount, InputStreamTimestamp, OutputStreamTimestamp, SampleRate}; +use crate::{BufferSize, CallbackInfo, ChannelCount, SampleRate}; /// Information relevant to a single call to the user's duplex stream data callback. /// -/// Combines the input and output timestamps for the callback. Because a duplex stream's input and -/// output share a single clock, both timestamps are drawn from the same time source. +/// Because a duplex stream's input and output share a single clock, `input.timestamp()` and +/// `output.timestamp()` are drawn from the same time source. The two directions have independent +/// buffers, so `input.xrun()` and `output.xrun()` can each report a glitch independently for the +/// same invocation. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct DuplexCallbackInfo { - input_timestamp: InputStreamTimestamp, - output_timestamp: OutputStreamTimestamp, + input: CallbackInfo, + output: CallbackInfo, } impl DuplexCallbackInfo { - /// Construct a `DuplexCallbackInfo` from its input and output timestamps. - pub fn new( - input_timestamp: InputStreamTimestamp, - output_timestamp: OutputStreamTimestamp, - ) -> Self { - Self { - input_timestamp, - output_timestamp, - } + /// Construct a `DuplexCallbackInfo` from its input and output callback info. + pub fn new(input: CallbackInfo, output: CallbackInfo) -> Self { + Self { input, output } } - /// The timestamp for the captured input data passed to the callback. - pub fn input_timestamp(&self) -> InputStreamTimestamp { - self.input_timestamp + /// The timestamp and xrun status for the captured input data passed to the callback. + pub fn input(&self) -> CallbackInfo { + self.input } - /// The timestamp for the output data written by the callback. - pub fn output_timestamp(&self) -> OutputStreamTimestamp { - self.output_timestamp + /// The timestamp and xrun status for the output data written by the callback. + pub fn output(&self) -> CallbackInfo { + self.output } } diff --git a/src/error.rs b/src/error.rs index ffff4fb9c..2c488c82e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -74,9 +74,6 @@ pub enum ErrorKind { /// not implemented by the backend. UnsupportedOperation, - /// A buffer underrun or overrun occurred, causing a potential audio glitch. - Xrun, - /// The underlying platform audio API returned an error that CPAL cannot map to a more /// specific error kind. BackendError, @@ -124,7 +121,6 @@ impl Display for ErrorKind { "The requested stream configuration is not supported by the device.", ), Self::UnsupportedOperation => f.write_str("The requested operation is not supported."), - Self::Xrun => f.write_str("A buffer underrun or overrun occurred."), Self::BackendError => f.write_str( "The audio backend returned an unclassified error.", ), diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index 582cb1bc4..c86ea144c 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -14,11 +14,10 @@ use std::{ }; use crate::{ - BufferSize, ChannelCount, Data, DeviceDescription, DeviceDescriptionBuilder, DeviceDirection, - DeviceId, DeviceType, Error, ErrorKind, FrameCount, InputCallbackInfo, InputStreamTimestamp, - InterfaceType, OutputCallbackInfo, OutputStreamTimestamp, ResultExt, SampleFormat, SampleRate, - StreamConfig, StreamInstant, SupportedBufferSize, SupportedStreamConfig, - SupportedStreamConfigRange, + BufferSize, CallbackInfo, ChannelCount, Data, DeviceDescription, DeviceDescriptionBuilder, + DeviceDirection, DeviceId, DeviceType, Error, ErrorKind, FrameCount, InterfaceType, ResultExt, + SampleFormat, SampleRate, StreamConfig, StreamInstant, StreamTimestamp, SupportedBufferSize, + SupportedStreamConfig, SupportedStreamConfigRange, host::{ErrorCallbackArc, emit_error}, traits::{DeviceTrait, HostTrait, StreamTrait}, }; @@ -308,7 +307,7 @@ fn build_input_stream( sample_format: SampleFormat, ) -> Result where - D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + D: FnMut(&Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { let builder = configure_for_device(builder, device, config); @@ -318,7 +317,6 @@ where let error_callback: ErrorCallbackArc = Arc::new(Mutex::new(error_callback)); let error_callback_for_stream = error_callback.clone(); let error_callback_for_data = error_callback.clone(); - let error_callback_for_xrun = error_callback.clone(); let last_xrun_count = Arc::new(AtomicI32::new(0)); let draining = Arc::new(AtomicBool::new(false)); @@ -348,9 +346,7 @@ where } let xrun_count = stream.x_run_count(); - if last_xrun_count.swap(xrun_count, Ordering::Relaxed) < xrun_count { - let _ = try_emit_error(&error_callback_for_xrun, ErrorKind::Xrun.into()); - } + let xrun = last_xrun_count.swap(xrun_count, Ordering::Relaxed) < xrun_count; let Some(n_samples) = u64::try_from(num_frames) .ok() @@ -369,11 +365,12 @@ where return ndk::audio::AudioCallbackResult::Stop; }; if !draining_for_data.load(Ordering::Relaxed) { - let cb_info = InputCallbackInfo { - timestamp: InputStreamTimestamp { + let cb_info = CallbackInfo { + timestamp: StreamTimestamp { callback: now_stream_instant(), - capture: input_stream_instant(stream, sample_rate), + device: input_stream_instant(stream, sample_rate), }, + xrun, }; (data_callback)( &unsafe { Data::from_parts(data as *mut _, n_samples, sample_format) }, @@ -407,7 +404,7 @@ fn build_output_stream( sample_format: SampleFormat, ) -> Result where - D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + D: FnMut(&mut Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { let builder = configure_for_device(builder, device, config); @@ -421,7 +418,6 @@ where let error_callback: ErrorCallbackArc = Arc::new(Mutex::new(error_callback)); let error_callback_for_stream = error_callback.clone(); let error_callback_for_data = error_callback.clone(); - let error_callback_for_xrun = error_callback.clone(); let last_xrun_count = Arc::new(AtomicI32::new(0)); let draining = Arc::new(AtomicBool::new(false)); @@ -451,9 +447,7 @@ where } let xrun_count = stream.x_run_count(); - if last_xrun_count.swap(xrun_count, Ordering::Relaxed) < xrun_count { - let _ = try_emit_error(&error_callback_for_xrun, ErrorKind::Xrun.into()); - } + let xrun = last_xrun_count.swap(xrun_count, Ordering::Relaxed) < xrun_count; let Some(n_samples) = u64::try_from(num_frames) .ok() @@ -480,11 +474,12 @@ where } if !draining_for_data.load(Ordering::Relaxed) { - let cb_info = OutputCallbackInfo { - timestamp: OutputStreamTimestamp { + let cb_info = CallbackInfo { + timestamp: StreamTimestamp { callback: now_stream_instant(), - playback: output_stream_instant(stream, sample_rate), + device: output_stream_instant(stream, sample_rate), }, + xrun, }; (data_callback)( &mut unsafe { Data::from_parts(data as *mut _, n_samples, sample_format) }, @@ -673,7 +668,7 @@ impl DeviceTrait for Device { _timeout: Option, ) -> Result where - D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + D: FnMut(&Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { crate::validate_stream_config(&config)?; @@ -720,7 +715,7 @@ impl DeviceTrait for Device { _timeout: Option, ) -> Result where - D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + D: FnMut(&mut Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { crate::validate_stream_config(&config)?; diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index 9ae0fef2e..87831357c 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -22,10 +22,9 @@ use std::{ use self::alsa::poll::Descriptors; pub use self::enumerate::Devices; use crate::{ - BufferSize, COMMON_SAMPLE_RATES, ChannelCount, Data, DeviceDescription, + BufferSize, COMMON_SAMPLE_RATES, CallbackInfo, ChannelCount, Data, DeviceDescription, DeviceDescriptionBuilder, DeviceDirection, DeviceId, Error, ErrorKind, FrameCount, - InputCallbackInfo, InputStreamTimestamp, OutputCallbackInfo, OutputStreamTimestamp, - SampleFormat, SampleRate, StreamConfig, StreamInstant, SupportedBufferSize, + SampleFormat, SampleRate, StreamConfig, StreamInstant, StreamTimestamp, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, host::{ Notify, @@ -266,7 +265,7 @@ impl DeviceTrait for Device { timeout: Option, ) -> Result where - D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + D: FnMut(&Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { // Keep `capture` monotonic: avail_delay() varies between cycles, and a capture overrun @@ -292,7 +291,7 @@ impl DeviceTrait for Device { timeout: Option, ) -> Result where - D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + D: FnMut(&mut Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { // Keep `playback` monotonic: avail_delay() varies between cycles, and a playback @@ -450,6 +449,7 @@ impl Device { timestamp_mode, creation_ts, creation_instant: std::time::Instant::now(), + pending_xrun: AtomicBool::new(false), _context: self._context.clone(), }; @@ -768,6 +768,9 @@ struct StreamInner { // mode and last-resort fallback if the status query in now() fails. creation_instant: std::time::Instant, + // Xrun pending delivery to the data callback. + pending_xrun: AtomicBool, + // Keep ALSA context alive to prevent premature ALSA config cleanup. _context: Arc, } @@ -976,7 +979,7 @@ impl StreamWorkerContext { fn input_stream_worker( rx: Arc, stream: &StreamInner, - data_callback: &mut (dyn FnMut(&Data, &InputCallbackInfo) + Send + 'static), + data_callback: &mut (dyn FnMut(&Data, &CallbackInfo) + Send + 'static), error_callback: &mut (dyn FnMut(Error) + Send + 'static), timeout: Option, ) { @@ -1001,6 +1004,7 @@ fn input_stream_worker( } let result = match poll_for_period(&rx, stream, &mut ctxt) { Ok(Poll::Pending) => continue, + Ok(Poll::Recover) => recover_input(stream), Ok(Poll::Ready { status, delay_frames, @@ -1015,14 +1019,6 @@ fn input_stream_worker( }; if let Err(err) = result { match err.kind() { - ErrorKind::Xrun => { - error_callback(err); - if let Err(err) = stream.handle.prepare() { - error_callback(err.into()); - } else if let Err(err) = stream.handle.start() { - error_callback(err.into()); - } - } ErrorKind::DeviceNotAvailable => { error_callback(err); stream.signal_worker_exit(); @@ -1037,7 +1033,7 @@ fn input_stream_worker( fn output_stream_worker( rx: Arc, stream: &StreamInner, - data_callback: &mut (dyn FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static), + data_callback: &mut (dyn FnMut(&mut Data, &CallbackInfo) + Send + 'static), error_callback: &mut (dyn FnMut(Error) + Send + 'static), timeout: Option, ) { @@ -1063,6 +1059,7 @@ fn output_stream_worker( } let result = match poll_for_period(&rx, stream, &mut ctxt) { Ok(Poll::Pending) => continue, + Ok(Poll::Recover) => recover_output(stream), Ok(Poll::Ready { status, delay_frames, @@ -1077,15 +1074,6 @@ fn output_stream_worker( }; if let Err(err) = result { match err.kind() { - ErrorKind::Xrun => { - error_callback(err); - if let Err(err) = stream.handle.prepare() { - error_callback(err.into()); - } - // No need to call start() for output streams after prepare(); - // ALSA automatically restarts them when the buffer is refilled - // and the stream is triggered again. - } ErrorKind::DeviceNotAvailable => { error_callback(err); stream.signal_worker_exit(); @@ -1098,13 +1086,14 @@ fn output_stream_worker( } /// Attempt hardware resume from a suspend event (`ESTRPIPE`). -fn try_resume(handle: &alsa::PCM) -> Result { +fn try_resume(stream: &StreamInner) -> Result { + let handle = &stream.handle; + let hw_params = handle.hw_params_current()?; if !hw_params.can_resume() { - return Err(Error::with_message( - ErrorKind::Xrun, // treat as xrun so the worker calls prepare() - "Device does not support suspend/resume", - )); + // Hardware doesn't support suspend/resume: fall back to full recovery. + stream.pending_xrun.store(true, Ordering::Relaxed); + return Ok(Poll::Recover); } match handle.resume() { @@ -1127,8 +1116,11 @@ fn try_resume(handle: &alsa::PCM) -> Result { } // device is still resuming; poll again until it is ready. Err(e) if e.errno() == libc::EAGAIN => Ok(Poll::Pending), - // hardware does not support soft resume; treat as xrun so the worker calls prepare() - Err(e) if e.errno() == libc::ENOSYS => Err(ErrorKind::Xrun.into()), + // hardware does not support soft resume: fall back to full recovery. + Err(e) if e.errno() == libc::ENOSYS => { + stream.pending_xrun.store(true, Ordering::Relaxed); + Ok(Poll::Recover) + } Err(e) => Err(e.into()), } } @@ -1139,6 +1131,8 @@ enum Poll { status: alsa::pcm::Status, delay_frames: usize, }, + // An xrun was detected; the worker should call prepare() (+ start() for input) and loop. + Recover, } // This block is shared between both input and output stream worker functions. @@ -1167,10 +1161,11 @@ fn poll_for_period( } // Xrun with POLLERR missed: recover the same way the POLLERR path does. alsa::pcm::State::XRun => { - return Err(ErrorKind::Xrun.into()); + stream.pending_xrun.store(true, Ordering::Relaxed); + return Ok(Poll::Recover); } // Suspend with POLLHUP/POLLERR missed: attempt hardware resume. - alsa::pcm::State::Suspended => return try_resume(&stream.handle), + alsa::pcm::State::Suspended => return try_resume(stream), // No events and no error state: spurious wakeup, poll again. _ => {} } @@ -1200,9 +1195,12 @@ fn poll_for_period( // POLLIN/POLLOUT: data is ready, fall through to process it. let (avail_frames, delay_frames) = match stream.handle.avail_delay() { // Xrun: recover via prepare() (+ start() for capture, handled by the worker). - Err(err) if err.errno() == libc::EPIPE => return Err(ErrorKind::Xrun.into()), + Err(err) if err.errno() == libc::EPIPE => { + stream.pending_xrun.store(true, Ordering::Relaxed); + return Ok(Poll::Recover); + } // Suspend: try hardware resume first; fall back to prepare() if unsupported. - Err(err) if err.errno() == libc::ESTRPIPE => return try_resume(&stream.handle), + Err(err) if err.errno() == libc::ESTRPIPE => return try_resume(stream), res => res, }?; // ALSA can have spurious wakeups where poll returns but avail < avail_min. @@ -1234,13 +1232,21 @@ fn poll_for_period( }) } +// Full input underrun recovery: mark the xrun, then prepare + start the stream. +fn recover_input(stream: &StreamInner) -> Result<(), Error> { + stream.pending_xrun.store(true, Ordering::Relaxed); + stream.handle.prepare()?; + stream.handle.start()?; + Ok(()) +} + // Read input data from ALSA and deliver it to the user. fn process_input( stream: &StreamInner, buffer: &mut [u8], status: alsa::pcm::Status, delay_frames: usize, - data_callback: &mut (dyn FnMut(&Data, &InputCallbackInfo) + Send + 'static), + data_callback: &mut (dyn FnMut(&Data, &CallbackInfo) + Send + 'static), ) -> Result<(), Error> { let mut frames_read = 0; while frames_read < stream.period_size { @@ -1252,19 +1258,18 @@ fn process_input( Ok(n) => frames_read += n, // EAGAIN = no frames available: skip this cycle if no progress was made, // otherwise treat as an underrun (partial period cannot be delivered safely). - Err(err) if err.errno() == libc::EAGAIN => { - if frames_read == 0 { - return Ok(()); - } else { - return Err(ErrorKind::Xrun.into()); - } + Err(err) if err.errno() == libc::EAGAIN && frames_read == 0 => return Ok(()), + // EAGAIN with partial progress, or EPIPE: full underrun recovery required. + Err(err) if err.errno() == libc::EAGAIN || err.errno() == libc::EPIPE => { + return recover_input(stream); } - // EPIPE = xrun: full underrun recovery (prepare + start) required. - Err(err) if err.errno() == libc::EPIPE => return Err(ErrorKind::Xrun.into()), // ESTRPIPE = hardware suspend: try soft resume first, falling back to underrun // recovery if the hardware doesn't support it. Err(err) if err.errno() == libc::ESTRPIPE => { - return try_resume(&stream.handle).map(|_| ()); + return match try_resume(stream)? { + Poll::Recover => recover_input(stream), + _ => Ok(()), + }; } Err(err) => return Err(err.into()), } @@ -1277,23 +1282,33 @@ fn process_input( let capture = callback_instant .checked_sub(delay_duration) .unwrap_or(StreamInstant::ZERO); - let timestamp = InputStreamTimestamp { + let timestamp = StreamTimestamp { callback: callback_instant, - capture, + device: capture, }; - data_callback(&data, &InputCallbackInfo { timestamp }); + let xrun = stream.pending_xrun.swap(false, Ordering::Relaxed); + data_callback(&data, &CallbackInfo { timestamp, xrun }); } Ok(()) } // Request data from the user's function and write it via ALSA. +// Full output underrun recovery: mark the xrun, then prepare the stream. No need to call +// start(): ALSA automatically restarts output streams once the buffer is refilled and +// triggered again. +fn recover_output(stream: &StreamInner) -> Result<(), Error> { + stream.pending_xrun.store(true, Ordering::Relaxed); + stream.handle.prepare()?; + Ok(()) +} + fn process_output( stream: &StreamInner, buffer: &mut [u8], status: alsa::pcm::Status, delay_frames: usize, - data_callback: &mut (dyn FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static), + data_callback: &mut (dyn FnMut(&mut Data, &CallbackInfo) + Send + 'static), ) -> Result<(), Error> { // Pre-fill buffer with equilibrium; user callback overwrites what it wants. stream.equilibrium.fill(buffer); @@ -1305,11 +1320,12 @@ fn process_output( let callback_instant = stream.callback_instant(&status); let delay_duration = frames_to_duration(delay_frames as FrameCount, stream.sample_rate); let playback = callback_instant + delay_duration; - let timestamp = OutputStreamTimestamp { + let timestamp = StreamTimestamp { callback: callback_instant, - playback, + device: playback, }; - data_callback(&mut data, &OutputCallbackInfo { timestamp }); + let xrun = stream.pending_xrun.swap(false, Ordering::Relaxed); + data_callback(&mut data, &CallbackInfo { timestamp, xrun }); } let mut frames_written = 0; @@ -1323,19 +1339,18 @@ fn process_output( // EAGAIN = device cannot currently accept more frames: skip this cycle if no // progress was made, otherwise treat as an underrun (partial period cannot be // completed safely). - Err(err) if err.errno() == libc::EAGAIN => { - if frames_written == 0 { - return Ok(()); - } else { - return Err(ErrorKind::Xrun.into()); - } + Err(err) if err.errno() == libc::EAGAIN && frames_written == 0 => return Ok(()), + // EAGAIN with partial progress, or EPIPE: full underrun recovery required. + Err(err) if err.errno() == libc::EAGAIN || err.errno() == libc::EPIPE => { + return recover_output(stream); } - // EPIPE = xrun: full underrun recovery (prepare) required. - Err(err) if err.errno() == libc::EPIPE => return Err(ErrorKind::Xrun.into()), // ESTRPIPE = hardware suspend: try soft resume first, falling back to underrun // recovery if the hardware doesn't support it. Err(err) if err.errno() == libc::ESTRPIPE => { - return try_resume(&stream.handle).map(|_| ()); + return match try_resume(stream)? { + Poll::Recover => recover_output(stream), + _ => Ok(()), + }; } Err(err) => return Err(err.into()), } @@ -1383,7 +1398,7 @@ impl Stream { timeout: Option, ) -> Stream where - D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + D: FnMut(&Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { let (tx, rx) = trigger(); @@ -1426,7 +1441,7 @@ impl Stream { timeout: Option, ) -> Stream where - D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + D: FnMut(&mut Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { let (tx, rx) = trigger(); @@ -1929,7 +1944,6 @@ impl From for Error { libc::EBUSY | libc::EAGAIN => ErrorKind::DeviceBusy.into(), libc::EINVAL => ErrorKind::UnsupportedConfig.into(), libc::ENOSYS => ErrorKind::UnsupportedOperation.into(), - libc::EPIPE => ErrorKind::Xrun.into(), _ => Error::with_message(ErrorKind::BackendError, err.to_string()), } } diff --git a/src/host/asio/mod.rs b/src/host/asio/mod.rs index ea5a98a15..3d91a1818 100644 --- a/src/host/asio/mod.rs +++ b/src/host/asio/mod.rs @@ -15,8 +15,8 @@ pub use self::{ stream::Stream, }; use crate::{ - Data, DeviceDescription, DeviceId, Error, FrameCount, InputCallbackInfo, OutputCallbackInfo, - SampleFormat, StreamConfig, StreamInstant, SupportedStreamConfig, + CallbackInfo, Data, DeviceDescription, DeviceId, Error, FrameCount, SampleFormat, StreamConfig, + StreamInstant, SupportedStreamConfig, host::com, traits::{DeviceTrait, HostTrait, StreamTrait}, }; @@ -109,7 +109,7 @@ impl DeviceTrait for Device { timeout: Option, ) -> Result where - D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + D: FnMut(&Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { Device::build_input_stream_raw( @@ -131,7 +131,7 @@ impl DeviceTrait for Device { timeout: Option, ) -> Result where - D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + D: FnMut(&mut Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { Device::build_output_stream_raw( diff --git a/src/host/asio/stream.rs b/src/host/asio/stream.rs index 2674dac1f..9a0a05b6c 100644 --- a/src/host/asio/stream.rs +++ b/src/host/asio/stream.rs @@ -4,7 +4,7 @@ extern crate num_traits; use std::{ sync::{ Arc, Mutex, - atomic::{AtomicU8, AtomicU32, AtomicU64, Ordering}, + atomic::{AtomicBool, AtomicU8, AtomicU32, AtomicU64, Ordering}, mpsc, }, time::Duration, @@ -13,14 +13,9 @@ use std::{ use self::num_traits::{FromPrimitive, PrimInt}; use super::Device; use crate::{ - BufferSize, Data, Error, ErrorKind, FrameCount, I24, InputCallbackInfo, InputStreamTimestamp, - OutputCallbackInfo, OutputStreamTimestamp, SampleFormat, SampleRate, StreamConfig, - StreamInstant, - host::{ - com, - error_emit::{emit_error, try_emit_error}, - frames_to_duration, - }, + BufferSize, CallbackInfo, Data, Error, ErrorKind, FrameCount, I24, SampleFormat, SampleRate, + StreamConfig, StreamInstant, StreamTimestamp, + host::{com, error_emit::emit_error, frames_to_duration}, }; /// Shared state for extending the 32-bit `timeGetTime()` millisecond counter into a @@ -77,7 +72,7 @@ impl StreamState { } pub struct Stream { - state: Arc, + playback_state: Arc, driver: Arc, asio_streams: Arc>, callback_id: sys::BufferCallbackId, @@ -95,12 +90,12 @@ impl Stream { } pub fn start(&self) -> Result<(), Error> { - StreamState::Playing.store(&self.state, Ordering::Relaxed); + StreamState::Playing.store(&self.playback_state, Ordering::Relaxed); Ok(()) } pub fn pause(&self) -> Result<(), Error> { - StreamState::Paused.store(&self.state, Ordering::Relaxed); + StreamState::Paused.store(&self.playback_state, Ordering::Relaxed); Ok(()) } @@ -127,7 +122,7 @@ impl Device { _timeout: Option, ) -> Result where - D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + D: FnMut(&Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { crate::validate_stream_config(&config)?; @@ -185,14 +180,16 @@ impl Device { .unwrap_or(0), )); - let state = Arc::new(AtomicU8::new(StreamState::Starting as u8)); + let playback_state = Arc::new(AtomicU8::new(StreamState::Starting as u8)); + let pending_xrun = Arc::new(AtomicBool::new(false)); let driver_event_callback_id = self .add_event_callback( &driver, error_callback, Arc::clone(&hardware_input_latency), true, - Arc::clone(&state), + Arc::clone(&playback_state), + Arc::clone(&pending_xrun), ) .inspect_err(|_| { // Roll back the input stream stored by get_or_create_input_stream. @@ -201,7 +198,8 @@ impl Device { } })?; - let state_cb = Arc::clone(&state); + let playback_state_cb = Arc::clone(&playback_state); + let pending_xrun_cb = Arc::clone(&pending_xrun); let asio_streams = self.asio_streams.clone(); let mut current_buffer_size = buffer_size as i32; let mut last_buffer_index: i32 = -1; @@ -213,7 +211,7 @@ impl Device { // This is most performance critical part of the ASIO bindings. let callback_id = driver.add_callback(move |callback_info| unsafe { // If not playing, return early. - if StreamState::load(&state_cb, Ordering::Relaxed) != StreamState::Playing { + if StreamState::load(&playback_state_cb, Ordering::Relaxed) != StreamState::Playing { return; } @@ -247,6 +245,7 @@ impl Device { let hardware_input_latency = hardware_input_latency.load(Ordering::Relaxed) as usize; let callback_instant = time_base_cb.to_stream_instant(callback_info.system_time); + let xrun = pending_xrun_cb.swap(false, Ordering::Relaxed); /// 1. Write from the ASIO buffer to the interleaved CPAL buffer. /// 2. Deliver the CPAL buffer to the user callback. @@ -261,9 +260,10 @@ impl Device { from_endianness: F, hardware_latency_frames: usize, callback_instant: StreamInstant, + xrun: bool, ) where A: Copy, - D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + D: FnMut(&Data, &CallbackInfo) + Send + 'static, F: Fn(A) -> A, { // 1. Write the ASIO channels to the CPAL buffer. @@ -288,6 +288,7 @@ impl Device { sample_rate, format, hardware_latency_frames, + xrun, ); } } @@ -304,6 +305,7 @@ impl Device { from_le, hardware_input_latency, callback_instant, + xrun, ); } (&sys::AsioSampleType::ASIOSTInt16MSB, SampleFormat::I16) => { @@ -317,6 +319,7 @@ impl Device { from_be, hardware_input_latency, callback_instant, + xrun, ); } @@ -331,6 +334,7 @@ impl Device { from_le, hardware_input_latency, callback_instant, + xrun, ); } (&sys::AsioSampleType::ASIOSTFloat32MSB, SampleFormat::F32) => { @@ -344,6 +348,7 @@ impl Device { from_be, hardware_input_latency, callback_instant, + xrun, ); } @@ -358,6 +363,7 @@ impl Device { from_le, hardware_input_latency, callback_instant, + xrun, ); } (&sys::AsioSampleType::ASIOSTInt32MSB, SampleFormat::I32) => { @@ -371,6 +377,7 @@ impl Device { from_be, hardware_input_latency, callback_instant, + xrun, ); } @@ -385,6 +392,7 @@ impl Device { from_le, hardware_input_latency, callback_instant, + xrun, ); } (&sys::AsioSampleType::ASIOSTFloat64MSB, SampleFormat::F64) => { @@ -398,6 +406,7 @@ impl Device { from_be, hardware_input_latency, callback_instant, + xrun, ); } @@ -411,6 +420,7 @@ impl Device { true, hardware_input_latency, callback_instant, + xrun, ); } (&sys::AsioSampleType::ASIOSTInt24MSB, SampleFormat::I24) => { @@ -423,6 +433,7 @@ impl Device { false, hardware_input_latency, callback_instant, + xrun, ); } @@ -446,9 +457,9 @@ impl Device { return Err(build_stream_err(e)); } - StreamState::Paused.store(&state, Ordering::Relaxed); + StreamState::Paused.store(&playback_state, Ordering::Relaxed); Ok(Stream { - state, + playback_state, driver, asio_streams, callback_id, @@ -466,7 +477,7 @@ impl Device { _timeout: Option, ) -> Result where - D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + D: FnMut(&mut Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { crate::validate_stream_config(&config)?; @@ -525,14 +536,16 @@ impl Device { .unwrap_or(0), )); - let state = Arc::new(AtomicU8::new(StreamState::Starting as u8)); + let playback_state = Arc::new(AtomicU8::new(StreamState::Starting as u8)); + let pending_xrun = Arc::new(AtomicBool::new(false)); let driver_event_callback_id = self .add_event_callback( &driver, error_callback, Arc::clone(&hardware_output_latency), false, - Arc::clone(&state), + Arc::clone(&playback_state), + Arc::clone(&pending_xrun), ) .inspect_err(|_| { // Roll back the output stream stored by get_or_create_output_stream. @@ -541,7 +554,8 @@ impl Device { } })?; - let state_cb = Arc::clone(&state); + let playback_state_cb = Arc::clone(&playback_state); + let pending_xrun_cb = Arc::clone(&pending_xrun); let asio_streams = self.asio_streams.clone(); let mut current_buffer_size = buffer_size as i32; let mut last_buffer_index: i32 = -1; @@ -551,7 +565,7 @@ impl Device { let callback_id = driver.add_callback(move |callback_info| unsafe { // If not playing, return early. - if StreamState::load(&state_cb, Ordering::Relaxed) != StreamState::Playing { + if StreamState::load(&playback_state_cb, Ordering::Relaxed) != StreamState::Playing { return; } @@ -585,6 +599,7 @@ impl Device { let hardware_output_latency = hardware_output_latency.load(Ordering::Relaxed) as usize; let callback_instant = time_base_cb.to_stream_instant(callback_info.system_time); + let xrun = pending_xrun_cb.swap(false, Ordering::Relaxed); // Silence the ASIO buffer that is about to be used. // @@ -613,9 +628,10 @@ impl Device { mix_samples: F, hardware_latency_frames: usize, callback_instant: StreamInstant, + xrun: bool, ) where A: Copy, - D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + D: FnMut(&mut Data, &CallbackInfo) + Send + 'static, F: Fn(A, A) -> A, { let interleaved: &mut [A] = unsafe { cast_slice_mut(interleaved) }; @@ -627,6 +643,7 @@ impl Device { sample_rate, format, hardware_latency_frames, + xrun, ); } let n_channels = interleaved.len() / asio_stream.buffer_size as usize; @@ -662,6 +679,7 @@ impl Device { }, hardware_output_latency, callback_instant, + xrun, ); } (SampleFormat::I16, &sys::AsioSampleType::ASIOSTInt16MSB) => { @@ -678,6 +696,7 @@ impl Device { }, hardware_output_latency, callback_instant, + xrun, ); } (SampleFormat::F32, &sys::AsioSampleType::ASIOSTFloat32LSB) => { @@ -696,6 +715,7 @@ impl Device { }, hardware_output_latency, callback_instant, + xrun, ); } @@ -715,6 +735,7 @@ impl Device { }, hardware_output_latency, callback_instant, + xrun, ); } @@ -732,6 +753,7 @@ impl Device { }, hardware_output_latency, callback_instant, + xrun, ); } (SampleFormat::I32, &sys::AsioSampleType::ASIOSTInt32MSB) => { @@ -748,6 +770,7 @@ impl Device { }, hardware_output_latency, callback_instant, + xrun, ); } @@ -767,6 +790,7 @@ impl Device { }, hardware_output_latency, callback_instant, + xrun, ); } @@ -786,6 +810,7 @@ impl Device { }, hardware_output_latency, callback_instant, + xrun, ); } @@ -800,6 +825,7 @@ impl Device { config.sample_rate, hardware_output_latency, callback_instant, + xrun, ); } @@ -814,6 +840,7 @@ impl Device { config.sample_rate, hardware_output_latency, callback_instant, + xrun, ); } @@ -836,9 +863,9 @@ impl Device { return Err(build_stream_err(e)); } - StreamState::Paused.store(&state, Ordering::Relaxed); + StreamState::Paused.store(&playback_state, Ordering::Relaxed); Ok(Stream { - state, + playback_state, driver, asio_streams, callback_id, @@ -939,7 +966,8 @@ impl Device { error_callback: E, hardware_latency: Arc, is_input: bool, - state: Arc, + playback_state: Arc, + pending_xrun: Arc, ) -> Result where E: FnMut(Error) + Send + 'static, @@ -1012,7 +1040,9 @@ impl Device { sys::AsioMessageSelectors::kAsioResetRequest => { // Guard on Starting: some USB ASIO drivers (ASIO4ALL, Focusrite, etc.) // fire spurious reset/resync requests during driver.start(). - if StreamState::load(&state, Ordering::Relaxed) != StreamState::Starting { + if StreamState::load(&playback_state, Ordering::Relaxed) + != StreamState::Starting + { let _ = timer_tx.send(Error::with_message( ErrorKind::StreamInvalidated, "Stream reset was requested by the ASIO driver", @@ -1024,7 +1054,9 @@ impl Device { // Per the ASIO spec (and matching JUCE's behavior), kAsioResyncRequest // means the driver needs a full stop/reinit/start. It is *not* a simple // xrun notification. - if StreamState::load(&state, Ordering::Relaxed) != StreamState::Starting { + if StreamState::load(&playback_state, Ordering::Relaxed) + != StreamState::Starting + { let _ = timer_tx.send(Error::with_message( ErrorKind::StreamInvalidated, "Stream resynchronization was requested by the ASIO driver", @@ -1033,9 +1065,10 @@ impl Device { true } sys::AsioMessageSelectors::kAsioOverload => { - if StreamState::load(&state, Ordering::Relaxed) == StreamState::Playing { - let _ = - try_emit_error(&error_callback_shared, Error::new(ErrorKind::Xrun)); + if StreamState::load(&playback_state, Ordering::Relaxed) + == StreamState::Playing + { + pending_xrun.store(true, Ordering::Relaxed); } true } @@ -1077,7 +1110,8 @@ impl Device { } }; if should_notify - && StreamState::load(&state, Ordering::Relaxed) != StreamState::Starting + && StreamState::load(&playback_state, Ordering::Relaxed) + != StreamState::Starting { let _ = timer_tx.send(Error::with_message( ErrorKind::StreamInvalidated, @@ -1288,8 +1322,9 @@ unsafe fn process_output_callback_i24( sample_rate: SampleRate, hardware_latency_frames: usize, callback_instant: StreamInstant, + xrun: bool, ) where - D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + D: FnMut(&mut Data, &CallbackInfo) + Send + 'static, { let format = SampleFormat::I24; let interleaved: &mut [I24] = unsafe { cast_slice_mut(interleaved) }; @@ -1301,6 +1336,7 @@ unsafe fn process_output_callback_i24( sample_rate, format, hardware_latency_frames, + xrun, ); } @@ -1364,8 +1400,9 @@ unsafe fn process_input_callback_i24( little_endian: bool, hardware_latency_frames: usize, callback_instant: StreamInstant, + xrun: bool, ) where - D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + D: FnMut(&Data, &CallbackInfo) + Send + 'static, { let format = SampleFormat::I24; @@ -1406,6 +1443,7 @@ unsafe fn process_input_callback_i24( sample_rate, format, hardware_latency_frames, + xrun, ); } } @@ -1419,9 +1457,10 @@ unsafe fn apply_output_callback_to_data( sample_rate: SampleRate, sample_format: SampleFormat, hardware_latency_frames: usize, + xrun: bool, ) where A: Copy, - D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + D: FnMut(&mut Data, &CallbackInfo) + Send + 'static, { let mut data = unsafe { Data::from_parts( @@ -1432,11 +1471,11 @@ unsafe fn apply_output_callback_to_data( }; let delay = frames_to_duration(hardware_latency_frames as FrameCount, sample_rate); let playback = callback_instant + delay; - let timestamp = OutputStreamTimestamp { + let timestamp = StreamTimestamp { callback: callback_instant, - playback, + device: playback, }; - let info = OutputCallbackInfo { timestamp }; + let info = CallbackInfo { timestamp, xrun }; data_callback(&mut data, &info); } @@ -1449,9 +1488,10 @@ unsafe fn apply_input_callback_to_data( sample_rate: SampleRate, format: SampleFormat, hardware_latency_frames: usize, + xrun: bool, ) where A: Copy, - D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + D: FnMut(&Data, &CallbackInfo) + Send + 'static, { let data = unsafe { Data::from_parts( @@ -1464,10 +1504,10 @@ unsafe fn apply_input_callback_to_data( let capture = callback_instant .checked_sub(delay) .unwrap_or(StreamInstant::ZERO); - let timestamp = InputStreamTimestamp { + let timestamp = StreamTimestamp { callback: callback_instant, - capture, + device: capture, }; - let info = InputCallbackInfo { timestamp }; + let info = CallbackInfo { timestamp, xrun }; data_callback(&data, &info); } diff --git a/src/host/audioworklet/mod.rs b/src/host/audioworklet/mod.rs index bcc239d47..1a603c7d3 100644 --- a/src/host/audioworklet/mod.rs +++ b/src/host/audioworklet/mod.rs @@ -18,10 +18,10 @@ use js_sys::wasm_bindgen; use wasm_bindgen::prelude::*; use crate::{ - BufferSize, ChannelCount, Data, DeviceDescription, DeviceDescriptionBuilder, DeviceDirection, - DeviceId, Error, ErrorKind, FrameCount, InputCallbackInfo, OutputCallbackInfo, - OutputStreamTimestamp, SampleFormat, SampleRate, StreamConfig, StreamInstant, - SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, + BufferSize, CallbackInfo, ChannelCount, Data, DeviceDescription, DeviceDescriptionBuilder, + DeviceDirection, DeviceId, Error, ErrorKind, FrameCount, SampleFormat, SampleRate, + StreamConfig, StreamInstant, StreamTimestamp, SupportedBufferSize, SupportedStreamConfig, + SupportedStreamConfigRange, host::frames_to_duration, traits::{DeviceTrait, HostTrait, StreamTrait}, }; @@ -238,7 +238,7 @@ impl DeviceTrait for Device { _timeout: Option, ) -> Result where - D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + D: FnMut(&Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { Err(Error::with_message( @@ -280,12 +280,12 @@ impl DeviceTrait for Device { _timeout: Option, ) -> Result where - D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + D: FnMut(&mut Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { crate::validate_stream_config(&config)?; - // Keep `playback` monotonic: the polled outputLatency can drop when the - // page calls `setSinkId()` to switch output devices, pulling `playback` backward. + // Keep `device` monotonic: the polled outputLatency can drop when the + // page calls `setSinkId()` to switch output devices, pulling `device` backward. let mut data_callback = crate::host::monotonic_output_callback(data_callback); if config.channels > MAX_CHANNELS { return Err(Error::with_message( @@ -416,9 +416,12 @@ impl DeviceTrait for Device { frames_to_duration(frame_size as FrameCount, sample_rate); let latency = Duration::from_nanos(latency_nanos_cb.load(Ordering::Relaxed)); - let playback = callback + (buffer_duration + latency); - let timestamp = OutputStreamTimestamp { callback, playback }; - let info = OutputCallbackInfo { timestamp }; + let device = callback + (buffer_duration + latency); + let timestamp = StreamTimestamp { callback, device }; + let info = CallbackInfo { + timestamp, + xrun: false, + }; (data_callback)(&mut data, &info); }, )) diff --git a/src/host/coreaudio/ios/mod.rs b/src/host/coreaudio/ios/mod.rs index 7957baacf..99dc8f574 100644 --- a/src/host/coreaudio/ios/mod.rs +++ b/src/host/coreaudio/ios/mod.rs @@ -24,10 +24,10 @@ use self::enumerate::{ }; use super::{asbd_from_config, host_time_to_stream_instant}; use crate::{ - BufferSize, ChannelCount, Data, DeviceDescription, DeviceDescriptionBuilder, DeviceId, Error, - ErrorKind, FrameCount, InputCallbackInfo, InputStreamTimestamp, OutputCallbackInfo, - OutputStreamTimestamp, ResultExt, SampleFormat, SampleRate, StreamConfig, StreamInstant, - SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, + BufferSize, CallbackInfo, ChannelCount, Data, DeviceDescription, DeviceDescriptionBuilder, + DeviceId, Error, ErrorKind, FrameCount, ResultExt, SampleFormat, SampleRate, StreamConfig, + StreamInstant, StreamTimestamp, SupportedBufferSize, SupportedStreamConfig, + SupportedStreamConfigRange, host::{ ErrorCallbackArc, equilibrium::fill_equilibrium, frames_to_duration, latch::Latch, try_emit_error, @@ -176,7 +176,7 @@ impl DeviceTrait for Device { _timeout: Option, ) -> Result where - D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + D: FnMut(&Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { crate::validate_stream_config(&config)?; @@ -235,7 +235,7 @@ impl DeviceTrait for Device { _timeout: Option, ) -> Result where - D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + D: FnMut(&mut Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { crate::validate_stream_config(&config)?; @@ -613,7 +613,7 @@ fn setup_input_callback( mut error_callback: E, ) -> Result<(), Error> where - D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + D: FnMut(&Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { let bytes_per_channel = sample_format.sample_size(); @@ -644,8 +644,18 @@ where }; let delay = frames_to_duration(latency_frames as FrameCount, sample_rate); let capture = callback.checked_sub(delay).unwrap_or(StreamInstant::ZERO); - let timestamp = InputStreamTimestamp { callback, capture }; - data_callback(&data, &InputCallbackInfo { timestamp }); + let timestamp = StreamTimestamp { + callback, + device: capture, + }; + // iOS exposes no xrun signal. + data_callback( + &data, + &CallbackInfo { + timestamp, + xrun: false, + }, + ); } Ok(()) })?; @@ -664,7 +674,7 @@ fn setup_output_callback( mut error_callback: E, ) -> Result<(), Error> where - D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + D: FnMut(&mut Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { let bytes_per_channel = sample_format.sample_size(); @@ -708,9 +718,16 @@ where }; let delay = frames_to_duration(latency_frames as FrameCount, sample_rate); let playback = callback + delay; - let timestamp = OutputStreamTimestamp { callback, playback }; + let timestamp = StreamTimestamp { + callback, + device: playback, + }; - let info = OutputCallbackInfo { timestamp }; + // iOS exposes no xrun signal. + let info = CallbackInfo { + timestamp, + xrun: false, + }; data_callback(&mut data, &info); Ok(()) })?; @@ -735,7 +752,7 @@ mod tests { let result = device.build_output_stream( &config, - |_data: &mut [f32], _info: &OutputCallbackInfo| {}, + |_data: &mut [f32], _info: &CallbackInfo| {}, |_err| {}, None, ); diff --git a/src/host/coreaudio/macos/device.rs b/src/host/coreaudio/macos/device.rs index dc0f1bb4b..bde36af6b 100644 --- a/src/host/coreaudio/macos/device.rs +++ b/src/host/coreaudio/macos/device.rs @@ -45,10 +45,10 @@ use super::{ host_time_to_stream_instant, }; use crate::{ - BufferSize, ChannelCount, Data, DeviceDescription, DeviceDescriptionBuilder, DeviceId, Error, - ErrorKind, FrameCount, InputCallbackInfo, InputStreamTimestamp, InterfaceType, - OutputCallbackInfo, OutputStreamTimestamp, ResultExt, SampleFormat, SampleRate, StreamConfig, - StreamInstant, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, + BufferSize, CallbackInfo, ChannelCount, Data, DeviceDescription, DeviceDescriptionBuilder, + DeviceId, Error, ErrorKind, FrameCount, InterfaceType, ResultExt, SampleFormat, SampleRate, + StreamConfig, StreamInstant, StreamTimestamp, SupportedBufferSize, SupportedStreamConfig, + SupportedStreamConfigRange, host::{ ErrorCallbackArc, coreaudio::macos::{StreamInner, loopback::LoopbackDevice}, @@ -323,7 +323,7 @@ impl DeviceTrait for Device { timeout: Option, ) -> Result where - D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + D: FnMut(&Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { Device::build_input_stream_raw( @@ -345,7 +345,7 @@ impl DeviceTrait for Device { timeout: Option, ) -> Result where - D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + D: FnMut(&mut Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { Device::build_output_stream_raw( @@ -702,7 +702,7 @@ impl Device { timeout: Option, ) -> Result where - D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + D: FnMut(&Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { crate::validate_stream_config(&config)?; @@ -754,6 +754,8 @@ impl Device { let error_callback: ErrorCallbackArc = Arc::new(Mutex::new(error_callback)); let error_callback_disconnect = error_callback.clone(); + let pending_xrun = Arc::new(AtomicBool::new(false)); + let pending_xrun_overload = pending_xrun.clone(); // Register the callback that is being called by coreaudio whenever it needs data to be // fed to the audio buffer. @@ -791,8 +793,12 @@ impl Device { device_buffer_frames.unwrap_or(buffer_frames) + extra_latency_frames; let delay = frames_to_duration(latency_frames as FrameCount, sample_rate); let capture = callback.checked_sub(delay).unwrap_or(StreamInstant::ZERO); - let timestamp = InputStreamTimestamp { callback, capture }; - data_callback(&data, &InputCallbackInfo { timestamp }); + let timestamp = StreamTimestamp { + callback, + device: capture, + }; + let xrun = pending_xrun.swap(false, Ordering::Relaxed); + data_callback(&data, &CallbackInfo { timestamp, xrun }); } Ok(()) })?; @@ -812,6 +818,7 @@ impl Device { self.audio_device_id, weak_inner, error_callback_disconnect, + pending_xrun_overload, )?); let stream = Stream::new(inner_arc, monitor, draining, Duration::ZERO); stream.signal_ready(); @@ -827,7 +834,7 @@ impl Device { timeout: Option, ) -> Result where - D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + D: FnMut(&mut Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { crate::validate_stream_config(&config)?; @@ -873,6 +880,8 @@ impl Device { let error_callback: ErrorCallbackArc = Arc::new(Mutex::new(error_callback)); let error_callback_for_render = error_callback.clone(); + let pending_xrun = Arc::new(AtomicBool::new(false)); + let pending_xrun_overload = pending_xrun.clone(); // Register the callback that is being called by coreaudio whenever it needs data to be // fed to the audio buffer. @@ -930,9 +939,13 @@ impl Device { }; let delay = frames_to_duration(latency_frames as FrameCount, sample_rate); let playback = callback + delay; - let timestamp = OutputStreamTimestamp { callback, playback }; + let timestamp = StreamTimestamp { + callback, + device: playback, + }; + let xrun = pending_xrun.swap(false, Ordering::Relaxed); - let info = OutputCallbackInfo { timestamp }; + let info = CallbackInfo { timestamp, xrun }; data_callback(&mut data, &info); Ok(()) })?; @@ -954,12 +967,14 @@ impl Device { weak_inner, error_callback, Some((latency_frames, Scope::Output)), + pending_xrun_overload, )?) } else { Box::new(DisconnectManager::new( self.audio_device_id, weak_inner, error_callback, + pending_xrun_overload, )?) }; let stream = Stream::new(inner_arc, monitor, draining, drain_window); diff --git a/src/host/coreaudio/macos/mod.rs b/src/host/coreaudio/macos/mod.rs index ce2f8c7e9..7b53a40a5 100644 --- a/src/host/coreaudio/macos/mod.rs +++ b/src/host/coreaudio/macos/mod.rs @@ -20,7 +20,7 @@ pub use self::enumerate::{Devices, default_input_device, default_output_device}; use super::{OSStatus, asbd_from_config, check_os_status, host_time_to_stream_instant}; use crate::{ Error, ErrorKind, FrameCount, ResultExt, StreamInstant, - host::{coreaudio::macos::loopback::LoopbackDevice, emit_error, latch::Latch, try_emit_error}, + host::{coreaudio::macos::loopback::LoopbackDevice, emit_error, latch::Latch}, traits::{HostTrait, StreamTrait}, }; @@ -65,20 +65,21 @@ impl HostTrait for Host { /// Type alias for the error callback to reduce complexity type ErrorCallback = dyn FnMut(Error) + Send; -/// Spawns a dedicated thread that registers a single property listener and signals a channel on -/// each change. The listener is deregistered when the returned `Sender<()>` is dropped. -fn spawn_property_listener_thread( +/// Spawns a dedicated thread that registers a single property listener, calling `on_change` on +/// each firing. The listener is deregistered when the returned `Sender<()>` is dropped. +fn spawn_property_listener_thread( object_id: AudioObjectID, address: AudioObjectPropertyAddress, -) -> Result<(mpsc::Receiver<()>, mpsc::Sender<()>), Error> { + on_change: F, +) -> Result, Error> +where + F: FnMut() + Send + 'static, +{ let (shutdown_tx, shutdown_rx) = mpsc::channel::<()>(); - let (change_tx, change_rx) = mpsc::channel::<()>(); let (ready_tx, ready_rx) = mpsc::channel(); std::thread::spawn(move || { - let listener = AudioObjectPropertyListener::new(object_id, address, move || { - let _ = change_tx.send(()); - }); + let listener = AudioObjectPropertyListener::new(object_id, address, on_change); match listener { Ok(_l) => { let _ = ready_tx.send(Ok(())); @@ -97,7 +98,7 @@ fn spawn_property_listener_thread( ) })??; - Ok((change_rx, shutdown_tx)) + Ok(shutdown_tx) } /// A device monitor that can signal when the owning `Stream` handle has been returned to the @@ -126,6 +127,7 @@ impl DisconnectManager { device_id: AudioDeviceID, stream_weak: Weak>, error_callback: Arc>, + pending_xrun: Arc, ) -> Result { let (shutdown_tx, shutdown_rx) = mpsc::channel(); let (disconnect_tx, disconnect_rx) = mpsc::channel::(); @@ -135,7 +137,6 @@ impl DisconnectManager { // AudioObjectPropertyListeners are added and removed on the same thread. let disconnect_tx_alive = disconnect_tx.clone(); let disconnect_tx_rate = disconnect_tx; - let error_callback_overload = error_callback.clone(); std::thread::spawn(move || { let alive_address = AudioObjectPropertyAddress { mSelector: kAudioDevicePropertyDeviceIsAlive, @@ -171,7 +172,7 @@ impl DisconnectManager { }; let overload_listener = AudioObjectPropertyListener::new(device_id, overload_address, move || { - let _ = try_emit_error(&error_callback_overload, ErrorKind::Xrun.into()); + pending_xrun.store(true, Ordering::Relaxed); }); match (alive_listener, rate_listener, overload_listener) { @@ -250,16 +251,42 @@ impl DefaultOutputMonitor { stream_weak: Weak>, error_callback: Arc>, latency_refresh: Option<(Arc, Scope)>, + pending_xrun: Arc, ) -> Result { - let (change_rx, shutdown_tx) = spawn_property_listener_thread( + let (change_tx, change_rx) = mpsc::channel::<()>(); + let shutdown_tx = spawn_property_listener_thread( kAudioObjectSystemObject as AudioObjectID, AudioObjectPropertyAddress { mSelector: kAudioHardwarePropertyDefaultOutputDevice, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain, }, + move || { + let _ = change_tx.send(()); + }, )?; + // The overload listener targets a specific device, so it must be re-registered against + // whatever device is current whenever the default output reroutes. + // Held only to shut down the previous listener thread on drop when reassigned below. + let mut _overload_shutdown_tx = match default_output_device() { + Some(device) => Some(spawn_property_listener_thread( + device.audio_device_id, + AudioObjectPropertyAddress { + mSelector: kAudioDeviceProcessorOverload, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain, + }, + { + let pending_xrun = pending_xrun.clone(); + move || { + pending_xrun.store(true, Ordering::Relaxed); + } + }, + )?), + None => None, + }; + let mut latch = Latch::new(); let waiter = latch.waiter(); @@ -273,41 +300,61 @@ impl DefaultOutputMonitor { let Some(stream) = stream_weak.upgrade() else { break; }; - if default_output_device().is_none() { - if let Ok(mut inner) = stream.try_lock() { - let _ = inner.pause(); + match default_output_device() { + None => { + _overload_shutdown_tx = None; + if let Ok(mut inner) = stream.try_lock() { + let _ = inner.pause(); + } + emit_error( + &error_callback, + Error::with_message( + ErrorKind::DeviceNotAvailable, + "no default output device", + ), + ); } - emit_error( - &error_callback, - Error::with_message( - ErrorKind::DeviceNotAvailable, - "no default output device", - ), - ); - } else { - // DefaultOutput AudioUnit rerouted automatically: recompute and notify - // the buffer depth for the new device. - if let Some((frames, scope)) = &latency_refresh { - if let Ok(inner) = stream.lock() { - let depth = device::get_device_buffer_frame_size(&inner.audio_unit) - .ok() - .map_or(0, |buffer| { - buffer - + device::get_device_extra_latency_frames( - &inner.audio_unit, - *scope, - ) - }); - frames.store(depth, Ordering::Relaxed); + Some(device) => { + // DefaultOutput AudioUnit rerouted automatically: recompute and notify + // the buffer depth for the new device. + if let Some((frames, scope)) = &latency_refresh { + if let Ok(inner) = stream.lock() { + let depth = + device::get_device_buffer_frame_size(&inner.audio_unit) + .ok() + .map_or(0, |buffer| { + buffer + + device::get_device_extra_latency_frames( + &inner.audio_unit, + *scope, + ) + }); + frames.store(depth, Ordering::Relaxed); + } } + _overload_shutdown_tx = spawn_property_listener_thread( + device.audio_device_id, + AudioObjectPropertyAddress { + mSelector: kAudioDeviceProcessorOverload, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain, + }, + { + let pending_xrun = pending_xrun.clone(); + move || { + pending_xrun.store(true, Ordering::Relaxed); + } + }, + ) + .ok(); + emit_error( + &error_callback, + Error::with_message( + ErrorKind::DeviceChanged, + "default output device changed", + ), + ); } - emit_error( - &error_callback, - Error::with_message( - ErrorKind::DeviceChanged, - "default output device changed", - ), - ); } } }) @@ -447,7 +494,7 @@ impl StreamTrait for Stream { #[cfg(test)] mod test { use crate::{ - InputCallbackInfo, OutputCallbackInfo, Sample, default_host, + CallbackInfo, Sample, default_host, traits::{DeviceTrait, HostTrait, StreamTrait}, }; @@ -495,7 +542,7 @@ mod test { let stream = device .build_input_stream( config, - move |data: &[f32], _: &InputCallbackInfo| { + move |data: &[f32], _: &CallbackInfo| { // react to stream events and read or write stream data here. println!("Got data: {:?}", &data[..25]); }, @@ -528,7 +575,7 @@ mod test { let stream = device .build_input_stream( config, - move |data: &[f32], _: &InputCallbackInfo| { + move |data: &[f32], _: &CallbackInfo| { // react to stream events and read or write stream data here. println!("Got data: {:?}", &data[..25]); }, @@ -540,7 +587,7 @@ mod test { std::thread::sleep(std::time::Duration::from_secs(1)); } - fn write_silence(data: &mut [T], _: &OutputCallbackInfo) { + fn write_silence(data: &mut [T], _: &CallbackInfo) { for sample in data.iter_mut() { *sample = Sample::EQUILIBRIUM; } diff --git a/src/host/custom/mod.rs b/src/host/custom/mod.rs index dfbee8094..2de8b5ff8 100644 --- a/src/host/custom/mod.rs +++ b/src/host/custom/mod.rs @@ -7,9 +7,8 @@ use core::time::Duration; use std::fmt; use crate::{ - Data, DeviceDescription, DeviceId, Error, ErrorKind, FrameCount, InputCallbackInfo, - OutputCallbackInfo, SampleFormat, StreamConfig, StreamInstant, SupportedStreamConfig, - SupportedStreamConfigRange, + CallbackInfo, Data, DeviceDescription, DeviceId, Error, ErrorKind, FrameCount, SampleFormat, + StreamConfig, StreamInstant, SupportedStreamConfig, SupportedStreamConfigRange, traits::{DeviceTrait, HostTrait, StreamTrait}, }; @@ -169,8 +168,8 @@ impl Clone for SupportedConfigs { } type ErrorCallback = Box; -type InputCallback = Box; -type OutputCallback = Box; +type InputCallback = Box; +type OutputCallback = Box; trait DeviceErased: Send + Sync { fn description(&self) -> Result; @@ -422,7 +421,7 @@ impl DeviceTrait for Device { timeout: Option, ) -> Result where - D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + D: FnMut(&Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { self.0.build_input_stream_raw( @@ -443,7 +442,7 @@ impl DeviceTrait for Device { timeout: Option, ) -> Result where - D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + D: FnMut(&mut Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { self.0.build_output_stream_raw( diff --git a/src/host/jack/device.rs b/src/host/jack/device.rs index 2496106d3..9bbb3e32c 100644 --- a/src/host/jack/device.rs +++ b/src/host/jack/device.rs @@ -7,10 +7,9 @@ use std::{ use super::{JACK_SAMPLE_FORMAT, stream::Stream}; pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; use crate::{ - BufferSize, ChannelCount, Data, DeviceDescription, DeviceDescriptionBuilder, DeviceDirection, - DeviceId, Error, ErrorKind, InputCallbackInfo, OutputCallbackInfo, SampleFormat, SampleRate, - StreamConfig, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, - traits::DeviceTrait, + BufferSize, CallbackInfo, ChannelCount, Data, DeviceDescription, DeviceDescriptionBuilder, + DeviceDirection, DeviceId, Error, ErrorKind, SampleFormat, SampleRate, StreamConfig, + SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, traits::DeviceTrait, }; const DEFAULT_NUM_CHANNELS: ChannelCount = 2; @@ -183,7 +182,7 @@ impl DeviceTrait for Device { timeout: Option, ) -> Result where - D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + D: FnMut(&Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { if self.is_output() { @@ -267,7 +266,7 @@ impl DeviceTrait for Device { timeout: Option, ) -> Result where - D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + D: FnMut(&mut Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { if self.is_input() { diff --git a/src/host/jack/stream.rs b/src/host/jack/stream.rs index f35851c6e..081f285db 100644 --- a/src/host/jack/stream.rs +++ b/src/host/jack/stream.rs @@ -1,13 +1,15 @@ use std::sync::{ Arc, Mutex, - atomic::{AtomicU8, Ordering}, + atomic::{AtomicBool, AtomicU8, Ordering}, }; use super::JACK_SAMPLE_FORMAT; +#[cfg(feature = "realtime")] +use crate::host::try_emit_error; use crate::{ - ChannelCount, Data, Error, ErrorKind, FrameCount, InputCallbackInfo, InputStreamTimestamp, - OutputCallbackInfo, OutputStreamTimestamp, ResultExt, Sample, SampleRate, StreamInstant, - host::{ErrorCallbackArc, emit_error, frames_to_duration, try_emit_error}, + CallbackInfo, ChannelCount, Data, Error, ErrorKind, FrameCount, ResultExt, Sample, SampleRate, + StreamInstant, StreamTimestamp, + host::{ErrorCallbackArc, emit_error, frames_to_duration}, traits::StreamTrait, }; @@ -34,7 +36,7 @@ impl StreamState { } pub struct Stream { - state: Arc, + playback_state: Arc, async_client: jack::AsyncClient, // Port names are stored in order to connect them to other ports in jack automatically input_port_names: Box<[String]>, @@ -49,7 +51,7 @@ impl Stream { error_callback: E, ) -> Result where - D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + D: FnMut(&Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { let mut ports = vec![]; @@ -64,7 +66,8 @@ impl Stream { ports.push(port); } - let state = Arc::new(AtomicU8::new(StreamState::Starting as u8)); + let playback_state = Arc::new(AtomicU8::new(StreamState::Starting as u8)); + let pending_xrun = Arc::new(AtomicBool::new(false)); let error_callback_ptr: ErrorCallbackArc = Arc::new(Mutex::new(error_callback)); let input_process_handler = LocalProcessHandler::new( @@ -74,24 +77,26 @@ impl Stream { client.buffer_size() as usize, Some(Box::new(data_callback)), None, - state.clone(), + playback_state.clone(), + pending_xrun.clone(), #[cfg(feature = "realtime")] error_callback_ptr.clone(), ); let notification_handler = JackNotificationHandler::new( error_callback_ptr, - state.clone(), + playback_state.clone(), client.sample_rate() as jack::Frames, + pending_xrun, ); let async_client = client .activate_async(notification_handler, input_process_handler) .context("Failed to activate client")?; - StreamState::Paused.store(&state, Ordering::Relaxed); + StreamState::Paused.store(&playback_state, Ordering::Relaxed); Ok(Self { - state, + playback_state, async_client, input_port_names: port_names.into_boxed_slice(), output_port_names: Default::default(), @@ -105,7 +110,7 @@ impl Stream { error_callback: E, ) -> Result where - D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + D: FnMut(&mut Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { let mut ports = vec![]; @@ -120,7 +125,8 @@ impl Stream { ports.push(port); } - let state = Arc::new(AtomicU8::new(StreamState::Starting as u8)); + let playback_state = Arc::new(AtomicU8::new(StreamState::Starting as u8)); + let pending_xrun = Arc::new(AtomicBool::new(false)); let error_callback_ptr: ErrorCallbackArc = Arc::new(Mutex::new(error_callback)); let output_process_handler = LocalProcessHandler::new( @@ -130,24 +136,26 @@ impl Stream { client.buffer_size() as usize, None, Some(Box::new(data_callback)), - state.clone(), + playback_state.clone(), + pending_xrun.clone(), #[cfg(feature = "realtime")] error_callback_ptr.clone(), ); let notification_handler = JackNotificationHandler::new( error_callback_ptr, - state.clone(), + playback_state.clone(), client.sample_rate() as jack::Frames, + pending_xrun, ); let async_client = client .activate_async(notification_handler, output_process_handler) .context("Failed to activate client")?; - StreamState::Paused.store(&state, Ordering::Relaxed); + StreamState::Paused.store(&playback_state, Ordering::Relaxed); Ok(Self { - state, + playback_state, async_client, input_port_names: Box::default(), output_port_names: port_names.into_boxed_slice(), @@ -219,17 +227,17 @@ impl Stream { impl StreamTrait for Stream { fn start(&self) -> Result<(), Error> { - StreamState::Playing.store(&self.state, Ordering::Relaxed); + StreamState::Playing.store(&self.playback_state, Ordering::Relaxed); Ok(()) } fn pause(&self) -> Result<(), Error> { - StreamState::Paused.store(&self.state, Ordering::Relaxed); + StreamState::Paused.store(&self.playback_state, Ordering::Relaxed); Ok(()) } fn stop(&self, timeout: Option) -> Result<(), Error> { - StreamState::Paused.store(&self.state, Ordering::Relaxed); + StreamState::Paused.store(&self.playback_state, Ordering::Relaxed); let is_output = !self.output_port_names.is_empty(); if is_output && timeout != Some(std::time::Duration::ZERO) { @@ -259,8 +267,8 @@ impl StreamTrait for Stream { } } -type InputDataCallback = Box; -type OutputDataCallback = Box; +type InputDataCallback = Box; +type OutputDataCallback = Box; struct LocalProcessHandler { /// No new ports are allowed to be created after the creation of the LocalProcessHandler as that would invalidate the buffer sizes @@ -275,7 +283,8 @@ struct LocalProcessHandler { // JACK audio samples are 32-bit float (unless you do some custom dark magic) temp_input_buffer: Vec, temp_output_buffer: Vec, - state: Arc, + playback_state: Arc, + pending_xrun: Arc, #[cfg(feature = "realtime")] error_callback: ErrorCallbackArc, #[cfg(feature = "realtime")] @@ -283,7 +292,7 @@ struct LocalProcessHandler { } impl LocalProcessHandler { - #[cfg_attr(feature = "realtime", expect(clippy::too_many_arguments))] + #[expect(clippy::too_many_arguments)] fn new( out_ports: Vec>, in_ports: Vec>, @@ -291,7 +300,8 @@ impl LocalProcessHandler { buffer_size: usize, input_data_callback: Option, output_data_callback: Option, - state: Arc, + playback_state: Arc, + pending_xrun: Arc, #[cfg(feature = "realtime")] error_callback: ErrorCallbackArc, ) -> Self { let temp_input_buffer = vec![0.0; in_ports.len() * buffer_size]; @@ -306,7 +316,8 @@ impl LocalProcessHandler { output_data_callback, temp_input_buffer, temp_output_buffer, - state, + playback_state, + pending_xrun, #[cfg(feature = "realtime")] error_callback, #[cfg(feature = "realtime")] @@ -328,7 +339,7 @@ impl jack::ProcessHandler for LocalProcessHandler { client: &jack::Client, process_scope: &jack::ProcessScope, ) -> jack::Control { - if StreamState::load(&self.state, Ordering::Relaxed) != StreamState::Playing { + if StreamState::load(&self.playback_state, Ordering::Relaxed) != StreamState::Playing { // JACK does not zero-fill output port buffers before calling the process handler for port in &mut self.out_ports { port.as_mut_slice(process_scope).fill(f32::EQUILIBRIUM); @@ -424,6 +435,8 @@ impl jack::ProcessHandler for LocalProcessHandler { self.sample_rate, ); + let xrun = self.pending_xrun.swap(false, Ordering::Relaxed); + if let Some(input_callback) = &mut self.input_data_callback { // Let's get the data from the input ports and run the callback @@ -452,8 +465,11 @@ impl jack::ProcessHandler for LocalProcessHandler { let capture = start_cycle_instant .checked_sub(latency) .unwrap_or(StreamInstant::ZERO); - let timestamp = InputStreamTimestamp { callback, capture }; - let info = InputCallbackInfo { timestamp }; + let timestamp = StreamTimestamp { + callback, + device: capture, + }; + let info = CallbackInfo { timestamp, xrun }; input_callback(&data, &info); } @@ -488,8 +504,11 @@ impl jack::ProcessHandler for LocalProcessHandler { } }, }; - let timestamp = OutputStreamTimestamp { callback, playback }; - let info = OutputCallbackInfo { timestamp }; + let timestamp = StreamTimestamp { + callback, + device: playback, + }; + let info = CallbackInfo { timestamp, xrun }; output_callback(&mut data, &info); // Deinterlace @@ -546,27 +565,30 @@ fn hardware_latency_frames( /// Receives notifications from the JACK server on JACK's notification thread (single-threaded). struct JackNotificationHandler { error_callback_ptr: ErrorCallbackArc, - state: Arc, + playback_state: Arc, configured_sample_rate: jack::Frames, + pending_xrun: Arc, } impl JackNotificationHandler { pub fn new( error_callback_ptr: ErrorCallbackArc, - state: Arc, + playback_state: Arc, configured_sample_rate: jack::Frames, + pending_xrun: Arc, ) -> Self { JackNotificationHandler { error_callback_ptr, - state, + playback_state, configured_sample_rate, + pending_xrun, } } } impl jack::NotificationHandler for JackNotificationHandler { unsafe fn shutdown(&mut self, _status: jack::ClientStatus, reason: &str) { - if StreamState::load(&self.state, Ordering::Relaxed) == StreamState::Starting { + if StreamState::load(&self.playback_state, Ordering::Relaxed) == StreamState::Starting { return; } emit_error( @@ -583,7 +605,7 @@ impl jack::NotificationHandler for JackNotificationHandler { // One of these notifications is sent every time a client is started. return jack::Control::Continue; } - if StreamState::load(&self.state, Ordering::Relaxed) != StreamState::Starting { + if StreamState::load(&self.playback_state, Ordering::Relaxed) != StreamState::Starting { emit_error( &self.error_callback_ptr, Error::with_message( @@ -596,8 +618,8 @@ impl jack::NotificationHandler for JackNotificationHandler { } fn xrun(&mut self, _: &jack::Client) -> jack::Control { - if StreamState::load(&self.state, Ordering::Relaxed) != StreamState::Starting { - let _ = try_emit_error(&self.error_callback_ptr, ErrorKind::Xrun.into()); + if StreamState::load(&self.playback_state, Ordering::Relaxed) != StreamState::Starting { + self.pending_xrun.store(true, Ordering::Relaxed); } jack::Control::Continue } diff --git a/src/host/mod.rs b/src/host/mod.rs index 25b59ff33..0e3f3e5c8 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -208,7 +208,34 @@ pub(crate) mod error_emit; ) ), ))] -pub(crate) use error_emit::{emit_error, try_emit_error}; +pub(crate) use error_emit::emit_error; + +#[cfg(any( + target_vendor = "apple", + target_os = "android", + all( + feature = "jack", + feature = "realtime", + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "macos", + target_os = "windows", + ) + ), + all( + feature = "pipewire", + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + ) + ), +))] +pub(crate) use error_emit::try_emit_error; /// Convert a frame count at a given sample rate to a [`std::time::Duration`]. #[cfg(any( @@ -258,15 +285,15 @@ fn non_decreasing(floor: &mut u64, instant: crate::StreamInstant) -> crate::Stre #[allow(dead_code)] pub(crate) fn monotonic_input_callback( mut data_callback: D, -) -> impl FnMut(&crate::Data, &crate::InputCallbackInfo) + Send + 'static +) -> impl FnMut(&crate::Data, &crate::CallbackInfo) + Send + 'static where - D: FnMut(&crate::Data, &crate::InputCallbackInfo) + Send + 'static, + D: FnMut(&crate::Data, &crate::CallbackInfo) + Send + 'static, { // FnMut runs on one thread at a time, so the floor needs no synchronization. let mut floor = 0u64; move |data, info| { let mut info = *info; - info.timestamp.capture = non_decreasing(&mut floor, info.timestamp.capture); + info.timestamp.device = non_decreasing(&mut floor, info.timestamp.device); data_callback(data, &info); } } @@ -275,14 +302,14 @@ where #[allow(dead_code)] pub(crate) fn monotonic_output_callback( mut data_callback: D, -) -> impl FnMut(&mut crate::Data, &crate::OutputCallbackInfo) + Send + 'static +) -> impl FnMut(&mut crate::Data, &crate::CallbackInfo) + Send + 'static where - D: FnMut(&mut crate::Data, &crate::OutputCallbackInfo) + Send + 'static, + D: FnMut(&mut crate::Data, &crate::CallbackInfo) + Send + 'static, { let mut floor = 0u64; move |data, info| { let mut info = *info; - info.timestamp.playback = non_decreasing(&mut floor, info.timestamp.playback); + info.timestamp.device = non_decreasing(&mut floor, info.timestamp.device); data_callback(data, &info); } } diff --git a/src/host/null/mod.rs b/src/host/null/mod.rs index 157aa3d40..2cce813f3 100644 --- a/src/host/null/mod.rs +++ b/src/host/null/mod.rs @@ -6,9 +6,8 @@ use std::fmt; use std::time::Duration; use crate::{ - Data, DeviceDescription, DeviceDescriptionBuilder, DeviceId, Error, FrameCount, - InputCallbackInfo, OutputCallbackInfo, SampleFormat, StreamConfig, StreamInstant, - SupportedStreamConfig, SupportedStreamConfigRange, + CallbackInfo, Data, DeviceDescription, DeviceDescriptionBuilder, DeviceId, Error, FrameCount, + SampleFormat, StreamConfig, StreamInstant, SupportedStreamConfig, SupportedStreamConfigRange, traits::{DeviceTrait, HostTrait, StreamTrait}, }; @@ -79,7 +78,7 @@ impl DeviceTrait for Device { _timeout: Option, ) -> Result where - D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + D: FnMut(&Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { unimplemented!() @@ -95,7 +94,7 @@ impl DeviceTrait for Device { _timeout: Option, ) -> Result where - D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + D: FnMut(&mut Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { unimplemented!() diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 2c0c8241e..9402f69ec 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -27,10 +27,10 @@ use pipewire::{ use super::stream::Stream; use crate::{ - BufferSize, ChannelCount, Data, DeviceDescription, DeviceDescriptionBuilder, DeviceDirection, - DeviceId, DeviceType, Error, ErrorKind, FrameCount, HostId, InputCallbackInfo, InterfaceType, - OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, SupportedBufferSize, - SupportedStreamConfig, SupportedStreamConfigRange, + BufferSize, CallbackInfo, ChannelCount, Data, DeviceDescription, DeviceDescriptionBuilder, + DeviceDirection, DeviceId, DeviceType, Error, ErrorKind, FrameCount, HostId, InterfaceType, + SampleFormat, SampleRate, StreamConfig, SupportedBufferSize, SupportedStreamConfig, + SupportedStreamConfigRange, host::{ Notify, emit_error, latch::Latch, @@ -352,7 +352,7 @@ impl DeviceTrait for Device { timeout: Option, ) -> Result where - D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + D: FnMut(&Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { crate::validate_stream_config(&config)?; @@ -544,7 +544,7 @@ impl DeviceTrait for Device { timeout: Option, ) -> Result where - D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + D: FnMut(&mut Data, &CallbackInfo) + Send + 'static, E: FnMut(Error) + Send + 'static, { crate::validate_stream_config(&config)?; diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index 587cc3a34..895096129 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -33,8 +33,8 @@ use pipewire::{ }; use crate::{ - Data, Error, ErrorKind, FrameCount, InputCallbackInfo, InputStreamTimestamp, - OutputCallbackInfo, OutputStreamTimestamp, SampleFormat, StreamConfig, StreamInstant, + CallbackInfo, Data, Error, ErrorKind, FrameCount, SampleFormat, StreamConfig, StreamInstant, + StreamTimestamp, host::{ ErrorCallbackArc, Notify, emit_error, equilibrium::fill_equilibrium, frames_to_duration, latch::Latch, try_emit_error, @@ -341,19 +341,18 @@ impl UserData { } } - fn check_xrun(&mut self) { + fn check_xrun(&mut self) -> bool { if self.spa_io_clock.is_null() { - return; + return false; } // spa/node/io.h; not present in bindgen output on all libspa versions. const SPA_IO_CLOCK_FLAG_XRUN_RECOVER: u32 = 1 << 1; // io_changed and process run on the same thread. let flags = unsafe { (*self.spa_io_clock).flags }; let recovering = flags & SPA_IO_CLOCK_FLAG_XRUN_RECOVER != 0; - if recovering && !self.xrun_recovering { - let _ = try_emit_error(&self.error_callback, ErrorKind::Xrun.into()); - } + let xrun = recovering && !self.xrun_recovering; self.xrun_recovering = recovering; + xrun } } @@ -370,9 +369,15 @@ fn pw_stream_time(stream: &pw::stream::Stream) -> Option