Skip to content

Commit 0efc950

Browse files
authored
fix: stream config validation for all backends (#1215)
1 parent 3e73d82 commit 0efc950

13 files changed

Lines changed: 464 additions & 162 deletions

File tree

CHANGELOG.md

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7272
silently returning an empty list.
7373
- **AAudio**: Bump MSRV to 1.85.
7474
- **AAudio**: Buffers with default sizes are now dynamically tuned.
75-
- **AAudio**: `SupportedBufferSize` now reports `min: 1`.
75+
- **AAudio**: `SupportedBufferSize` in enumeration is now `Unknown`.
7676
- **AAudio**: `default_input_config()` and `default_output_config()` now prefer 48 kHz, then
7777
44.1 kHz, then the maximum supported sample rate, instead of always taking the maximum.
78+
- **AAudio**: Channel enumeration extended to 8 channels.
7879
- **ALSA**: Stream error callback now receives `ErrorKind::DeviceNotAvailable` on device
7980
disconnection.
8081
- **ALSA**: Polling errors trigger underrun recovery instead of looping.
@@ -99,6 +100,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99100
- **AudioWorklet**: `BufferSize::Fixed` now sets `renderSizeHint` on the `AudioContext`.
100101
- **AudioWorklet**: `default_output_config()` now uses 48 kHz as the default sample rate instead
101102
of 44.1 kHz, reflecting the dominant native rate on modern hardware.
103+
- **AudioWorklet**: `channels: 0` or `sample_rate: 0` now return `InvalidInput` instead of `UnsupportedConfig`.
104+
- **AudioWorklet**: Sample rates now enumerated as discrete standard rates in the spec-required
105+
range of 3–768 kHz.
102106
- **CoreAudio**: Bump MSRV to 1.85.
103107
- **CoreAudio**: Bump `mach2` to 0.6 (uses `core::ffi` instead of `libc`, enables tvOS builds).
104108
- **CoreAudio**: Timestamps now include device latency and safety offset.
@@ -141,6 +145,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
141145
- **WebAudio**: Initial buffer scheduling offset now scales with buffer duration.
142146
- **WebAudio**: `default_output_config()` now uses 48 kHz as the default sample rate instead of
143147
44.1 kHz, reflecting the dominant native rate on modern hardware.
148+
- **WebAudio**: Sample rates now enumerated as discrete standard rates in the spec-required
149+
range of 3–768 kHz.
144150

145151
### Removed
146152

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

156162
- Fix numeric overflows in calls to create `StreamInstant` in ASIO, CoreAudio and JACK.
163+
- **AAudio**: Fix panic in device configuration enumeration for pathological channel counts.
157164
- **AAudio**: Fix thread lock when a stream is dropped before it fully starts.
158165
- **AAudio**: Fix capture and playback timestamps falling back to time-zero on error.
159166
- **AAudio**: Fix capture and playback timestamp not accounting for audio pipeline buffer depth.
160167
- **AAudio**: Fix overflow in `buffer_capacity_in_frames` for large fixed buffer sizes.
161168
- **AAudio**: Poisoned stream locks now return `ErrorKind::StreamInvalidated` instead of panicking.
162169
- **AAudio**: Output buffers are now zero-filled before the callback runs.
163170
- **AAudio**: Stream errors are now forwarded to `error_callback`.
171+
- **AAudio**: Fix `channels: 0` returning `UnsupportedConfig` instead of `InvalidInput`.
172+
- **AAudio**: Fix `sample_rate: 0` silently opening a stream at the NDK default rate instead of
173+
returning `InvalidInput`.
164174
- **ALSA**: Fix capture stream hanging or spinning on overruns.
165175
- **ALSA**: Fix timestamps stepping backward during stream startup or after xrun recovery.
166176
- **ALSA**: Fix spurious timestamp errors during stream startup.
@@ -177,6 +187,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
177187
- **ALSA**: Fix `supported_configs()` using the same buffer range for all formats and channels.
178188
- **ALSA**: Fix `supported_configs()` dropping sample rates outside of `COMMON_SAMPLE_RATES`.
179189
- **ALSA**: Fix `BufferSize::Fixed(0)` being silently accepted.
190+
- **ALSA**: Fix `channels: 0` or `sample_rate: 0` returning `UnsupportedConfig` instead of `InvalidInput`.
191+
- **ALSA**: Fix `build_*_stream_raw` returning `UnsupportedConfig` instead of `UnsupportedOperation` when
192+
the device does not support the requested direction.
180193
- **ASIO**: Fix enumeration returning only the first device when using `collect()`.
181194
- **ASIO**: Fix device enumeration and stream creation failing when called from spawned threads.
182195
- **ASIO**: Fix buffer size not resizing when the driver reports `kAsioBufferSizeChange`.
@@ -190,7 +203,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
190203
- **ASIO**: Fix `driver.sample_rate()` failures at stream creation being silently ignored.
191204
- **ASIO**: Fix callbacks firing before `build_*_stream` returns the `Stream` handle.
192205
- **ASIO**: Fix overrun not being reported when the driver reports `kAsioOverload`.
206+
- **ASIO**: Fix `channels: 0`, `sample_rate: 0`, or `BufferSize::Fixed(0)` not returning
207+
`ErrorKind::InvalidInput`; preventing a divide-by-zero panic.
208+
- **ASIO**: Fix `BufferSize::Fixed` with a size that does not align to the driver's step constraint
209+
not returning `ErrorKind::UnsupportedConfig`.
193210
- **AudioWorklet**: Fix `default_output_device()` to return `None` when AudioWorklet is unavailable.
211+
- **AudioWorklet**: Fix channel count exceeding `destination.maxChannelCount` silently using fewer
212+
channels than requested.
213+
- **AudioWorklet**: Fix `supported_output_configs()` reporting the buffer size upper bound as
214+
`FrameCount::MAX`; now correctly `floor(6 × sample_rate)` per spec.
215+
- **AudioWorklet**: Fix `supported_output_configs()` reporting the minimum render quantum size as
216+
128 when `renderQuantumSize` is supported; the spec minimum is 1.
194217
- **CoreAudio**: Fix default output streams silently stopping when the system default output
195218
device is unplugged; they now reroute automatically or report `ErrorKind::DeviceNotAvailable`.
196219
- **CoreAudio**: Fix undefined behaviour and silent failure in loopback device creation.
@@ -201,23 +224,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
201224
- **CoreAudio**: Fix crashes on certain drivers due to early initialization.
202225
- **CoreAudio**: Fix `supported_output_configs()` and `supported_input_configs()` collapsing
203226
non-continuous hardware rates into a continuous range of sample rates (regression since v0.17.0).
227+
- **CoreAudio**: Fix `channels: 0`, `sample_rate: 0`, or `BufferSize::Fixed(0)` to return `ErrorKind::InvalidInput`.
228+
- **CoreAudio**: Fix `BufferSize::Fixed` producing cryptic backend errors when not validated against
229+
the hardware buffer frame size range before stream creation.
230+
- **CoreAudio (iOS)**: Fix `BufferSize::Fixed` not being validated against the supported range before stream creation.
204231
- **JACK**: Fix input capture timestamp using callback execution time instead of cycle start.
205232
- **JACK**: Poisoned error callback mutex no longer silently drops subsequent error notifications.
206233
- **JACK**: Port registration failure now fails stream creation instead of silently failing.
207234
- **JACK**: `activate_async()` failure now returns an error instead of panicking.
208235
- **JACK**: Sample rate is now validated against the live JACK server at stream creation time.
209236
- **JACK**: Underrun notification no longer blocks the notification thread.
210237
- **JACK**: Output buffers are now zero-filled before the callback runs.
238+
- **JACK**: Fix `channels: 0`, `sample_rate: 0`, or `BufferSize::Fixed(0)` not returning `ErrorKind::InvalidInput`
239+
before attempting server connection.
240+
- **JACK**: Fix `supported_input_configs()` and `supported_output_configs()` reporting a hardcoded sparse channel
241+
list instead of enumerating all counts up to the number of physical system ports.
242+
- **PipeWire**: Fix `channels: 0` or `sample_rate: 0` silently using PipeWire-negotiated values instead of
243+
returning `ErrorKind::InvalidInput`.
244+
- **PulseAudio**: Fix `channels: 0` or `sample_rate: 0` reaching the server instead of returning `ErrorKind::InvalidInput`.
211245
- **WASAPI**: Poisoned locks now returns an error instead of panicking.
212246
- **WASAPI**: Output buffers are now zero-filled before the callback runs.
213247
- **WASAPI**: Fix audio worker thread spawn failure panicking instead of returning an error.
214248
- **WASAPI**: Fix callbacks firing before `build_*_stream` returns the `Stream` handle.
215249
- **WASAPI**: Fix Communications-class inputs to return silence.
216250
- **WASAPI**: Fix `supported_input_configs()` advertising unsupported sample rates on input
217251
devices.
252+
- **WASAPI**: Fix `sample_rate: 0` with `BufferSize::Fixed` causing a divide-by-zero panic.
253+
- **WASAPI**: Fix `channels: 0` or `sample_rate: 0` not returning `ErrorKind::InvalidInput`.
254+
- **WASAPI**: Fix `supported_input_configs()`, `supported_output_configs()`, `default_input_config()`,
255+
and `default_output_config()` reporting an unconstrained buffer range on software audio stacks.
256+
- **PulseAudio**: Fix `supported_output_configs()` and `default_output_config()` to account for PulseAudio's double-buffer.
257+
- **WebAudio**: Fix overflow with pathological channel counts.
218258
- **WebAudio**: Fix duplicated callbacks on repeated `play()` calls.
219259
- **WebAudio**: Report errors through the callback instead of panicking.
220260
- **WebAudio**: Fix `default_output_device()` to return `None` when WebAudio is unavailable.
261+
- **WebAudio**: Fix `channels: 0`, `sample_rate: 0`, or `BufferSize::Fixed(0)` not returning `ErrorKind::InvalidInput`.
262+
- **WebAudio**: Fix channel count exceeding `destination.maxChannelCount` silently using fewer
263+
channels than requested.
221264

222265

223266
## [0.17.3] - 2026-02-18

asio-sys/CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Added
11-
- Added `Driver::latencies()`
11+
- Added `Driver::latencies()` to query input and output stream latencies in frames
12+
- Added `BufferPreference` enum expressing the driver's preferred buffer size and valid-size constraints
1213
- `asio_message` now dispatches `kAsioResyncRequest` and `kAsioLatenciesChanged` to callbacks
1314
instead of silently ignoring them
1415
- `sample_rate_did_change` now dispatches `AsioDriverEvent::SampleRateChanged` to registered
@@ -25,7 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2526
- Public-facing `c_long` fields and return types replaced with `i32`
2627
- Public-facing `c_double` parameters and return types replaced with `f64`
2728
- `Driver::latencies()` now returns `Latencies { input, output }`
28-
- `Driver::buffersize_range()` now returns `BufferSizeRange { min, max }`
29+
- `BufferSizeRange` adds `preferred: BufferPreference` field
2930
- `CallbackInfo::system_time` is now `u64` nanoseconds
3031
- `AsioError::ASE_NoMemory` renamed to `AsioError::NoMemory`
3132
- `AsioTime::reserved`, `AsioTimeInfo::reserved`, `AsioTimeCode::future` fields made private.

asio-sys/src/bindings/mod.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,20 @@ pub struct Latencies {
9898
pub output: i32,
9999
}
100100

101+
/// Hardware buffer size preferences and constraints.
102+
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
103+
pub enum BufferPreference {
104+
Only(u32),
105+
Preferred(u32),
106+
Stepped { preferred: u32, step: u32 },
107+
}
108+
101109
/// Minimum and maximum supported buffer sizes in frames.
102110
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
103111
pub struct BufferSizeRange {
104112
pub min: i32,
105113
pub max: i32,
114+
pub preferred: BufferPreference,
106115
}
107116

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

452461
unsafe {
453-
match ai::load_asio_driver(driver_name_cstring.as_ptr() as *mut i8) {
462+
match ai::load_asio_driver(driver_name_cstring.as_ptr() as *mut c_char) {
454463
false => Err(LoadDriverError::LoadDriverFailed),
455464
true => {
456465
// Initialize ASIO.
@@ -527,6 +536,14 @@ impl Driver {
527536
Ok(BufferSizeRange {
528537
min: buffer_sizes.min,
529538
max: buffer_sizes.max,
539+
preferred: match buffer_sizes.grans {
540+
-1 => BufferPreference::Only(buffer_sizes.pref as u32),
541+
0 => BufferPreference::Preferred(buffer_sizes.pref as u32),
542+
granularity => BufferPreference::Stepped {
543+
preferred: buffer_sizes.pref as u32,
544+
step: granularity as u32,
545+
},
546+
},
530547
})
531548
}
532549

@@ -600,7 +617,7 @@ impl Driver {
600617
let name_cstring = CString::new(self.inner.name.as_str())
601618
.expect("driver name already stored must not contain null bytes");
602619
unsafe {
603-
if !ai::load_asio_driver(name_cstring.as_ptr() as *mut i8) {
620+
if !ai::load_asio_driver(name_cstring.as_ptr() as *mut c_char) {
604621
return Err(AsioError::NoDrivers);
605622
}
606623
let mut driver_info = std::mem::MaybeUninit::<ai::ASIODriverInfo>::uninit();

src/host/aaudio/mod.rs

Lines changed: 20 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
//! Default backend on Android.
44
55
use std::{
6-
cmp,
76
convert::TryInto,
87
fmt,
98
hash::{Hash, Hasher},
@@ -104,14 +103,9 @@ impl From<AndroidDeviceType> for InterfaceType {
104103
}
105104
}
106105

107-
// constants from android.media.AudioFormat
108-
const CHANNEL_OUT_MONO: i32 = 4;
109-
const CHANNEL_OUT_STEREO: i32 = 12;
110-
111-
// Android Java API supports up to 8 channels
112-
// TODO: more channels available in native AAudio
113-
// Maps channel masks to their corresponding channel counts
114-
const CHANNEL_CONFIGS: [(i32, ChannelCount); 2] = [(CHANNEL_OUT_MONO, 1), (CHANNEL_OUT_STEREO, 2)];
106+
// ITU-R BS.2051 standard surround channel counts; used as fallback when the device does not
107+
// report its own via AudioDeviceInfo.getChannelCounts().
108+
const DEFAULT_CHANNEL_COUNTS: [i32; 5] = [1, 2, 4, 6, 8];
115109

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

208202
fn buffer_size_range() -> SupportedBufferSize {
209-
SupportedBufferSize::Range {
210-
min: 1,
211-
max: i32::MAX as FrameCount,
212-
}
203+
// The valid range for frames_per_data_callback is any positive i32, but the meaningful
204+
// lower bound (frames_per_burst) is only known after open_stream.
205+
SupportedBufferSize::Unknown
213206
}
214207

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

218211
let buffer_size = buffer_size_range();
219-
let mut output = Vec::with_capacity(SAMPLE_RATES.len() * CHANNEL_CONFIGS.len() * FORMATS.len());
212+
let mut output =
213+
Vec::with_capacity(SAMPLE_RATES.len() * DEFAULT_CHANNEL_COUNTS.len() * FORMATS.len());
220214
for sample_format in &FORMATS {
221-
for (_channel_mask, channel_count) in &CHANNEL_CONFIGS {
215+
for channel_count in &DEFAULT_CHANNEL_COUNTS {
222216
for sample_rate in &SAMPLE_RATES {
223217
output.push(SupportedStreamConfigRange {
224-
channels: *channel_count,
218+
channels: *channel_count as ChannelCount,
225219
min_sample_rate: *sample_rate as SampleRate,
226220
max_sample_rate: *sample_rate as SampleRate,
227221
buffer_size,
@@ -241,11 +235,10 @@ fn device_supported_configs(device: &AudioDeviceInfo) -> VecIntoIter<SupportedSt
241235
&SAMPLE_RATES
242236
};
243237

244-
const ALL_CHANNELS: [i32; 2] = [1, 2];
245238
let channel_counts: &[i32] = if !device.channel_counts.is_empty() {
246239
&device.channel_counts
247240
} else {
248-
&ALL_CHANNELS
241+
&DEFAULT_CHANNEL_COUNTS
249242
};
250243

251244
const ALL_FORMATS: [SampleFormat; 2] = [SampleFormat::I16, SampleFormat::F32];
@@ -259,15 +252,15 @@ fn device_supported_configs(device: &AudioDeviceInfo) -> VecIntoIter<SupportedSt
259252
let mut output = Vec::with_capacity(sample_rates.len() * channel_counts.len() * formats.len());
260253
for sample_rate in sample_rates {
261254
for channel_count in channel_counts {
262-
assert!(*channel_count > 0);
263-
if *channel_count > 2 {
264-
// could be supported by the device
265-
// TODO: more channels available in native AAudio
255+
let Ok(channels) = ChannelCount::try_from(*channel_count) else {
256+
continue;
257+
};
258+
if channels == 0 {
266259
continue;
267260
}
268261
for format in formats {
269262
output.push(SupportedStreamConfigRange {
270-
channels: cmp::min(*channel_count as ChannelCount, 2),
263+
channels,
271264
min_sample_rate: *sample_rate as SampleRate,
272265
max_sample_rate: *sample_rate as SampleRate,
273266
buffer_size,
@@ -635,6 +628,7 @@ impl DeviceTrait for Device {
635628
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
636629
E: FnMut(Error) + Send + 'static,
637630
{
631+
crate::validate_stream_config(&config)?;
638632
let format = match sample_format {
639633
SampleFormat::I16 => ndk::audio::AudioFormat::PCM_I16,
640634
SampleFormat::F32 => ndk::audio::AudioFormat::PCM_Float,
@@ -645,21 +639,9 @@ impl DeviceTrait for Device {
645639
))
646640
}
647641
};
648-
let channel_count = match config.channels {
649-
1 => 1,
650-
2 => 2,
651-
channels => {
652-
// TODO: more channels available in native AAudio
653-
return Err(Error::with_message(
654-
ErrorKind::UnsupportedConfig,
655-
format!("Channel count {channels} is not supported"),
656-
));
657-
}
658-
};
659-
660642
let builder = ndk::audio::AudioStreamBuilder::new()?
661643
.direction(ndk::audio::AudioDirection::Input)
662-
.channel_count(channel_count)
644+
.channel_count(config.channels as i32)
663645
.format(format);
664646

665647
build_input_stream(
@@ -684,6 +666,7 @@ impl DeviceTrait for Device {
684666
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
685667
E: FnMut(Error) + Send + 'static,
686668
{
669+
crate::validate_stream_config(&config)?;
687670
let format = match sample_format {
688671
SampleFormat::I16 => ndk::audio::AudioFormat::PCM_I16,
689672
SampleFormat::F32 => ndk::audio::AudioFormat::PCM_Float,
@@ -694,21 +677,9 @@ impl DeviceTrait for Device {
694677
))
695678
}
696679
};
697-
let channel_count = match config.channels {
698-
1 => 1,
699-
2 => 2,
700-
channels => {
701-
// TODO: more channels available in native AAudio
702-
return Err(Error::with_message(
703-
ErrorKind::UnsupportedConfig,
704-
format!("Channel count {channels} is not supported"),
705-
));
706-
}
707-
};
708-
709680
let builder = ndk::audio::AudioStreamBuilder::new()?
710681
.direction(ndk::audio::AudioDirection::Output)
711-
.channel_count(channel_count)
682+
.channel_count(config.channels as i32)
712683
.format(format);
713684

714685
build_output_stream(

0 commit comments

Comments
 (0)