diff --git a/CHANGELOG.md b/CHANGELOG.md index ffe9f7e40..0cf25fb01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,7 +42,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **ASIO**: Timestamps now include driver-reported hardware latency. - **ASIO**: Hardware latency is now re-queried when the driver reports `kAsioLatenciesChanged`. - **ASIO**: Stream error callback now receives `StreamError::BufferUnderrun` on `kAsioResyncRequest`. -- **ASIO**: Stream error callback now receives `StreamError::StreamInvalidated` when the driver reports a sample rate change (`sampleRateDidChange`) of 1 Hz or more from the configured rate. +- **ASIO**: Stream error callback now receives `StreamError::StreamInvalidated` when the driver reports a sample + rate change (`sampleRateDidChange`) of 1 Hz or more from the configured rate. +- **AudioWorklet**: `BufferSize::Fixed` now sets `renderSizeHint` on the `AudioContext`. - **CoreAudio**: Timestamps now include device latency and safety offset. - **JACK**: Timestamps now use the precise hardware deadline. - **Linux/BSD**: Default host in order from first to last available now is: PipeWire, PulseAudio, ALSA. diff --git a/examples/custom.rs b/examples/custom.rs index e1f40d045..e35678446 100644 --- a/examples/custom.rs +++ b/examples/custom.rs @@ -33,6 +33,9 @@ struct StreamControls { pause: AtomicBool, } +const CHANNEL_COUNT: cpal::ChannelCount = 2; +const BUFFER_SIZE: cpal::FrameCount = 4096; + impl HostTrait for MyHost { type Device = MyDevice; type Devices = std::iter::Once; @@ -81,10 +84,13 @@ impl DeviceTrait for MyDevice { &self, ) -> Result { Ok(std::iter::once(cpal::SupportedStreamConfigRange::new( - 2, + CHANNEL_COUNT, 44100, 44100, - cpal::SupportedBufferSize::Unknown, + cpal::SupportedBufferSize::Range { + min: BUFFER_SIZE, + max: BUFFER_SIZE, + }, cpal::SampleFormat::F32, ))) } @@ -99,9 +105,12 @@ impl DeviceTrait for MyDevice { &self, ) -> Result { Ok(cpal::SupportedStreamConfig::new( - 2, + CHANNEL_COUNT, 44100, - cpal::SupportedBufferSize::Unknown, + cpal::SupportedBufferSize::Range { + min: BUFFER_SIZE, + max: BUFFER_SIZE, + }, cpal::SampleFormat::I16, )) } @@ -145,7 +154,7 @@ impl DeviceTrait for MyDevice { let start = Instant::now(); let thread_controls = controls.clone(); let handle = std::thread::spawn(move || { - let mut buffer = [0.0_f32; 4096]; + let mut buffer = [0.0_f32; BUFFER_SIZE as usize * CHANNEL_COUNT as usize]; while !thread_controls.exit.load(Ordering::Relaxed) { std::thread::sleep(std::time::Duration::from_secs_f32( buffer.len() as f32 / 44100.0, @@ -203,6 +212,10 @@ impl StreamTrait for MyStream { let elapsed = self.start.elapsed(); cpal::StreamInstant::new(elapsed.as_secs(), elapsed.subsec_nanos()) } + + fn buffer_size(&self) -> Result { + Ok(BUFFER_SIZE) + } } // streams are expected to stop when dropped diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index 9a2394f2b..3e91461e0 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -721,8 +721,8 @@ impl StreamTrait for Stream { now_stream_instant() } - fn buffer_size(&self) -> Option { - let stream = self.inner.lock().ok()?; + fn buffer_size(&self) -> Result { + let stream = self.inner.lock().unwrap(); // frames_per_data_callback is only set for BufferSize::Fixed; for Default AAudio // schedules callbacks at the burst size, so that is the best available estimate. @@ -730,10 +730,6 @@ impl StreamTrait for Stream { Some(size) if size > 0 => size, _ => stream.frames_per_burst(), }; - if frames > 0 { - Some(frames as crate::FrameCount) - } else { - None - } + Ok(frames as crate::FrameCount) } } diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index 3fc234ad3..910b81f3d 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -1289,8 +1289,8 @@ impl StreamTrait for Stream { .expect("stream duration exceeded `StreamInstant` range") } - fn buffer_size(&self) -> Option { - Some(self.inner.period_frames as FrameCount) + fn buffer_size(&self) -> Result { + Ok(self.inner.period_frames as FrameCount) } } diff --git a/src/host/asio/mod.rs b/src/host/asio/mod.rs index 11d0783a0..60e1ca4ef 100644 --- a/src/host/asio/mod.rs +++ b/src/host/asio/mod.rs @@ -160,7 +160,7 @@ impl StreamTrait for Stream { Stream::now(self) } - fn buffer_size(&self) -> Option { + fn buffer_size(&self) -> Result { Stream::buffer_size(self) } } diff --git a/src/host/asio/stream.rs b/src/host/asio/stream.rs index 44632fbad..a857c3432 100644 --- a/src/host/asio/stream.rs +++ b/src/host/asio/stream.rs @@ -81,13 +81,14 @@ impl Stream { Ok(()) } - pub fn buffer_size(&self) -> Option { - let streams = self.asio_streams.lock().ok()?; - streams + pub fn buffer_size(&self) -> Result { + let streams = self.asio_streams.lock().unwrap(); + Ok(streams .output .as_ref() .or(streams.input.as_ref()) - .map(|s| s.buffer_size as crate::FrameCount) + .expect("ASIO stream has neither input nor output") + .buffer_size as crate::FrameCount) } } diff --git a/src/host/audioworklet/mod.rs b/src/host/audioworklet/mod.rs index 26154a56b..375460b1b 100644 --- a/src/host/audioworklet/mod.rs +++ b/src/host/audioworklet/mod.rs @@ -5,6 +5,10 @@ mod dependent_module; use js_sys::wasm_bindgen; +use std::sync::{ + atomic::{AtomicU64, Ordering}, + Arc, +}; use crate::dependent_module; use wasm_bindgen::prelude::*; @@ -30,6 +34,7 @@ pub struct Host; pub struct Stream { audio_context: web_sys::AudioContext, + buffer_size_frames: Arc, } pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; @@ -41,6 +46,9 @@ const MAX_SAMPLE_RATE: SampleRate = 96_000; const DEFAULT_SAMPLE_RATE: SampleRate = 44_100; const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32; +// https://webaudio.github.io/web-audio-api/#render-quantum-size +const DEFAULT_RENDER_SIZE: u64 = 128; + impl Host { pub fn new() -> Result { if Self::is_available() { @@ -195,6 +203,13 @@ impl DeviceTrait for Device { let stream_opts = web_sys::AudioContextOptions::new(); stream_opts.set_sample_rate(config.sample_rate as f32); + if let crate::BufferSize::Fixed(n) = config.buffer_size { + let _ = js_sys::Reflect::set( + stream_opts.as_ref(), + &JsValue::from_str("renderSizeHint"), + &JsValue::from_f64(n as f64), + ); + } let audio_context = web_sys::AudioContext::new_with_context_options(&stream_opts).map_err( |err| -> BuildStreamError { @@ -213,6 +228,12 @@ impl DeviceTrait for Device { destination.set_channel_count(config.channels as u32); } + let initial_quantum = match config.buffer_size { + crate::BufferSize::Fixed(n) => n as u64, + crate::BufferSize::Default => DEFAULT_RENDER_SIZE, + }; + let buffer_size_frames = Arc::new(AtomicU64::new(initial_quantum)); + let buffer_size_frames_cb = buffer_size_frames.clone(); let ctx = audio_context.clone(); wasm_bindgen_futures::spawn_local(async move { let result: Result<(), JsValue> = async move { @@ -255,6 +276,7 @@ impl DeviceTrait for Device { &wasm_bindgen::memory(), &WasmAudioProcessor::new(Box::new( move |interleaved_data, frame_size, sample_rate, now| { + buffer_size_frames_cb.store(frame_size as u64, Ordering::Relaxed); let data = interleaved_data.as_mut_ptr() as *mut (); let mut data = unsafe { Data::from_parts(data, interleaved_data.len(), sample_format) @@ -295,11 +317,18 @@ impl DeviceTrait for Device { } }); - Ok(Stream { audio_context }) + Ok(Stream { + audio_context, + buffer_size_frames, + }) } } impl StreamTrait for Stream { + fn buffer_size(&self) -> Result { + Ok(self.buffer_size_frames.load(Ordering::Relaxed) as crate::FrameCount) + } + fn play(&self) -> Result<(), PlayStreamError> { match self.audio_context.resume() { Ok(_) => Ok(()), diff --git a/src/host/coreaudio/ios/mod.rs b/src/host/coreaudio/ios/mod.rs index 5fcd4af33..0b5b0c3b9 100644 --- a/src/host/coreaudio/ios/mod.rs +++ b/src/host/coreaudio/ios/mod.rs @@ -285,8 +285,8 @@ impl StreamTrait for Stream { host_time_to_stream_instant(m_host_time).expect("mach_timebase_info failed") } - fn buffer_size(&self) -> Option { - Some(get_device_buffer_frames() as crate::FrameCount) + fn buffer_size(&self) -> Result { + Ok(get_device_buffer_frames() as crate::FrameCount) } } diff --git a/src/host/coreaudio/macos/mod.rs b/src/host/coreaudio/macos/mod.rs index b0bb9499d..64d5c35a1 100644 --- a/src/host/coreaudio/macos/mod.rs +++ b/src/host/coreaudio/macos/mod.rs @@ -268,12 +268,12 @@ impl StreamTrait for Stream { host_time_to_stream_instant(m_host_time).expect("mach_timebase_info failed") } - fn buffer_size(&self) -> Option { - let stream = self.inner.lock().ok()?; + fn buffer_size(&self) -> Result { + let stream = self.inner.lock().unwrap(); device::get_device_buffer_frame_size(&stream.audio_unit) - .ok() .map(|size| size as crate::FrameCount) + .map_err(|_| crate::StreamError::DeviceNotAvailable) } } diff --git a/src/host/custom/mod.rs b/src/host/custom/mod.rs index 7b7ebbbd1..ec874013f 100644 --- a/src/host/custom/mod.rs +++ b/src/host/custom/mod.rs @@ -178,6 +178,7 @@ trait StreamErased: Send + Sync { fn play(&self) -> Result<(), PlayStreamError>; fn pause(&self) -> Result<(), PauseStreamError>; fn now(&self) -> StreamInstant; + fn buffer_size(&self) -> Result; } fn device_to_erased(d: impl DeviceErased + 'static) -> Device { @@ -317,6 +318,10 @@ where fn now(&self) -> StreamInstant { ::now(self) } + + fn buffer_size(&self) -> Result { + ::buffer_size(self) + } } // implementations of HostTrait, DeviceTrait, and StreamTrait for custom versions @@ -444,4 +449,8 @@ impl StreamTrait for Stream { fn now(&self) -> StreamInstant { self.0.now() } + + fn buffer_size(&self) -> Result { + self.0.buffer_size() + } } diff --git a/src/host/emscripten/mod.rs b/src/host/emscripten/mod.rs index 18d9f8793..e33863b6c 100644 --- a/src/host/emscripten/mod.rs +++ b/src/host/emscripten/mod.rs @@ -12,8 +12,8 @@ use web_sys::AudioContext; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BufferSize, BuildStreamError, Data, DefaultStreamConfigError, DeviceDescription, - DeviceDescriptionBuilder, DeviceId, DeviceIdError, DeviceNameError, DevicesError, + BufferSize, BuildStreamError, ChannelCount, Data, DefaultStreamConfigError, DeviceDescription, + DeviceDescriptionBuilder, DeviceId, DeviceIdError, DeviceNameError, DevicesError, FrameCount, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, StreamInstant, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, @@ -36,8 +36,8 @@ pub struct Device; #[wasm_bindgen] #[derive(Clone)] pub struct Stream { - // A reference to an `AudioContext` object. audio_ctxt: AudioContext, + buffer_size_frames: FrameCount, } // WASM runs in a single-threaded environment, so Send and Sync are safe by design. @@ -50,14 +50,14 @@ crate::assert_stream_sync!(Stream); pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; -const MIN_CHANNELS: u16 = 1; -const MAX_CHANNELS: u16 = 32; +const MIN_CHANNELS: ChannelCount = 1; +const MAX_CHANNELS: ChannelCount = 32; const MIN_SAMPLE_RATE: SampleRate = 8_000; const MAX_SAMPLE_RATE: SampleRate = 96_000; const DEFAULT_SAMPLE_RATE: SampleRate = 44_100; -const MIN_BUFFER_SIZE: u32 = 1; -const MAX_BUFFER_SIZE: u32 = u32::MAX; -const DEFAULT_BUFFER_SIZE: usize = 2048; +const MIN_BUFFER_SIZE: FrameCount = 1; +const MAX_BUFFER_SIZE: FrameCount = FrameCount::MAX; +const DEFAULT_BUFFER_SIZE: FrameCount = 2048; const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32; impl Host { @@ -219,14 +219,17 @@ impl DeviceTrait for Device { if !(MIN_BUFFER_SIZE..=MAX_BUFFER_SIZE).contains(&v) { return Err(BuildStreamError::StreamConfigNotSupported); } - v as usize + v } BufferSize::Default => DEFAULT_BUFFER_SIZE, }; // Create the stream. let audio_ctxt = AudioContext::new().expect("webaudio is not present on this system"); - let stream = Stream { audio_ctxt }; + let stream = Stream { + audio_ctxt, + buffer_size_frames, + }; // Use `set_timeout` to invoke a Rust callback repeatedly. // @@ -241,7 +244,7 @@ impl DeviceTrait for Device { data_callback, config, sample_format, - buffer_size_frames as u32, + buffer_size_frames, ); Ok(stream) @@ -249,6 +252,10 @@ impl DeviceTrait for Device { } impl StreamTrait for Stream { + fn buffer_size(&self) -> Result { + Ok(self.buffer_size_frames) + } + fn play(&self) -> Result<(), PlayStreamError> { let future = JsFuture::from( self.audio_ctxt @@ -286,7 +293,7 @@ impl StreamTrait for Stream { fn audio_callback_fn( mut data_callback: AssertUnwindSafe, -) -> impl FnOnce(Stream, StreamConfig, SampleFormat, u32) +) -> impl FnOnce(Stream, StreamConfig, SampleFormat, FrameCount) where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, { @@ -368,7 +375,7 @@ fn set_timeout( data_callback: AssertUnwindSafe, config: StreamConfig, sample_format: SampleFormat, - buffer_size_frames: u32, + buffer_size_frames: FrameCount, ) where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, { diff --git a/src/host/jack/stream.rs b/src/host/jack/stream.rs index 6659cea2b..4c305c82d 100644 --- a/src/host/jack/stream.rs +++ b/src/host/jack/stream.rs @@ -225,8 +225,8 @@ impl StreamTrait for Stream { micros_to_stream_instant(self.async_client.as_client().time()) } - fn buffer_size(&self) -> Option { - Some(self.async_client.as_client().buffer_size() as crate::FrameCount) + fn buffer_size(&self) -> Result { + Ok(self.async_client.as_client().buffer_size() as crate::FrameCount) } } diff --git a/src/host/null/mod.rs b/src/host/null/mod.rs index ac5cbe57f..16e7a2457 100644 --- a/src/host/null/mod.rs +++ b/src/host/null/mod.rs @@ -147,6 +147,10 @@ impl StreamTrait for Stream { fn now(&self) -> crate::StreamInstant { unimplemented!() } + + fn buffer_size(&self) -> Result { + unimplemented!() + } } impl Iterator for Devices { diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 2676b4625..62cd751e8 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -296,7 +296,11 @@ impl DeviceTrait for Device { let (pw_init_tx, pw_init_rx) = std::sync::mpsc::channel::(); let device = self.clone(); let wait_timeout = timeout.unwrap_or(Duration::from_secs(2)); - let last_quantum = Arc::new(AtomicU64::new(0)); + let initial_quantum = match config.buffer_size { + crate::BufferSize::Fixed(n) => n as u64, + crate::BufferSize::Default => self.quantum as u64, + }; + let last_quantum = Arc::new(AtomicU64::new(initial_quantum)); let last_quantum_clone = last_quantum.clone(); let handle = thread::Builder::new() .name("pw_in".to_owned()) @@ -372,7 +376,11 @@ impl DeviceTrait for Device { let (pw_init_tx, pw_init_rx) = std::sync::mpsc::channel::(); let device = self.clone(); let wait_timeout = timeout.unwrap_or(Duration::from_secs(2)); - let last_quantum = Arc::new(AtomicU64::new(0)); + let initial_quantum = match config.buffer_size { + crate::BufferSize::Fixed(n) => n as u64, + crate::BufferSize::Default => self.quantum as u64, + }; + let last_quantum = Arc::new(AtomicU64::new(initial_quantum)); let last_quantum_clone = last_quantum.clone(); let handle = thread::Builder::new() .name("pw_out".to_owned()) diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index 314fa0fcf..b82f26bbc 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -71,11 +71,8 @@ impl StreamTrait for Stream { monotonic_stream_instant().expect("clock_gettime failed") } - fn buffer_size(&self) -> Option { - match self.last_quantum.load(Ordering::Relaxed) { - 0 => None, - n => Some(n as _), - } + fn buffer_size(&self) -> Result { + Ok(self.last_quantum.load(Ordering::Relaxed) as _) } } diff --git a/src/host/pulseaudio/stream.rs b/src/host/pulseaudio/stream.rs index f55c21968..07e548ff0 100644 --- a/src/host/pulseaudio/stream.rs +++ b/src/host/pulseaudio/stream.rs @@ -55,7 +55,7 @@ impl StreamTrait for Stream { StreamInstant::new(elapsed.as_secs(), elapsed.subsec_nanos()) } - fn buffer_size(&self) -> Option { + fn buffer_size(&self) -> Result { let (spec, bytes) = match self { Stream::Playback(s, _) => ( s.sample_spec(), @@ -64,11 +64,7 @@ impl StreamTrait for Stream { Stream::Record(s, _) => (s.sample_spec(), s.buffer_attr().fragment_size as usize), }; let frame_size = spec.channels as usize * spec.format.bytes_per_sample(); - if bytes > 0 { - Some((bytes / frame_size) as _) - } else { - None - } + Ok((bytes / frame_size) as _) } } diff --git a/src/host/wasapi/stream.rs b/src/host/wasapi/stream.rs index d0aa8a8f8..90d00954e 100644 --- a/src/host/wasapi/stream.rs +++ b/src/host/wasapi/stream.rs @@ -250,8 +250,8 @@ impl StreamTrait for Stream { ) } - fn buffer_size(&self) -> Option { - Some(self.period_frames) + fn buffer_size(&self) -> Result { + Ok(self.period_frames) } } diff --git a/src/host/webaudio/mod.rs b/src/host/webaudio/mod.rs index 745b500dd..30b2b2a6b 100644 --- a/src/host/webaudio/mod.rs +++ b/src/host/webaudio/mod.rs @@ -500,8 +500,8 @@ impl StreamTrait for Stream { StreamInstant::from_secs_f64(self.ctx.current_time()) } - fn buffer_size(&self) -> Option { - Some(self.buffer_size_frames as crate::FrameCount) + fn buffer_size(&self) -> Result { + Ok(self.buffer_size_frames as crate::FrameCount) } } diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 0a2d953e4..599b3c4e3 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -604,7 +604,7 @@ macro_rules! impl_platform_host { } } - fn buffer_size(&self) -> Option { + fn buffer_size(&self) -> Result { match self.0 { $( $(#[cfg($feat)])? diff --git a/src/traits.rs b/src/traits.rs index 9314270df..0e99a9340 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -304,24 +304,23 @@ pub trait StreamTrait { /// fail in these cases. fn pause(&self) -> Result<(), PauseStreamError>; - /// Query the stream's buffer size in frames per callback invocation. + /// Returns the backend's best available estimate of the number of frames per callback. /// - /// Returns the platform's best estimate of the number of frames per callback. + /// The value is available immediately after stream creation: for fixed buffer sizes this is + /// the negotiated hardware size; for default buffer sizes this is the backend's configured + /// default. The value is updated when it changes during the lifetime of the stream. /// - /// - [`crate::BufferSize::Fixed`]: the actual callback size after hardware negotiation, which may - /// differ from the requested value due to hardware constraints. - /// - [`crate::BufferSize::Default`]: the system-configured callback size (e.g. ALSA period, - /// JACK buffer size, AAudio burst size). This reflects the typical callback size, not a - /// guaranteed upper bound. + /// Returns `Err` if the backend cannot retrieve the buffer size. /// - /// Returns `None` if the platform cannot report a meaningful estimate — for example, before - /// the first callback has fired, or on platforms that do not expose this information. + /// # Implementation notes /// - /// Applications should use this value to size pre-allocated buffers or estimate latency, but - /// must always use the actual frame count passed to each individual callback invocation. - fn buffer_size(&self) -> Option { - None - } + /// It is not enforced that each callback delivers exactly this many frames. The actual frame + /// count for each callback is given by its buffer. + /// + /// `buffer_size()` is primarily intended for sizing pre-allocated buffers, but must not be + /// trusted as a guaranteed bound. An incorrect implementation of `buffer_size()` should not + /// lead to memory safety violations. + fn buffer_size(&self) -> Result; /// Returns a [`StreamInstant`] representing the current moment on the stream's clock. ///