Skip to content
Merged
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
45 changes: 44 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
silently returning an empty list.
- **AAudio**: Bump MSRV to 1.85.
- **AAudio**: Buffers with default sizes are now dynamically tuned.
- **AAudio**: `SupportedBufferSize` now reports `min: 1`.
- **AAudio**: `SupportedBufferSize` in enumeration is now `Unknown`.
- **AAudio**: `default_input_config()` and `default_output_config()` now prefer 48 kHz, then
44.1 kHz, then the maximum supported sample rate, instead of always taking the maximum.
- **AAudio**: Channel enumeration extended to 8 channels.
- **ALSA**: Stream error callback now receives `ErrorKind::DeviceNotAvailable` on device
disconnection.
- **ALSA**: Polling errors trigger underrun recovery instead of looping.
Expand All @@ -99,6 +100,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **AudioWorklet**: `BufferSize::Fixed` now sets `renderSizeHint` on the `AudioContext`.
- **AudioWorklet**: `default_output_config()` now uses 48 kHz as the default sample rate instead
of 44.1 kHz, reflecting the dominant native rate on modern hardware.
- **AudioWorklet**: `channels: 0` or `sample_rate: 0` now return `InvalidInput` instead of `UnsupportedConfig`.
- **AudioWorklet**: Sample rates now enumerated as discrete standard rates in the spec-required
range of 3–768 kHz.
- **CoreAudio**: Bump MSRV to 1.85.
- **CoreAudio**: Bump `mach2` to 0.6 (uses `core::ffi` instead of `libc`, enables tvOS builds).
- **CoreAudio**: Timestamps now include device latency and safety offset.
Expand Down Expand Up @@ -141,6 +145,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **WebAudio**: Initial buffer scheduling offset now scales with buffer duration.
- **WebAudio**: `default_output_config()` now uses 48 kHz as the default sample rate instead of
44.1 kHz, reflecting the dominant native rate on modern hardware.
- **WebAudio**: Sample rates now enumerated as discrete standard rates in the spec-required
range of 3–768 kHz.

### Removed

