Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ 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]

### Added

- `StreamTrait::buffer_size` method to query the callback buffer size (actual size or upper limit depending on platform).
- **AAudio**: `BufferSize::Default` now explicitly configures using the optimal burst size from AudioManager.

## [0.17.0] - 2025-12-20

### Added
Expand Down Expand Up @@ -1017,6 +1024,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Initial commit.

[Unreleased]: https://github.com/RustAudio/cpal/compare/v0.17.0...HEAD
[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
Expand Down
81 changes: 51 additions & 30 deletions src/host/aaudio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@ pub struct Device(Option<AudioDeviceInfo>);
/// - The pointer in AudioStream (NonNull<AAudioStreamStruct>) 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<Mutex<AudioStream>>),
Output(Arc<Mutex<AudioStream>>),
pub struct Stream {
inner: Arc<Mutex<AudioStream>>,
direction: DeviceDirection,
}

// SAFETY: AudioStream can be safely sent between threads. The AAudio C API is thread-safe
Expand Down Expand Up @@ -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().map(|s| s as u32)
}
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
}
}

Expand Down Expand Up @@ -330,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<D, E>(
Expand Down Expand Up @@ -376,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 {
Expand Down Expand Up @@ -579,31 +594,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<crate::FrameCount> {
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.
let frames = match stream.frames_per_data_callback() {
Some(size) if size > 0 => size,
_ => stream.frames_per_burst(),
};
Some(frames as crate::FrameCount)
}
}
3 changes: 3 additions & 0 deletions src/host/alsa/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1220,6 +1220,9 @@ impl StreamTrait for Stream {
self.inner.channel.pause(true).ok();
Ok(())
}
fn buffer_size(&self) -> Option<FrameCount> {
Some(self.inner.period_frames as FrameCount)
}
}

// Convert ALSA frames to FrameCount, clamping to valid range.
Expand Down
4 changes: 4 additions & 0 deletions src/host/asio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,8 @@ impl StreamTrait for Stream {
fn pause(&self) -> Result<(), PauseStreamError> {
Stream::pause(self)
}

fn buffer_size(&self) -> Option<crate::FrameCount> {
Stream::buffer_size(self)
}
}
9 changes: 9 additions & 0 deletions src/host/asio/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ impl Stream {
self.playing.store(false, Ordering::SeqCst);
Ok(())
}

pub fn buffer_size(&self) -> Option<crate::FrameCount> {
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 {
Expand Down
5 changes: 4 additions & 1 deletion src/host/coreaudio/ios/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<crate::FrameCount> {
Some(get_device_buffer_frames() as crate::FrameCount)
}
}

struct StreamInner {
Expand Down
4 changes: 3 additions & 1 deletion src/host/coreaudio/macos/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<usize, coreaudio::Error> {
pub(crate) fn get_device_buffer_frame_size(
audio_unit: &AudioUnit,
) -> Result<usize, coreaudio::Error> {
// 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(
Expand Down
8 changes: 8 additions & 0 deletions src/host/coreaudio/macos/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,14 @@ impl StreamTrait for Stream {

stream.pause()
}

fn buffer_size(&self) -> Option<crate::FrameCount> {
let stream = self.inner.lock().ok()?;

device::get_device_buffer_frame_size(&stream.audio_unit)
.ok()
.map(|size| size as crate::FrameCount)
}
}

#[cfg(test)]
Expand Down
4 changes: 4 additions & 0 deletions src/host/jack/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,10 @@ impl StreamTrait for Stream {
self.playing.store(false, Ordering::SeqCst);
Ok(())
}

fn buffer_size(&self) -> Option<crate::FrameCount> {
Some(self.async_client.as_client().buffer_size() as crate::FrameCount)
}
}

type InputDataCallback = Box<dyn FnMut(&Data, &InputCallbackInfo) + Send + 'static>;
Expand Down
17 changes: 17 additions & 0 deletions src/host/wasapi/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -130,6 +137,7 @@ impl Stream {
thread: Some(thread),
commands: tx,
pending_scheduled_event,
max_frames_in_buffer,
}
}

Expand All @@ -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,
Expand All @@ -163,6 +173,7 @@ impl Stream {
thread: Some(thread),
commands: tx,
pending_scheduled_event,
max_frames_in_buffer,
}
}

Expand Down Expand Up @@ -198,6 +209,12 @@ impl StreamTrait for Stream {
.map_err(|_| crate::error::PauseStreamError::DeviceNotAvailable)?;
Ok(())
}

fn buffer_size(&self) -> Option<crate::FrameCount> {
// 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 {
Expand Down
11 changes: 11 additions & 0 deletions src/platform/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,17 @@ macro_rules! impl_platform_host {
)*
}
}

fn buffer_size(&self) -> Option<crate::FrameCount> {
match self.0 {
$(
$(#[cfg($feat)])?
StreamInner::$HostVariant(ref s) => {
s.buffer_size()
}
)*
}
}
}

impl From<DeviceInner> for Device {
Expand Down
22 changes: 22 additions & 0 deletions src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,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<crate::FrameCount> {
None
}
}

/// Compile-time assertion that a stream type implements [`Send`].
Expand Down