From 9fdc6ea8bade5b31b6baafb093edbcb70cb1caf8 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 23 Dec 2025 20:54:21 +0100 Subject: [PATCH 1/9] feat: add Stream::buffer_size() and improve AAudio buffer configuration Add buffer_size() method to Stream trait that returns the number of frames passed to each data callback invocation (actual size or upper limit depending on platform). AAudio improvements: - BufferSize::Default now explicitly configures using optimal burst size from AudioManager, following Android low-latency audio best practices - buffer_size() query falls back to burst size if frames_per_data_callback was not explicitly set - Refactored buffer configuration to eliminate code duplication Addresses #1042 Relates to #964, #942 --- CHANGELOG.md | 6 +++++ src/host/aaudio/mod.rs | 39 ++++++++++++++++++++++++------ src/host/alsa/mod.rs | 3 +++ src/host/asio/mod.rs | 4 +++ src/host/asio/stream.rs | 9 +++++++ src/host/coreaudio/ios/mod.rs | 5 +++- src/host/coreaudio/macos/device.rs | 4 ++- src/host/coreaudio/macos/mod.rs | 8 ++++++ src/host/jack/stream.rs | 4 +++ src/host/wasapi/stream.rs | 17 +++++++++++++ src/platform/mod.rs | 11 +++++++++ src/traits.rs | 22 +++++++++++++++++ 12 files changed, 123 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7cc8e2d1..8eb8a86fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `DeviceBusy` error variant to `SupportedStreamConfigsError`, `DefaultStreamConfigError`, and `BuildStreamError` for retryable device access errors (EBUSY, EAGAIN). - `StreamConfig` now implements `Copy`. +- `StreamTrait::buffer_size` method to query the callback buffer size. - **PulseAudio**: New host for Linux and some BSDs using the PulseAudio API. - **PipeWire**: New host for Linux and some BSDs using the PipeWire API. @@ -25,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **AAudio**: `supported_input_configs` and `supported_output_configs` now return an error for direction-mismatched devices (e.g. querying input configs on an output-only device) instead of silently returning an empty list. +- **AAudio**: Buffer sizes are now dynamically tuned. - **ASIO**: `Device::driver`, `asio_streams`, and `current_callback_flag` are no longer `pub`. ### Fixed @@ -1100,10 +1102,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial commit. +<<<<<<< HEAD [Unreleased]: https://github.com/RustAudio/cpal/compare/v0.17.3...HEAD [0.17.3]: https://github.com/RustAudio/cpal/compare/v0.17.2...v0.17.3 [0.17.2]: https://github.com/RustAudio/cpal/compare/v0.17.1...v0.17.2 [0.17.1]: https://github.com/RustAudio/cpal/compare/v0.17.0...v0.17.1 +======= +[Unreleased]: https://github.com/RustAudio/cpal/compare/v0.17.0...HEAD +>>>>>>> de4500b (feat: add Stream::buffer_size() and improve AAudio buffer configuration) [0.17.0]: https://github.com/RustAudio/cpal/compare/v0.16.0...v0.17.0 [0.16.0]: https://github.com/RustAudio/cpal/compare/v0.15.3...v0.16.0 [0.15.3]: https://github.com/RustAudio/cpal/compare/v0.15.2...v0.15.3 diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index 9d04c2831..98a2fb463 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -277,13 +277,22 @@ fn configure_for_device( }; builder = builder.sample_rate(config.sample_rate.try_into().unwrap()); - // Note: Buffer size validation is not needed - the native AAudio API validates buffer sizes - // when `open_stream()` is called. - match &config.buffer_size { - BufferSize::Default => builder, - BufferSize::Fixed(size) => builder - .frames_per_data_callback(*size as i32) - .buffer_capacity_in_frames((*size * 2) as i32), // Double-buffering + let buffer_size = match config.buffer_size { + BufferSize::Default => { + // Use the optimal burst size from AudioManager: + // https://developer.android.com/ndk/guides/audio/audio-latency#buffer-size + AudioManager::get_frames_per_buffer().ok() + } + BufferSize::Fixed(size) => Some(size), + }; + + if let Some(size) = buffer_size { + builder + .frames_per_data_callback(size as i32) + .buffer_capacity_in_frames((size * 2) as i32) // Double-buffering + } else { + // If we couldn't determine a buffer size, let AAudio choose defaults + builder } } @@ -625,4 +634,20 @@ impl StreamTrait for Stream { .map_err(PauseStreamError::from), } } + + fn buffer_size(&self) -> Option { + let stream = match self { + Self::Input(stream) => stream.lock().ok()?, + Self::Output(stream) => stream.lock().ok()?, + }; + + // If frames_per_data_callback was not explicitly set (returning 0), + // fall back to the burst size as that's what AAudio uses by default. + match stream.get_frames_per_data_callback() { + Some(size) if size > 0 => Some(size as crate::FrameCount), + _ => stream + .get_frames_per_burst() + .map(|f| f as crate::FrameCount), + } + } } diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index 6b55ca184..9b518e3e1 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -1223,6 +1223,9 @@ impl StreamTrait for Stream { self.inner.channel.pause(true).ok(); Ok(()) } + fn buffer_size(&self) -> Option { + Some(self.inner.period_frames as FrameCount) + } } // Convert ALSA frames to FrameCount, clamping to valid range. diff --git a/src/host/asio/mod.rs b/src/host/asio/mod.rs index aea5675b6..4cb4af354 100644 --- a/src/host/asio/mod.rs +++ b/src/host/asio/mod.rs @@ -155,4 +155,8 @@ impl StreamTrait for Stream { fn pause(&self) -> Result<(), PauseStreamError> { Stream::pause(self) } + + fn buffer_size(&self) -> Option { + Stream::buffer_size(self) + } } diff --git a/src/host/asio/stream.rs b/src/host/asio/stream.rs index 226ac375e..bb41254dd 100644 --- a/src/host/asio/stream.rs +++ b/src/host/asio/stream.rs @@ -38,6 +38,15 @@ impl Stream { self.playing.store(false, Ordering::SeqCst); Ok(()) } + + pub fn buffer_size(&self) -> Option { + let streams = self.asio_streams.lock().ok()?; + streams + .output + .as_ref() + .or(streams.input.as_ref()) + .map(|s| s.buffer_size as crate::FrameCount) + } } impl Device { diff --git a/src/host/coreaudio/ios/mod.rs b/src/host/coreaudio/ios/mod.rs index 94de54563..750b5d0d3 100644 --- a/src/host/coreaudio/ios/mod.rs +++ b/src/host/coreaudio/ios/mod.rs @@ -274,11 +274,14 @@ impl StreamTrait for Stream { let err = BackendSpecificError { description }; return Err(err.into()); } - stream.playing = false; } Ok(()) } + + fn buffer_size(&self) -> Option { + Some(get_device_buffer_frames() as crate::FrameCount) + } } struct StreamInner { diff --git a/src/host/coreaudio/macos/device.rs b/src/host/coreaudio/macos/device.rs index 1cae99a65..1b9745a0b 100644 --- a/src/host/coreaudio/macos/device.rs +++ b/src/host/coreaudio/macos/device.rs @@ -1007,7 +1007,9 @@ fn setup_callback_vars( /// /// Buffer frame size is a device-level property that always uses Scope::Global + Element::Output, /// regardless of whether the audio unit is configured for input or output streams. -fn get_device_buffer_frame_size(audio_unit: &AudioUnit) -> Result { +pub(crate) fn get_device_buffer_frame_size( + audio_unit: &AudioUnit, +) -> Result { // Device-level property: always use Scope::Global + Element::Output // This is consistent with how we set the buffer size and query the buffer size range let frames: u32 = audio_unit.get_property( diff --git a/src/host/coreaudio/macos/mod.rs b/src/host/coreaudio/macos/mod.rs index 6acbd4cc8..d94525037 100644 --- a/src/host/coreaudio/macos/mod.rs +++ b/src/host/coreaudio/macos/mod.rs @@ -262,6 +262,14 @@ impl StreamTrait for Stream { stream.pause() } + + fn buffer_size(&self) -> Option { + let stream = self.inner.lock().ok()?; + + device::get_device_buffer_frame_size(&stream.audio_unit) + .ok() + .map(|size| size as crate::FrameCount) + } } #[cfg(test)] diff --git a/src/host/jack/stream.rs b/src/host/jack/stream.rs index 6563a64d0..7e54c9f45 100644 --- a/src/host/jack/stream.rs +++ b/src/host/jack/stream.rs @@ -220,6 +220,10 @@ impl StreamTrait for Stream { self.playing.store(false, Ordering::SeqCst); Ok(()) } + + fn buffer_size(&self) -> Option { + Some(self.async_client.as_client().buffer_size() as crate::FrameCount) + } } type InputDataCallback = Box; diff --git a/src/host/wasapi/stream.rs b/src/host/wasapi/stream.rs index 330b6d12b..11dcd81e2 100644 --- a/src/host/wasapi/stream.rs +++ b/src/host/wasapi/stream.rs @@ -29,6 +29,11 @@ pub struct Stream { // This event is signalled after a new entry is added to `commands`, so that the `run()` // method can be notified. pending_scheduled_event: Foundation::HANDLE, + + // Number of frames in the WASAPI buffer. + // + // Note: the actual callback size is variable and may be less than this value. + max_frames_in_buffer: u32, } // SAFETY: Windows Event HANDLEs are safe to send between threads - they are designed for @@ -115,6 +120,8 @@ impl Stream { .expect("cpal: could not create input stream event"); let (tx, rx) = channel(); + let max_frames_in_buffer = stream_inner.max_frames_in_buffer; + let run_context = RunContext { handles: vec![pending_scheduled_event, stream_inner.event], stream: stream_inner, @@ -130,6 +137,7 @@ impl Stream { thread: Some(thread), commands: tx, pending_scheduled_event, + max_frames_in_buffer, } } @@ -148,6 +156,8 @@ impl Stream { .expect("cpal: could not create output stream event"); let (tx, rx) = channel(); + let max_frames_in_buffer = stream_inner.max_frames_in_buffer; + let run_context = RunContext { handles: vec![pending_scheduled_event, stream_inner.event], stream: stream_inner, @@ -163,6 +173,7 @@ impl Stream { thread: Some(thread), commands: tx, pending_scheduled_event, + max_frames_in_buffer, } } @@ -200,6 +211,12 @@ impl StreamTrait for Stream { .map_err(|_| crate::error::PauseStreamError::DeviceNotAvailable)?; Ok(()) } + + fn buffer_size(&self) -> Option { + // WASAPI uses event-driven callbacks with variable callback sizes. + // We return the total buffer size allocated by Windows as an upper bound. + Some(self.max_frames_in_buffer as crate::FrameCount) + } } impl Drop for StreamInner { diff --git a/src/platform/mod.rs b/src/platform/mod.rs index f738fdd0e..0e2de0eca 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -592,6 +592,17 @@ macro_rules! impl_platform_host { )* } } + + fn buffer_size(&self) -> Option { + match self.0 { + $( + $(#[cfg($feat)])? + StreamInner::$HostVariant(ref s) => { + s.buffer_size() + } + )* + } + } } impl From for Device { diff --git a/src/traits.rs b/src/traits.rs index 446dd13f5..1c353145b 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -302,6 +302,28 @@ pub trait StreamTrait { /// Note: Not all devices support suspending the stream at the hardware level. This method may /// fail in these cases. fn pause(&self) -> Result<(), PauseStreamError>; + + /// Query the stream's callback buffer size in frames. + /// + /// Returns the actual buffer size chosen by the platform, which may differ from a requested + /// `BufferSize::Fixed` value due to hardware constraints, or is determined by the platform + /// when using `BufferSize::Default`. + /// + /// # Returns + /// + /// Returns `Some(frames)` if the callback buffer size is known, or `None` if: + /// - The platform doesn't support querying buffer size at runtime + /// - The stream hasn't been fully initialized yet + /// + /// # Note on Variable Callback Sizes + /// + /// Some platforms (notably WASAPI and mobile) may deliver variable-sized buffers to callbacks + /// that are smaller than the reported buffer size. When `buffer_size()` returns a value, it + /// should be treated as the maximum expected size, but applications should always check the + /// actual buffer size passed to each callback. + fn buffer_size(&self) -> Option { + None + } } /// Compile-time assertion that a stream type implements [`Send`]. From 36a1b24d5fed3d4431e3d453fa9f88810cdd075b Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 23 Dec 2025 21:05:24 +0100 Subject: [PATCH 2/9] refactor(aaudio): simplify Stream from enum to struct Replace `Stream` enum with a struct containing `inner: Arc>` and `direction: DeviceDirection` fields. This eliminates code duplication while maintaining the same functionality. --- src/host/aaudio/mod.rs | 64 ++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index 98a2fb463..2a0cd147b 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -131,9 +131,9 @@ pub struct Device(Option); /// - The pointer in AudioStream (NonNull) is valid for the lifetime /// of the stream and AAudio C API functions are thread-safe at the C level #[derive(Clone)] -pub enum Stream { - Input(Arc>), - Output(Arc>), +pub struct Stream { + inner: Arc>, + direction: DeviceDirection, } // SAFETY: AudioStream can be safely sent between threads. The AAudio C API is thread-safe @@ -281,7 +281,7 @@ fn configure_for_device( BufferSize::Default => { // Use the optimal burst size from AudioManager: // https://developer.android.com/ndk/guides/audio/audio-latency#buffer-size - AudioManager::get_frames_per_buffer().ok() + AudioManager::get_frames_per_buffer().ok().map(|s| s as u32) } BufferSize::Fixed(size) => Some(size), }; @@ -339,7 +339,10 @@ where // is safe because the Mutex provides exclusive access and AudioStream's thread safety // is documented in the AAudio C API. #[allow(clippy::arc_with_non_send_sync)] - Ok(Stream::Input(Arc::new(Mutex::new(stream)))) + Ok(Stream { + inner: Arc::new(Mutex::new(stream)), + direction: DeviceDirection::Input, + }) } fn build_output_stream( @@ -385,7 +388,10 @@ where // is safe because the Mutex provides exclusive access and AudioStream's thread safety // is documented in the AAudio C API. #[allow(clippy::arc_with_non_send_sync)] - Ok(Stream::Output(Arc::new(Mutex::new(stream)))) + Ok(Stream { + inner: Arc::new(Mutex::new(stream)), + direction: DeviceDirection::Output, + }) } impl DeviceTrait for Device { @@ -607,47 +613,37 @@ impl DeviceTrait for Device { impl StreamTrait for Stream { fn play(&self) -> Result<(), PlayStreamError> { - match self { - Self::Input(stream) => stream - .lock() - .unwrap() - .request_start() - .map_err(PlayStreamError::from), - Self::Output(stream) => stream - .lock() - .unwrap() - .request_start() - .map_err(PlayStreamError::from), - } + self.inner + .lock() + .unwrap() + .request_start() + .map_err(PlayStreamError::from) } fn pause(&self) -> Result<(), PauseStreamError> { - match self { - Self::Input(_) => Err(BackendSpecificError { - description: "Pause called on the input stream.".to_owned(), - } - .into()), - Self::Output(stream) => stream + match self.direction { + DeviceDirection::Output => self + .inner .lock() .unwrap() .request_pause() .map_err(PauseStreamError::from), + _ => Err(BackendSpecificError { + description: "Pause only supported on output streams.".to_owned(), + } + .into()), } } fn buffer_size(&self) -> Option { - let stream = match self { - Self::Input(stream) => stream.lock().ok()?, - Self::Output(stream) => stream.lock().ok()?, - }; + let stream = self.inner.lock().ok()?; // If frames_per_data_callback was not explicitly set (returning 0), // fall back to the burst size as that's what AAudio uses by default. - match stream.get_frames_per_data_callback() { - Some(size) if size > 0 => Some(size as crate::FrameCount), - _ => stream - .get_frames_per_burst() - .map(|f| f as crate::FrameCount), - } + let frames = match stream.frames_per_data_callback() { + Some(size) if size > 0 => size, + _ => stream.frames_per_burst(), + }; + Some(frames as crate::FrameCount) } } From e221a7c68722c07fbd44f5ad6e467f21b426cdb0 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 28 Dec 2025 23:32:55 +0100 Subject: [PATCH 3/9] feat: always set AAudio buffer size with 256 fallback --- src/host/aaudio/mod.rs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index 2a0cd147b..40322435c 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -277,23 +277,21 @@ fn configure_for_device( }; builder = builder.sample_rate(config.sample_rate.try_into().unwrap()); - let buffer_size = match config.buffer_size { + let size = match config.buffer_size { BufferSize::Default => { // Use the optimal burst size from AudioManager: // https://developer.android.com/ndk/guides/audio/audio-latency#buffer-size - AudioManager::get_frames_per_buffer().ok().map(|s| s as u32) + match AudioManager::get_frames_per_buffer() { + Ok(size) if size > 0 => size as u32, + _ => 256, + } } - BufferSize::Fixed(size) => Some(size), + BufferSize::Fixed(size) => size, }; - if let Some(size) = buffer_size { - builder - .frames_per_data_callback(size as i32) - .buffer_capacity_in_frames((size * 2) as i32) // Double-buffering - } else { - // If we couldn't determine a buffer size, let AAudio choose defaults - builder - } + builder + .frames_per_data_callback(size as i32) + .buffer_capacity_in_frames((size * 2) as i32) // Double-buffering } fn build_input_stream( From ae745ed3d06c9e96e3abdb93c288ca1d064ec96c Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 2 Jan 2026 17:27:39 +0100 Subject: [PATCH 4/9] fix(aaudio): use system mixer bursts for buffer capacity --- .../aaudio/java_interface/audio_manager.rs | 23 ++++++++++- src/host/aaudio/java_interface/utils.rs | 20 ++++++++++ src/host/aaudio/mod.rs | 40 +++++++++++++------ 3 files changed, 69 insertions(+), 14 deletions(-) diff --git a/src/host/aaudio/java_interface/audio_manager.rs b/src/host/aaudio/java_interface/audio_manager.rs index f96d8e2cf..cd71eb150 100644 --- a/src/host/aaudio/java_interface/audio_manager.rs +++ b/src/host/aaudio/java_interface/audio_manager.rs @@ -1,6 +1,7 @@ use super::{ utils::{ - get_context, get_property, get_system_service, with_attached, JNIEnv, JObject, JResult, + get_context, get_property, get_system_property, get_system_service, with_attached, JNIEnv, + JObject, JResult, }, AudioManager, Context, }; @@ -13,6 +14,15 @@ impl AudioManager { with_attached(context, |env, context| get_frames_per_buffer(env, &context)) .map_err(|error| error.to_string()) } + + /// Get the AAudio mixer burst count from system property + /// Returns the value from aaudio.mixer_bursts property, defaulting to 2 + pub fn get_mixer_bursts() -> Result { + let context = get_context(); + + with_attached(context, |env, _context| get_mixer_bursts(env)) + .map_err(|error| error.to_string()) + } } fn get_frames_per_buffer<'j>(env: &mut JNIEnv<'j>, context: &JObject<'j>) -> JResult { @@ -31,3 +41,14 @@ fn get_frames_per_buffer<'j>(env: &mut JNIEnv<'j>, context: &JObject<'j>) -> JRe .parse::() .map_err(|_| jni::errors::Error::JniCall(jni::errors::JniError::Unknown)) } + +fn get_mixer_bursts<'j>(env: &mut JNIEnv<'j>) -> JResult { + let mixer_bursts = get_system_property(env, "aaudio.mixer_bursts", "2")?; + + let mixer_bursts_string = String::from(env.get_string(&mixer_bursts)?); + + // TODO: Use jni::errors::Error::ParseFailed instead of jni::errors::Error::JniCall once jni > v0.21.1 is released + mixer_bursts_string + .parse::() + .map_err(|_| jni::errors::Error::JniCall(jni::errors::JniError::Unknown)) +} diff --git a/src/host/aaudio/java_interface/utils.rs b/src/host/aaudio/java_interface/utils.rs index 5671ee6ae..092b0f5c4 100644 --- a/src/host/aaudio/java_interface/utils.rs +++ b/src/host/aaudio/java_interface/utils.rs @@ -165,6 +165,26 @@ pub fn get_property<'j>( call_method_string_arg_ret_string(env, subject, "getProperty", name) } +/// Read an Android system property +pub fn get_system_property<'j>( + env: &mut JNIEnv<'j>, + name: &str, + default_value: &str, +) -> JResult> { + Ok(env + .call_static_method( + "android/os/SystemProperties", + "get", + "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", + &[ + (&env.new_string(name)?).into(), + (&env.new_string(default_value)?).into(), + ], + )? + .l()? + .into()) +} + pub fn get_devices<'j>( env: &mut JNIEnv<'j>, subject: &JObject<'j>, diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index 40322435c..130e321ec 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -277,21 +277,35 @@ fn configure_for_device( }; builder = builder.sample_rate(config.sample_rate.try_into().unwrap()); - let size = match config.buffer_size { + match config.buffer_size { BufferSize::Default => { - // Use the optimal burst size from AudioManager: - // https://developer.android.com/ndk/guides/audio/audio-latency#buffer-size - match AudioManager::get_frames_per_buffer() { - Ok(size) if size > 0 => size as u32, - _ => 256, - } - } - BufferSize::Fixed(size) => size, - }; + // Following the pattern from Oboe and Google's AAudio samples, we only set the buffer + // capacity and let AAudio choose the optimal callback size dynamically. See: + // - https://developer.android.com/ndk/reference/group/audio + // - https://developer.android.com/ndk/guides/audio/audio-latency#buffer-size + let burst = match AudioManager::get_frames_per_buffer() { + Ok(size) if size > 0 => size, + _ => 256, // default from Android docs + }; - builder - .frames_per_data_callback(size as i32) - .buffer_capacity_in_frames((size * 2) as i32) // Double-buffering + // Determine the buffer capacity multiplier. This matches AOSP's + // AAudioServiceEndpointPlay buffer sizing strategy. + let mixer_bursts = match AudioManager::get_mixer_bursts() { + Ok(bursts) if bursts > 1 => bursts, + _ => 2, // double-buffering: default from AOSP + }; + + let capacity = burst * mixer_bursts; + builder.buffer_capacity_in_frames(capacity) + } + BufferSize::Fixed(size) => { + // For fixed sizes, the user explicitly wants control over the callback size, + // so we set both the callback size and capacity (with double-buffering). + builder + .frames_per_data_callback(size as i32) + .buffer_capacity_in_frames((size * 2) as i32) + } + } } fn build_input_stream( From ac519c6f6a0399289e5b2c654ba009070d1c57f5 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 2 Jan 2026 23:59:55 +0100 Subject: [PATCH 5/9] feat(aaudio): tune buffers dynamically --- CHANGELOG.md | 4 - .../aaudio/java_interface/audio_manager.rs | 1 - src/host/aaudio/mod.rs | 103 +++++++++++++----- 3 files changed, 75 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8eb8a86fb..a12a5505d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1102,14 +1102,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial commit. -<<<<<<< HEAD [Unreleased]: https://github.com/RustAudio/cpal/compare/v0.17.3...HEAD [0.17.3]: https://github.com/RustAudio/cpal/compare/v0.17.2...v0.17.3 [0.17.2]: https://github.com/RustAudio/cpal/compare/v0.17.1...v0.17.2 [0.17.1]: https://github.com/RustAudio/cpal/compare/v0.17.0...v0.17.1 -======= -[Unreleased]: https://github.com/RustAudio/cpal/compare/v0.17.0...HEAD ->>>>>>> de4500b (feat: add Stream::buffer_size() and improve AAudio buffer configuration) [0.17.0]: https://github.com/RustAudio/cpal/compare/v0.16.0...v0.17.0 [0.16.0]: https://github.com/RustAudio/cpal/compare/v0.15.3...v0.16.0 [0.15.3]: https://github.com/RustAudio/cpal/compare/v0.15.2...v0.15.3 diff --git a/src/host/aaudio/java_interface/audio_manager.rs b/src/host/aaudio/java_interface/audio_manager.rs index cd71eb150..958921506 100644 --- a/src/host/aaudio/java_interface/audio_manager.rs +++ b/src/host/aaudio/java_interface/audio_manager.rs @@ -16,7 +16,6 @@ impl AudioManager { } /// Get the AAudio mixer burst count from system property - /// Returns the value from aaudio.mixer_bursts property, defaulting to 2 pub fn get_mixer_bursts() -> Result { let context = get_context(); diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index 130e321ec..7d0593689 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -4,6 +4,7 @@ use std::cmp; use std::convert::TryInto; +use std::sync::atomic::{AtomicI32, Ordering}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use std::vec::IntoIter as VecIntoIter; @@ -148,6 +149,14 @@ unsafe impl Sync for Stream {} crate::assert_stream_send!(Stream); crate::assert_stream_sync!(Stream); +/// State for dynamic buffer tuning on output streams. +#[derive(Default)] +struct BufferTuningState { + previous_underrun_count: AtomicI32, + capacity: AtomicI32, + mixer_bursts: AtomicI32, +} + pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; pub type Devices = std::vec::IntoIter; @@ -277,35 +286,16 @@ fn configure_for_device( }; builder = builder.sample_rate(config.sample_rate.try_into().unwrap()); - match config.buffer_size { - BufferSize::Default => { - // Following the pattern from Oboe and Google's AAudio samples, we only set the buffer - // capacity and let AAudio choose the optimal callback size dynamically. See: - // - https://developer.android.com/ndk/reference/group/audio - // - https://developer.android.com/ndk/guides/audio/audio-latency#buffer-size - let burst = match AudioManager::get_frames_per_buffer() { - Ok(size) if size > 0 => size, - _ => 256, // default from Android docs - }; - - // Determine the buffer capacity multiplier. This matches AOSP's - // AAudioServiceEndpointPlay buffer sizing strategy. - let mixer_bursts = match AudioManager::get_mixer_bursts() { - Ok(bursts) if bursts > 1 => bursts, - _ => 2, // double-buffering: default from AOSP - }; - - let capacity = burst * mixer_bursts; - builder.buffer_capacity_in_frames(capacity) - } - BufferSize::Fixed(size) => { - // For fixed sizes, the user explicitly wants control over the callback size, - // so we set both the callback size and capacity (with double-buffering). - builder - .frames_per_data_callback(size as i32) - .buffer_capacity_in_frames((size * 2) as i32) - } + // Following the pattern from Oboe and Google's AAudio, we let AAudio choose the optimal + // callback size dynamically by default. See + // - https://developer.android.com/ndk/reference/group/audio#aaudiostreambuilder_setframesperdatacallback + // - https://developer.android.com/ndk/guides/audio/audio-latency#buffer-size + if let BufferSize::Fixed(size) = config.buffer_size { + // Only for fixed sizes, the user explicitly wants control over the callback size. + builder = builder.frames_per_data_callback(size as i32); } + + builder } fn build_input_stream( @@ -347,6 +337,7 @@ where (error_callback)(StreamError::from(error)) })) .open_stream()?; + // SAFETY: Stream implements Send + Sync (see unsafe impl below). Arc> // is safe because the Mutex provides exclusive access and AudioStream's thread safety // is documented in the AAudio C API. @@ -372,8 +363,13 @@ where let builder = configure_for_device(builder, device, config); let created = Instant::now(); let channel_count = config.channels as i32; + + let tuning = Arc::new(BufferTuningState::default()); + let tuning_for_callback = tuning.clone(); + let stream = builder .data_callback(Box::new(move |stream, data, num_frames| { + // Deliver audio data to user callback let cb_info = OutputCallbackInfo { timestamp: OutputStreamTimestamp { callback: to_stream_instant(created.elapsed()), @@ -390,12 +386,63 @@ where }, &cb_info, ); + + // Dynamic buffer tuning for output streams + // See: https://developer.android.com/ndk/guides/audio/aaudio/aaudio#tuning-buffers + let underrun_count = stream.x_run_count(); + let previous = tuning_for_callback + .previous_underrun_count + .load(Ordering::Relaxed); + + if underrun_count > previous { + // The number of frames per burst can vary dynamically + let mut burst_size = stream.frames_per_burst(); + if burst_size <= 0 { + burst_size = 256; // fallback from AAudio documentation + } else if burst_size < 16 { + burst_size = 16; // floor from Oboe + } + + let mixer_bursts = tuning_for_callback + .mixer_bursts + .fetch_add(1, Ordering::Relaxed); + let mut buffer_size = burst_size * mixer_bursts; + + let buffer_capacity = tuning_for_callback.capacity.load(Ordering::Relaxed); + if buffer_size > buffer_capacity { + buffer_size = buffer_capacity; + } + let _ = stream.set_buffer_size_in_frames(buffer_size); + + tuning_for_callback + .previous_underrun_count + .store(underrun_count, Ordering::Relaxed); + } + ndk::audio::AudioCallbackResult::Continue })) .error_callback(Box::new(move |_stream, error| { (error_callback)(StreamError::from(error)) })) .open_stream()?; + + // After stream opens, query and cache the values + let capacity = stream.buffer_capacity_in_frames(); + tuning.capacity.store(capacity, Ordering::Relaxed); + + let mixer_bursts = match AudioManager::get_mixer_bursts() { + Ok(bursts) => bursts, + Err(_) => { + let burst_size = stream.frames_per_burst(); + if burst_size > 0 { + stream.buffer_size_in_frames() / burst_size + } else { + 0 // defer to dynamic tuning + } + } + }; + tuning.mixer_bursts.store(mixer_bursts, Ordering::Relaxed); + // SAFETY: Stream implements Send + Sync (see unsafe impl below). Arc> // is safe because the Mutex provides exclusive access and AudioStream's thread safety // is documented in the AAudio C API. From dc55eb27cd7e94a55becba1911778d6f21d84845 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 3 Jan 2026 11:51:52 +0100 Subject: [PATCH 6/9] refactor(aaudio): update mixer_bursts only on successful set --- src/host/aaudio/mod.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index 7d0593689..db4c39785 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -403,16 +403,22 @@ where burst_size = 16; // floor from Oboe } - let mixer_bursts = tuning_for_callback + let new_mixer_bursts = tuning_for_callback .mixer_bursts - .fetch_add(1, Ordering::Relaxed); - let mut buffer_size = burst_size * mixer_bursts; + .load(Ordering::Relaxed) + .saturating_add(1); + let mut buffer_size = burst_size * new_mixer_bursts; let buffer_capacity = tuning_for_callback.capacity.load(Ordering::Relaxed); if buffer_size > buffer_capacity { buffer_size = buffer_capacity; } - let _ = stream.set_buffer_size_in_frames(buffer_size); + + if stream.set_buffer_size_in_frames(buffer_size).is_ok() { + tuning_for_callback + .mixer_bursts + .store(new_mixer_bursts, Ordering::Relaxed); + } tuning_for_callback .previous_underrun_count From 22d887898668b09a3b68ae0e781b232cec8eeed3 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 8 Mar 2026 23:05:53 +0100 Subject: [PATCH 7/9] fix(aaudio): skip dynamic buffer tuning when BufferSize::Fixed is requested --- CHANGELOG.md | 2 +- src/host/aaudio/mod.rs | 61 ++++++++++++++++++++++-------------------- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a12a5505d..a9d35e616 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **AAudio**: `supported_input_configs` and `supported_output_configs` now return an error for direction-mismatched devices (e.g. querying input configs on an output-only device) instead of silently returning an empty list. -- **AAudio**: Buffer sizes are now dynamically tuned. +- **AAudio**: Buffers with default sizes are now dynamically tuned. - **ASIO**: `Device::driver`, `asio_streams`, and `current_callback_flag` are no longer `pub`. ### Fixed diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index db4c39785..56572d311 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -363,6 +363,7 @@ where let builder = configure_for_device(builder, device, config); let created = Instant::now(); let channel_count = config.channels as i32; + let tune_dynamically = config.buffer_size == BufferSize::Default; let tuning = Arc::new(BufferTuningState::default()); let tuning_for_callback = tuning.clone(); @@ -389,40 +390,42 @@ where // Dynamic buffer tuning for output streams // See: https://developer.android.com/ndk/guides/audio/aaudio/aaudio#tuning-buffers - let underrun_count = stream.x_run_count(); - let previous = tuning_for_callback - .previous_underrun_count - .load(Ordering::Relaxed); - - if underrun_count > previous { - // The number of frames per burst can vary dynamically - let mut burst_size = stream.frames_per_burst(); - if burst_size <= 0 { - burst_size = 256; // fallback from AAudio documentation - } else if burst_size < 16 { - burst_size = 16; // floor from Oboe - } + if tune_dynamically { + let underrun_count = stream.x_run_count(); + let previous = tuning_for_callback + .previous_underrun_count + .load(Ordering::Relaxed); + + if underrun_count > previous { + // The number of frames per burst can vary dynamically + let mut burst_size = stream.frames_per_burst(); + if burst_size <= 0 { + burst_size = 256; // fallback from AAudio documentation + } else if burst_size < 16 { + burst_size = 16; // floor from Oboe + } + + let new_mixer_bursts = tuning_for_callback + .mixer_bursts + .load(Ordering::Relaxed) + .saturating_add(1); + let mut buffer_size = burst_size * new_mixer_bursts; - let new_mixer_bursts = tuning_for_callback - .mixer_bursts - .load(Ordering::Relaxed) - .saturating_add(1); - let mut buffer_size = burst_size * new_mixer_bursts; + let buffer_capacity = tuning_for_callback.capacity.load(Ordering::Relaxed); + if buffer_size > buffer_capacity { + buffer_size = buffer_capacity; + } - let buffer_capacity = tuning_for_callback.capacity.load(Ordering::Relaxed); - if buffer_size > buffer_capacity { - buffer_size = buffer_capacity; - } + if stream.set_buffer_size_in_frames(buffer_size).is_ok() { + tuning_for_callback + .mixer_bursts + .store(new_mixer_bursts, Ordering::Relaxed); + } - if stream.set_buffer_size_in_frames(buffer_size).is_ok() { tuning_for_callback - .mixer_bursts - .store(new_mixer_bursts, Ordering::Relaxed); + .previous_underrun_count + .store(underrun_count, Ordering::Relaxed); } - - tuning_for_callback - .previous_underrun_count - .store(underrun_count, Ordering::Relaxed); } ndk::audio::AudioCallbackResult::Continue From b40eb0a235a9a281a56f36698c976df452174a2b Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 8 Mar 2026 23:24:57 +0100 Subject: [PATCH 8/9] fix(pulseaudio): report correct buffer size limits in supported configs --- src/host/pulseaudio/mod.rs | 110 ++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 64 deletions(-) diff --git a/src/host/pulseaudio/mod.rs b/src/host/pulseaudio/mod.rs index 5c05681a2..dfdedc2f8 100644 --- a/src/host/pulseaudio/mod.rs +++ b/src/host/pulseaudio/mod.rs @@ -152,6 +152,48 @@ pub enum Device { }, } +fn supported_config_ranges() -> Vec { + let mut ranges = vec![]; + for format in PULSE_FORMATS { + for channel_count in 1..protocol::sample_spec::MAX_CHANNELS { + let bytes_per_frame = channel_count as usize * format.sample_size(); + let max_frames = (protocol::MAX_MEMBLOCKQ_LENGTH / bytes_per_frame) as u32; + ranges.push(SupportedStreamConfigRange { + channels: channel_count as _, + min_sample_rate: 1, + max_sample_rate: protocol::sample_spec::MAX_RATE, + buffer_size: SupportedBufferSize::Range { + min: 0, + max: max_frames, + }, + sample_format: *format, + }); + } + } + ranges +} + +fn default_config_from_spec( + sample_spec: &protocol::SampleSpec, + channel_map: &protocol::ChannelMap, +) -> Result { + let sample_format: SampleFormat = sample_spec + .format + .try_into() + .map_err(|_| DefaultStreamConfigError::StreamTypeNotSupported)?; + let bytes_per_frame = channel_map.num_channels() as usize * sample_format.sample_size(); + let max_frames = (protocol::MAX_MEMBLOCKQ_LENGTH / bytes_per_frame) as u32; + Ok(SupportedStreamConfig { + channels: channel_map.num_channels() as _, + sample_rate: sample_spec.sample_rate, + buffer_size: SupportedBufferSize::Range { + min: 0, + max: max_frames, + }, + sample_format, + }) +} + impl DeviceTrait for Device { type SupportedInputConfigs = std::vec::IntoIter; type SupportedOutputConfigs = std::vec::IntoIter; @@ -172,24 +214,7 @@ impl DeviceTrait for Device { let Device::Source { .. } = self else { return Ok(vec![].into_iter()); }; - - let mut ranges = vec![]; - for format in PULSE_FORMATS { - for channel_count in 1..protocol::sample_spec::MAX_CHANNELS { - ranges.push(SupportedStreamConfigRange { - channels: channel_count as _, - min_sample_rate: 1, - max_sample_rate: protocol::sample_spec::MAX_RATE, - buffer_size: SupportedBufferSize::Range { - min: 0, - max: protocol::MAX_MEMBLOCKQ_LENGTH as _, - }, - sample_format: *format, - }) - } - } - - Ok(ranges.into_iter()) + Ok(supported_config_ranges().into_iter()) } fn supported_output_configs( @@ -198,64 +223,21 @@ impl DeviceTrait for Device { let Device::Sink { .. } = self else { return Ok(vec![].into_iter()); }; - - let mut ranges = vec![]; - for format in PULSE_FORMATS { - for channel_count in 1..protocol::sample_spec::MAX_CHANNELS { - ranges.push(SupportedStreamConfigRange { - channels: channel_count as _, - min_sample_rate: 1, - max_sample_rate: protocol::sample_spec::MAX_RATE, - buffer_size: SupportedBufferSize::Range { - min: 0, - max: protocol::MAX_MEMBLOCKQ_LENGTH as _, - }, - sample_format: *format, - }) - } - } - - Ok(ranges.into_iter()) + Ok(supported_config_ranges().into_iter()) } fn default_input_config(&self) -> Result { let Device::Source { info, .. } = self else { return Err(DefaultStreamConfigError::StreamTypeNotSupported); }; - - Ok(SupportedStreamConfig { - channels: info.channel_map.num_channels() as _, - sample_rate: info.sample_spec.sample_rate, - buffer_size: SupportedBufferSize::Range { - min: 0, - max: protocol::MAX_MEMBLOCKQ_LENGTH as _, - }, - sample_format: info - .sample_spec - .format - .try_into() - .unwrap_or(SampleFormat::F32), - }) + default_config_from_spec(&info.sample_spec, &info.channel_map) } fn default_output_config(&self) -> Result { let Device::Sink { info, .. } = self else { return Err(DefaultStreamConfigError::StreamTypeNotSupported); }; - - Ok(SupportedStreamConfig { - channels: info.channel_map.num_channels() as _, - sample_rate: info.sample_spec.sample_rate, - buffer_size: SupportedBufferSize::Range { - min: 0, - max: protocol::MAX_MEMBLOCKQ_LENGTH as _, - }, - sample_format: info - .sample_spec - .format - .try_into() - .unwrap_or(SampleFormat::F32), - }) + default_config_from_spec(&info.sample_spec, &info.channel_map) } fn build_input_stream_raw( From 19fbfdb918caea127f3a2d43633eec7eac62cf14 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 9 Mar 2026 00:05:15 +0100 Subject: [PATCH 9/9] feat(pipewire,pulseaudio): implement StreamTrait::buffer_size() --- src/host/pipewire/device.rs | 9 ++++++ src/host/pipewire/stream.rs | 55 +++++++++++++++++++++++++++++------ src/host/pulseaudio/stream.rs | 15 ++++++++++ 3 files changed, 70 insertions(+), 9 deletions(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index c81d28566..1bc992322 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -1,3 +1,4 @@ +use std::sync::{atomic::AtomicU64, Arc}; use std::time::Duration; use std::{cell::RefCell, rc::Rc}; @@ -297,6 +298,8 @@ 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 last_quantum_clone = last_quantum.clone(); let handle = thread::Builder::new() .name("pw_in".to_owned()) .spawn(move || { @@ -312,6 +315,7 @@ impl DeviceTrait for Device { sample_format, data_callback, error_callback, + last_quantum_clone, ) else { let _ = pw_init_tx.send(false); @@ -342,6 +346,7 @@ impl DeviceTrait for Device { Ok(true) => Ok(Stream { handle: Some(handle), controller: pw_play_tx, + last_quantum, }), Ok(false) => Err(crate::BuildStreamError::StreamConfigNotSupported), Err(_) => Err(crate::BuildStreamError::BackendSpecific { @@ -369,6 +374,8 @@ 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 last_quantum_clone = last_quantum.clone(); let handle = thread::Builder::new() .name("pw_out".to_owned()) .spawn(move || { @@ -385,6 +392,7 @@ impl DeviceTrait for Device { sample_format, data_callback, error_callback, + last_quantum_clone, ) else { let _ = pw_init_tx.send(false); @@ -416,6 +424,7 @@ impl DeviceTrait for Device { Ok(true) => Ok(Stream { handle: Some(handle), controller: pw_play_tx, + last_quantum, }), Ok(false) => Err(crate::BuildStreamError::StreamConfigNotSupported), Err(_) => Err(crate::BuildStreamError::BackendSpecific { diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index 4c46e856b..308befbee 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -1,4 +1,11 @@ -use std::{thread::JoinHandle, time::Instant}; +use std::{ + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, + }, + thread::JoinHandle, + time::Instant, +}; use crate::{ host::fill_with_equilibrium, traits::StreamTrait, BackendSpecificError, InputCallbackInfo, @@ -29,6 +36,7 @@ pub enum StreamCommand { pub struct Stream { pub(crate) handle: Option>, pub(crate) controller: pw::channel::Sender, + pub(crate) last_quantum: Arc, } impl Drop for Stream { @@ -59,6 +67,13 @@ impl StreamTrait for Stream { })?; Ok(()) } + + fn buffer_size(&self) -> Option { + match self.last_quantum.load(Ordering::Relaxed) { + 0 => None, + n => Some(n as _), + } + } } pub(crate) const SUPPORTED_FORMATS: &[SampleFormat] = &[ @@ -128,6 +143,7 @@ pub struct UserData { sample_format: SampleFormat, format: pw::spa::param::audio::AudioInfoRaw, created_instance: Instant, + last_quantum: Arc, } impl UserData where @@ -157,6 +173,8 @@ struct PwTime { /// For output: how far ahead of the driver our next sample will be played. /// For input: how long ago the data in the buffer was captured. delay_ns: i64, + /// Quantum size in samples (frames) for this graph cycle. + quantum: u64, } /// Returns a hardware timestamp for the current graph cycle, or `None` if @@ -178,6 +196,7 @@ fn pw_stream_time(stream: &pw::stream::Stream) -> Option { Some(PwTime { now_ns: t.now, delay_ns, + quantum: t.size, }) } @@ -193,10 +212,17 @@ where data: &Data, ) -> Result<(), BackendSpecificError> { let (callback, capture) = match pw_stream_time(stream) { - Some(PwTime { now_ns, delay_ns }) => ( - StreamInstant::from_nanos(now_ns), - StreamInstant::from_nanos(now_ns - delay_ns), - ), + Some(PwTime { + now_ns, + delay_ns, + quantum, + }) => { + self.last_quantum.store(quantum, Ordering::Relaxed); + ( + StreamInstant::from_nanos(now_ns), + StreamInstant::from_nanos(now_ns - delay_ns), + ) + } None => { let cb = stream_timestamp_fallback(self.created_instance)?; let pl = cb @@ -227,10 +253,17 @@ where data: &mut Data, ) -> Result<(), BackendSpecificError> { let (callback, playback) = match pw_stream_time(stream) { - Some(PwTime { now_ns, delay_ns }) => ( - StreamInstant::from_nanos(now_ns), - StreamInstant::from_nanos(now_ns + delay_ns), - ), + Some(PwTime { + now_ns, + delay_ns, + quantum, + }) => { + self.last_quantum.store(quantum, Ordering::Relaxed); + ( + StreamInstant::from_nanos(now_ns), + StreamInstant::from_nanos(now_ns + delay_ns), + ) + } None => { let cb = stream_timestamp_fallback(self.created_instance)?; let pl = cb @@ -285,6 +318,7 @@ pub fn connect_output( sample_format: SampleFormat, data_callback: D, error_callback: E, + last_quantum: Arc, ) -> Result, pw::Error> where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, @@ -301,6 +335,7 @@ where sample_format, format: Default::default(), created_instance: Instant::now(), + last_quantum, }; let channels = config.channels as _; let rate = config.sample_rate as _; @@ -435,6 +470,7 @@ pub fn connect_input( sample_format: SampleFormat, data_callback: D, error_callback: E, + last_quantum: Arc, ) -> Result, pw::Error> where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, @@ -451,6 +487,7 @@ where sample_format, format: Default::default(), created_instance: Instant::now(), + last_quantum, }; let channels = config.channels as _; diff --git a/src/host/pulseaudio/stream.rs b/src/host/pulseaudio/stream.rs index 6cbb8c395..e3fd877d0 100644 --- a/src/host/pulseaudio/stream.rs +++ b/src/host/pulseaudio/stream.rs @@ -44,6 +44,21 @@ impl StreamTrait for Stream { res.map_err(Into::::into)?; Ok(()) } + + fn buffer_size(&self) -> Option { + let (spec, bytes) = match self { + Stream::Playback(s) => ( + s.sample_spec(), + s.buffer_attr().minimum_request_length as usize, + ), + 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 { + return None; + } + Some((bytes / frame_size) as _) + } } impl Stream {