Expand All @@ -154,13 +160,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- Fix numeric overflows in calls to create `StreamInstant` in ASIO, CoreAudio and JACK.
- **AAudio**: Fix panic in device configuration enumeration for pathological channel counts.
- **AAudio**: Fix thread lock when a stream is dropped before it fully starts.
- **AAudio**: Fix capture and playback timestamps falling back to time-zero on error.
- **AAudio**: Fix capture and playback timestamp not accounting for audio pipeline buffer depth.
- **AAudio**: Fix overflow in `buffer_capacity_in_frames` for large fixed buffer sizes.
- **AAudio**: Poisoned stream locks now return `ErrorKind::StreamInvalidated` instead of panicking.
- **AAudio**: Output buffers are now zero-filled before the callback runs.
- **AAudio**: Stream errors are now forwarded to `error_callback`.
- **AAudio**: Fix `channels: 0` returning `UnsupportedConfig` instead of `InvalidInput`.
- **AAudio**: Fix `sample_rate: 0` silently opening a stream at the NDK default rate instead of
returning `InvalidInput`.
- **ALSA**: Fix capture stream hanging or spinning on overruns.
- **ALSA**: Fix timestamps stepping backward during stream startup or after xrun recovery.
- **ALSA**: Fix spurious timestamp errors during stream startup.
Expand All @@ -177,6 +187,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **ALSA**: Fix `supported_configs()` using the same buffer range for all formats and channels.
- **ALSA**: Fix `supported_configs()` dropping sample rates outside of `COMMON_SAMPLE_RATES`.
- **ALSA**: Fix `BufferSize::Fixed(0)` being silently accepted.
- **ALSA**: Fix `channels: 0` or `sample_rate: 0` returning `UnsupportedConfig` instead of `InvalidInput`.
- **ALSA**: Fix `build_*_stream_raw` returning `UnsupportedConfig` instead of `UnsupportedOperation` when
the device does not support the requested direction.
- **ASIO**: Fix enumeration returning only the first device when using `collect()`.
- **ASIO**: Fix device enumeration and stream creation failing when called from spawned threads.
- **ASIO**: Fix buffer size not resizing when the driver reports `kAsioBufferSizeChange`.
Expand All @@ -190,7 +203,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **ASIO**: Fix `driver.sample_rate()` failures at stream creation being silently ignored.
- **ASIO**: Fix callbacks firing before `build_*_stream` returns the `Stream` handle.
- **ASIO**: Fix overrun not being reported when the driver reports `kAsioOverload`.
- **ASIO**: Fix `channels: 0`, `sample_rate: 0`, or `BufferSize::Fixed(0)` not returning
`ErrorKind::InvalidInput`; preventing a divide-by-zero panic.
- **ASIO**: Fix `BufferSize::Fixed` with a size that does not align to the driver's step constraint
not returning `ErrorKind::UnsupportedConfig`.
- **AudioWorklet**: Fix `default_output_device()` to return `None` when AudioWorklet is unavailable.
- **AudioWorklet**: Fix channel count exceeding `destination.maxChannelCount` silently using fewer
channels than requested.
- **AudioWorklet**: Fix `supported_output_configs()` reporting the buffer size upper bound as
`FrameCount::MAX`; now correctly `floor(6 × sample_rate)` per spec.
- **AudioWorklet**: Fix `supported_output_configs()` reporting the minimum render quantum size as
128 when `renderQuantumSize` is supported; the spec minimum is 1.
- **CoreAudio**: Fix default output streams silently stopping when the system default output
device is unplugged; they now reroute automatically or report `ErrorKind::DeviceNotAvailable`.
- **CoreAudio**: Fix undefined behaviour and silent failure in loopback device creation.
Expand All @@ -201,23 +224,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **CoreAudio**: Fix crashes on certain drivers due to early initialization.
- **CoreAudio**: Fix `supported_output_configs()` and `supported_input_configs()` collapsing
non-continuous hardware rates into a continuous range of sample rates (regression since v0.17.0).
- **CoreAudio**: Fix `channels: 0`, `sample_rate: 0`, or `BufferSize::Fixed(0)` to return `ErrorKind::InvalidInput`.
- **CoreAudio**: Fix `BufferSize::Fixed` producing cryptic backend errors when not validated against
the hardware buffer frame size range before stream creation.
- **CoreAudio (iOS)**: Fix `BufferSize::Fixed` not being validated against the supported range before stream creation.
- **JACK**: Fix input capture timestamp using callback execution time instead of cycle start.
- **JACK**: Poisoned error callback mutex no longer silently drops subsequent error notifications.
- **JACK**: Port registration failure now fails stream creation instead of silently failing.
- **JACK**: `activate_async()` failure now returns an error instead of panicking.
- **JACK**: Sample rate is now validated against the live JACK server at stream creation time.
- **JACK**: Underrun notification no longer blocks the notification thread.
- **JACK**: Output buffers are now zero-filled before the callback runs.
- **JACK**: Fix `channels: 0`, `sample_rate: 0`, or `BufferSize::Fixed(0)` not returning `ErrorKind::InvalidInput`
before attempting server connection.
- **JACK**: Fix `supported_input_configs()` and `supported_output_configs()` reporting a hardcoded sparse channel
list instead of enumerating all counts up to the number of physical system ports.
- **PipeWire**: Fix `channels: 0` or `sample_rate: 0` silently using PipeWire-negotiated values instead of
returning `ErrorKind::InvalidInput`.
- **PulseAudio**: Fix `channels: 0` or `sample_rate: 0` reaching the server instead of returning `ErrorKind::InvalidInput`.
- **WASAPI**: Poisoned locks now returns an error instead of panicking.
- **WASAPI**: Output buffers are now zero-filled before the callback runs.
- **WASAPI**: Fix audio worker thread spawn failure panicking instead of returning an error.
- **WASAPI**: Fix callbacks firing before `build_*_stream` returns the `Stream` handle.
- **WASAPI**: Fix Communications-class inputs to return silence.
- **WASAPI**: Fix `supported_input_configs()` advertising unsupported sample rates on input
devices.
- **WASAPI**: Fix `sample_rate: 0` with `BufferSize::Fixed` causing a divide-by-zero panic.
- **WASAPI**: Fix `channels: 0` or `sample_rate: 0` not returning `ErrorKind::InvalidInput`.
- **WASAPI**: Fix `supported_input_configs()`, `supported_output_configs()`, `default_input_config()`,
and `default_output_config()` reporting an unconstrained buffer range on software audio stacks.
- **PulseAudio**: Fix `supported_output_configs()` and `default_output_config()` to account for PulseAudio's double-buffer.
- **WebAudio**: Fix overflow with pathological channel counts.
- **WebAudio**: Fix duplicated callbacks on repeated `play()` calls.
- **WebAudio**: Report errors through the callback instead of panicking.
- **WebAudio**: Fix `default_output_device()` to return `None` when WebAudio is unavailable.
- **WebAudio**: Fix `channels: 0`, `sample_rate: 0`, or `BufferSize::Fixed(0)` not returning `ErrorKind::InvalidInput`.
- **WebAudio**: Fix channel count exceeding `destination.maxChannelCount` silently using fewer
channels than requested.


