Skip to content

Commit 8c4d971

Browse files
authored
feat: validate BufferSize::Fixed against supported range for hosts (#1031)
* feat: validate BufferSize::Fixed against supported range for hosts Add buffer size validation for ALSA, ASIO, JACK, Emscripten, and WebAudio. Remove redundant validation for CoreAudio. * refactor: move JACK buffer size validation into helper method
1 parent 2b7dc14 commit 8c4d971

File tree

9 files changed

+69
-18
lines changed

9 files changed

+69
-18
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
- Add `Sample::bits_per_sample` method.
44
- Update `audio_thread_priority` to 0.34.
55
- AAudio: Configure buffer to ensure consistent callback buffer sizes.
6-
- ALSA: Improve `BufferSize::Fixed` latency precision and audio callback performance.
6+
- ALSA: Improve `BufferSize::Fixed` precision and audio callback performance.
77
- ALSA: Change `BufferSize::Default` to use the device defaults.
88
- ALSA: Change card enumeration to work like `aplay -L` does.
99
- ALSA: Add `I24` and `U24` sample format support (24-bit samples stored in 4 bytes).
@@ -22,12 +22,15 @@
2222
- CoreAudio: Update `mach2` to 0.5.
2323
- CoreAudio: Configure device buffer to ensure predictable callback buffer sizes.
2424
- CoreAudio: Fix timestamp accuracy.
25+
- Emscripten: Add `BufferSize::Fixed` validation against supported range.
2526
- iOS: Fix example by properly activating audio session.
2627
- iOS: Add complete AVAudioSession integration for device enumeration and buffer size control.
28+
- JACK: Add `BufferSize::Fixed` validation to reject requests that don't match server buffer size.
2729
- WASAPI: Expose `IMMDevice` from WASAPI host Device.
2830
- WASAPI: Add `I24` and `U24` sample format support (24-bit samples stored in 4 bytes).
2931
- WASAPI: Update `windows` to >= 0.58, <= 0.62.
3032
- Wasm: Removed optional `wee-alloc` feature for security reasons.
33+
- WebAudio: Add `BufferSize::Fixed` validation against supported range.
3134

3235
# Version 0.16.0 (2025-06-07)
3336

src/host/aaudio/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,9 @@ fn configure_for_device(
215215
builder
216216
};
217217
builder = builder.sample_rate(config.sample_rate.0.try_into().unwrap());
218+
219+
// Note: Buffer size validation is not needed - the native AAudio API validates buffer sizes
220+
// when `open_stream()` is called.
218221
match &config.buffer_size {
219222
BufferSize::Default => builder,
220223
BufferSize::Fixed(size) => builder

src/host/alsa/mod.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,28 @@ impl Device {
279279
sample_format: SampleFormat,
280280
stream_type: alsa::Direction,
281281
) -> Result<StreamInner, BuildStreamError> {
282+
// Validate buffer size if Fixed is specified. This is necessary because
283+
// `set_period_size_near()` with `ValueOr::Nearest` will accept ANY value and return the
284+
// "nearest" supported value, which could be wildly different (e.g., requesting 4096 frames
285+
// might return 512 frames if that's "nearest").
286+
if let BufferSize::Fixed(requested_size) = conf.buffer_size {
287+
// Note: We use `default_input_config`/`default_output_config` to get the buffer size
288+
// range. This queries the CURRENT device (`self.pcm_id`), not the default device. The
289+
// buffer size range is the same across all format configurations for a given device
290+
// (see `supported_configs()`).
291+
let supported_config = match stream_type {
292+
alsa::Direction::Capture => self.default_input_config(),
293+
alsa::Direction::Playback => self.default_output_config(),
294+
};
295+
if let Ok(config) = supported_config {
296+
if let SupportedBufferSize::Range { min, max } = config.buffer_size {
297+
if !(min..=max).contains(&requested_size) {
298+
return Err(BuildStreamError::StreamConfigNotSupported);
299+
}
300+
}
301+
}
302+
}
303+
282304
let handle_result = self
283305
.handles
284306
.lock()

src/host/asio/stream.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -661,7 +661,7 @@ fn frames_to_duration(frames: usize, rate: crate::SampleRate) -> std::time::Dura
661661

662662
/// Check whether or not the desired config is supported by the stream.
663663
///
664-
/// Checks sample rate, data type and then finally the number of channels.
664+
/// Checks sample rate, data type, number of channels, and buffer size.
665665
fn check_config(
666666
driver: &sys::Driver,
667667
config: &StreamConfig,
@@ -671,8 +671,21 @@ fn check_config(
671671
let StreamConfig {
672672
channels,
673673
sample_rate,
674-
buffer_size: _,
674+
buffer_size,
675675
} = config;
676+
677+
// Validate buffer size if `Fixed` is specified. This is necessary because ASIO's
678+
// `create_buffers` only validates the upper bound (returns `InvalidBufferSize` if > max) but
679+
// does NOT validate the lower bound. Passing a buffer size below min would be accepted but
680+
// behavior is unspecified.
681+
if let BufferSize::Fixed(requested_size) = buffer_size {
682+
let (min, max) = driver.buffersize_range().map_err(build_stream_err)?;
683+
let requested_size_i32 = *requested_size as i32;
684+
if !(min..=max).contains(&requested_size_i32) {
685+
return Err(BuildStreamError::StreamConfigNotSupported);
686+
}
687+
}
688+
676689
// Try and set the sample rate to what the user selected.
677690
let sample_rate = sample_rate.0.into();
678691
if sample_rate != driver.sample_rate().map_err(build_stream_err)? {

src/host/coreaudio/macos/device.rs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -863,7 +863,6 @@ impl Device {
863863
/// This handles the common setup tasks for both input and output streams:
864864
/// - Sets the stream format (ASBD)
865865
/// - Configures buffer size for Fixed buffer size requests
866-
/// - Validates buffer size ranges
867866
fn configure_stream_format_and_buffer(
868867
audio_unit: &mut AudioUnit,
869868
config: &StreamConfig,
@@ -879,14 +878,6 @@ fn configure_stream_format_and_buffer(
879878

880879
// Configure device buffer size if requested
881880
if let BufferSize::Fixed(buffer_size) = config.buffer_size {
882-
let buffer_size_range = get_io_buffer_frame_size_range(audio_unit)?;
883-
884-
if let SupportedBufferSize::Range { min, max } = buffer_size_range {
885-
if !(min..=max).contains(&buffer_size) {
886-
return Err(BuildStreamError::StreamConfigNotSupported);
887-
}
888-
}
889-
890881
// IMPORTANT: Buffer frame size is a DEVICE-LEVEL property, not stream-specific.
891882
// Unlike stream format above, we ALWAYS use Scope::Global + Element::Output
892883
// for device properties, regardless of whether this is an input or output stream.

src/host/emscripten/mod.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,11 +197,10 @@ impl DeviceTrait for Device {
197197

198198
let buffer_size_frames = match config.buffer_size {
199199
BufferSize::Fixed(v) => {
200-
if v == 0 {
200+
if !(MIN_BUFFER_SIZE..=MAX_BUFFER_SIZE).contains(&v) {
201201
return Err(BuildStreamError::StreamConfigNotSupported);
202-
} else {
203-
v as usize
204202
}
203+
v as usize
205204
}
206205
BufferSize::Default => DEFAULT_BUFFER_SIZE,
207206
};

src/host/jack/device.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,20 @@ impl Device {
135135
pub fn is_output(&self) -> bool {
136136
matches!(self.device_type, DeviceType::OutputDevice)
137137
}
138+
139+
/// Validate buffer size if Fixed is specified. This is necessary because JACK buffer size
140+
/// is controlled by the JACK server and cannot be changed by clients. Without validation,
141+
/// cpal would silently use the server's buffer size even if a different value was requested.
142+
fn validate_buffer_size(&self, conf: &StreamConfig) -> Result<(), BuildStreamError> {
143+
if let crate::BufferSize::Fixed(requested_size) = conf.buffer_size {
144+
if let SupportedBufferSize::Range { min, max } = self.buffer_size {
145+
if !(min..=max).contains(&requested_size) {
146+
return Err(BuildStreamError::StreamConfigNotSupported);
147+
}
148+
}
149+
}
150+
Ok(())
151+
}
138152
}
139153

140154
impl DeviceTrait for Device {
@@ -191,6 +205,8 @@ impl DeviceTrait for Device {
191205
if conf.sample_rate != self.sample_rate || sample_format != JACK_SAMPLE_FORMAT {
192206
return Err(BuildStreamError::StreamConfigNotSupported);
193207
}
208+
self.validate_buffer_size(conf)?;
209+
194210
// The settings should be fine, create a Client
195211
let client_options = super::get_client_options(self.start_server_automatically);
196212
let client;
@@ -230,6 +246,7 @@ impl DeviceTrait for Device {
230246
if conf.sample_rate != self.sample_rate || sample_format != JACK_SAMPLE_FORMAT {
231247
return Err(BuildStreamError::StreamConfigNotSupported);
232248
}
249+
self.validate_buffer_size(conf)?;
233250

234251
// The settings should be fine, create a Client
235252
let client_options = super::get_client_options(self.start_server_automatically);

src/host/wasapi/device.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,8 @@ impl Device {
559559
}
560560
};
561561

562+
// Note: Buffer size validation is not needed here - `IAudioClient::Initialize`
563+
// will return `AUDCLNT_E_BUFFER_SIZE_ERROR` if the buffer size is not supported.
562564
let buffer_duration =
563565
buffer_size_to_duration(&config.buffer_size, config.sample_rate.0);
564566

@@ -674,6 +676,8 @@ impl Device {
674676
.build_audioclient()
675677
.map_err(windows_err_to_cpal_err::<BuildStreamError>)?;
676678

679+
// Note: Buffer size validation is not needed here - `IAudioClient::Initialize`
680+
// will return `AUDCLNT_E_BUFFER_SIZE_ERROR` if the buffer size is not supported.
677681
let buffer_duration =
678682
buffer_size_to_duration(&config.buffer_size, config.sample_rate.0);
679683

src/host/webaudio/mod.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,11 +203,10 @@ impl DeviceTrait for Device {
203203

204204
let buffer_size_frames = match config.buffer_size {
205205
BufferSize::Fixed(v) => {
206-
if v == 0 {
206+
if !(MIN_BUFFER_SIZE..=MAX_BUFFER_SIZE).contains(&v) {
207207
return Err(BuildStreamError::StreamConfigNotSupported);
208-
} else {
209-
v as usize
210208
}
209+
v as usize
211210
}
212211
BufferSize::Default => DEFAULT_BUFFER_SIZE,
213212
};

0 commit comments

Comments
 (0)