## [0.17.3] - 2026-02-18
Expand Down
5 changes: 3 additions & 2 deletions asio-sys/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- Added `Driver::latencies()`
- Added `Driver::latencies()` to query input and output stream latencies in frames
- Added `BufferPreference` enum expressing the driver's preferred buffer size and valid-size constraints
- `asio_message` now dispatches `kAsioResyncRequest` and `kAsioLatenciesChanged` to callbacks
instead of silently ignoring them
- `sample_rate_did_change` now dispatches `AsioDriverEvent::SampleRateChanged` to registered
Expand All @@ -25,7 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Public-facing `c_long` fields and return types replaced with `i32`
- Public-facing `c_double` parameters and return types replaced with `f64`
- `Driver::latencies()` now returns `Latencies { input, output }`
- `Driver::buffersize_range()` now returns `BufferSizeRange { min, max }`
- `BufferSizeRange` adds `preferred: BufferPreference` field
- `CallbackInfo::system_time` is now `u64` nanoseconds
- `AsioError::ASE_NoMemory` renamed to `AsioError::NoMemory`
- `AsioTime::reserved`, `AsioTimeInfo::reserved`, `AsioTimeCode::future` fields made private.
Expand Down
23 changes: 20 additions & 3 deletions asio-sys/src/bindings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,20 @@ pub struct Latencies {
pub output: i32,
}

/// Hardware buffer size preferences and constraints.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum BufferPreference {
Only(u32),
Preferred(u32),
Stepped { preferred: u32, step: u32 },
}

/// Minimum and maximum supported buffer sizes in frames.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct BufferSizeRange {
pub min: i32,
pub max: i32,
pub preferred: BufferPreference,
}

/// Information provided to the BufferCallback.
Expand Down Expand Up @@ -389,7 +398,7 @@ impl Asio {
let mut driver_names: [[c_char; MAX_DRIVER_NAME_LEN]; MAX_DRIVERS] =
[[0; MAX_DRIVER_NAME_LEN]; MAX_DRIVERS];
// Pointer to each driver name.
let mut driver_name_ptrs: [*mut i8; MAX_DRIVERS] = [null_mut(); MAX_DRIVERS];
let mut driver_name_ptrs: [*mut c_char; MAX_DRIVERS] = [null_mut(); MAX_DRIVERS];
for (ptr, name) in driver_name_ptrs.iter_mut().zip(&mut driver_names[..]) {
*ptr = (*name).as_mut_ptr();
}
Expand Down Expand Up @@ -450,7 +459,7 @@ impl Asio {
let mut driver_info = std::mem::MaybeUninit::<ai::ASIODriverInfo>::uninit();

unsafe {
match ai::load_asio_driver(driver_name_cstring.as_ptr() as *mut i8) {
match ai::load_asio_driver(driver_name_cstring.as_ptr() as *mut c_char) {
false => Err(LoadDriverError::LoadDriverFailed),
true => {
// Initialize ASIO.
Expand Down Expand Up @@ -527,6 +536,14 @@ impl Driver {
Ok(BufferSizeRange {
min: buffer_sizes.min,
max: buffer_sizes.max,
preferred: match buffer_sizes.grans {
-1 => BufferPreference::Only(buffer_sizes.pref as u32),
0 => BufferPreference::Preferred(buffer_sizes.pref as u32),
granularity => BufferPreference::Stepped {
preferred: buffer_sizes.pref as u32,
step: granularity as u32,
},
},
})
}

Expand Down Expand Up @@ -600,7 +617,7 @@ impl Driver {
let name_cstring = CString::new(self.inner.name.as_str())
.expect("driver name already stored must not contain null bytes");
unsafe {
if !ai::load_asio_driver(name_cstring.as_ptr() as *mut i8) {
if !ai::load_asio_driver(name_cstring.as_ptr() as *mut c_char) {
return Err(AsioError::NoDrivers);
}
let mut driver_info = std::mem::MaybeUninit::<ai::ASIODriverInfo>::uninit();
Expand Down
69 changes: 20 additions & 49 deletions src/host/aaudio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
//! Default backend on Android.

use std::{
cmp,
convert::TryInto,
fmt,
hash::{Hash, Hasher},
Expand Down Expand Up @@ -104,14 +103,9 @@ impl From<AndroidDeviceType> for InterfaceType {
}
}

// constants from android.media.AudioFormat
const CHANNEL_OUT_MONO: i32 = 4;
const CHANNEL_OUT_STEREO: i32 = 12;

// Android Java API supports up to 8 channels
// TODO: more channels available in native AAudio
// Maps channel masks to their corresponding channel counts
const CHANNEL_CONFIGS: [(i32, ChannelCount); 2] = [(CHANNEL_OUT_MONO, 1), (CHANNEL_OUT_STEREO, 2)];
// ITU-R BS.2051 standard surround channel counts; used as fallback when the device does not
// report its own via AudioDeviceInfo.getChannelCounts().
const DEFAULT_CHANNEL_COUNTS: [i32; 5] = [1, 2, 4, 6, 8];

const SAMPLE_RATES: [i32; 15] = [
5512, 8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000,
Expand Down Expand Up @@ -206,22 +200,22 @@ impl HostTrait for Host {
}

fn buffer_size_range() -> SupportedBufferSize {
SupportedBufferSize::Range {
min: 1,
max: i32::MAX as FrameCount,
}
// The valid range for frames_per_data_callback is any positive i32, but the meaningful
// lower bound (frames_per_burst) is only known after open_stream.
SupportedBufferSize::Unknown
}

fn default_supported_configs() -> VecIntoIter<SupportedStreamConfigRange> {
const FORMATS: [SampleFormat; 2] = [SampleFormat::I16, SampleFormat::F32];

let buffer_size = buffer_size_range();
let mut output = Vec::with_capacity(SAMPLE_RATES.len() * CHANNEL_CONFIGS.len() * FORMATS.len());
let mut output =
Vec::with_capacity(SAMPLE_RATES.len() * DEFAULT_CHANNEL_COUNTS.len() * FORMATS.len());
for sample_format in &FORMATS {
for (_channel_mask, channel_count) in &CHANNEL_CONFIGS {
for channel_count in &DEFAULT_CHANNEL_COUNTS {
for sample_rate in &SAMPLE_RATES {
output.push(SupportedStreamConfigRange {
channels: *channel_count,
channels: *channel_count as ChannelCount,
min_sample_rate: *sample_rate as SampleRate,
max_sample_rate: *sample_rate as SampleRate,
buffer_size,
Expand All @@ -241,11 +235,10 @@ fn device_supported_configs(device: &AudioDeviceInfo) -> VecIntoIter<SupportedSt
&SAMPLE_RATES
};

const ALL_CHANNELS: [i32; 2] = [1, 2];
let channel_counts: &[i32] = if !device.channel_counts.is_empty() {
&device.channel_counts
} else {
&ALL_CHANNELS
&DEFAULT_CHANNEL_COUNTS
};

const ALL_FORMATS: [SampleFormat; 2] = [SampleFormat::I16, SampleFormat::F32];
Expand All @@ -259,15 +252,15 @@ fn device_supported_configs(device: &AudioDeviceInfo) -> VecIntoIter<SupportedSt
let mut output = Vec::with_capacity(sample_rates.len() * channel_counts.len() * formats.len());
for sample_rate in sample_rates {
for channel_count in channel_counts {
assert!(*channel_count > 0);
if *channel_count > 2 {
// could be supported by the device
// TODO: more channels available in native AAudio
let Ok(channels) = ChannelCount::try_from(*channel_count) else {
continue;
};
if channels == 0 {
continue;
}
for format in formats {
output.push(SupportedStreamConfigRange {
channels: cmp::min(*channel_count as ChannelCount, 2),
channels,
min_sample_rate: *sample_rate as SampleRate,
max_sample_rate: *sample_rate as SampleRate,
buffer_size,
Expand Down Expand Up @@ -635,6 +628,7 @@ impl DeviceTrait for Device {
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
E: FnMut(Error) + Send + 'static,
{
crate::validate_stream_config(&config)?;
let format = match sample_format {
SampleFormat::I16 => ndk::audio::AudioFormat::PCM_I16,
SampleFormat::F32 => ndk::audio::AudioFormat::PCM_Float,
Expand All @@ -645,21 +639,9 @@ impl DeviceTrait for Device {
))
}
};
let channel_count = match config.channels {
1 => 1,
2 => 2,
channels => {
// TODO: more channels available in native AAudio
return Err(Error::with_message(
ErrorKind::UnsupportedConfig,
format!("Channel count {channels} is not supported"),
));
}
};

let builder = ndk::audio::AudioStreamBuilder::new()?
.direction(ndk::audio::AudioDirection::Input)
.channel_count(channel_count)
.channel_count(config.channels as i32)
.format(format);

build_input_stream(
Expand All @@ -684,6 +666,7 @@ impl DeviceTrait for Device {
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
E: FnMut(Error) + Send + 'static,
{
crate::validate_stream_config(&config)?;
let format = match sample_format {
SampleFormat::I16 => ndk::audio::AudioFormat::PCM_I16,
SampleFormat::F32 => ndk::audio::AudioFormat::PCM_Float,
Expand All @@ -694,21 +677,9 @@ impl DeviceTrait for Device {
))
}
};
let channel_count = match config.channels {
1 => 1,
2 => 2,
channels => {
// TODO: more channels available in native AAudio
return Err(Error::with_message(
ErrorKind::UnsupportedConfig,
format!("Channel count {channels} is not supported"),
));
}
};

let builder = ndk::audio::AudioStreamBuilder::new()?
.direction(ndk::audio::AudioDirection::Output)
.channel_count(channel_count)
.channel_count(config.channels as i32)
.format(format);

build_output_stream(
Expand Down
Loading
Loading