From 1508ff3f7f87f5731af249d4c64c840eaf0b0d88 Mon Sep 17 00:00:00 2001 From: John Wells Date: Tue, 22 Jul 2025 23:38:15 -0400 Subject: [PATCH 1/7] use set_buffer_size_near to calculate fixed alsa buffer size --- src/host/alsa/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index df060bae9..c74aea3fb 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -1107,7 +1107,7 @@ fn set_hw_params_from_format( match config.buffer_size { BufferSize::Fixed(v) => { hw_params.set_period_size_near((v / 4) as alsa::pcm::Frames, alsa::ValueOr::Nearest)?; - hw_params.set_buffer_size(v as alsa::pcm::Frames)?; + hw_params.set_buffer_size_near(v as alsa::pcm::Frames)?; } BufferSize::Default => { // These values together represent a moderate latency and wakeup interval. From cddb3611f2052b4cb80eae1ed7688a48f7a084d3 Mon Sep 17 00:00:00 2001 From: John Wells Date: Thu, 31 Jul 2025 09:46:07 -0400 Subject: [PATCH 2/7] ALSA: Fallback to default buffer size on error --- CHANGELOG.md | 1 + src/host/alsa/mod.rs | 28 ++++++++++++++++++---------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cc14b98a..ea8088d77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Unreleased - ALSA(process_output): pass `silent=true` to `PCM.try_recover`, so it doesn't write to stderr +- ALSA: Fix buffer size selection. (error 22) - WASAPI: Expose IMMDevice from WASAPI host Device. # Version 0.16.0 (2025-06-07) diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index c74aea3fb..7999d02a1 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -1104,19 +1104,27 @@ fn set_hw_params_from_format( hw_params.set_rate(config.sample_rate.0, alsa::ValueOr::Nearest)?; hw_params.set_channels(config.channels as u32)?; - match config.buffer_size { - BufferSize::Fixed(v) => { - hw_params.set_period_size_near((v / 4) as alsa::pcm::Frames, alsa::ValueOr::Nearest)?; - hw_params.set_buffer_size_near(v as alsa::pcm::Frames)?; - } - BufferSize::Default => { - // These values together represent a moderate latency and wakeup interval. - // Without them, we are at the mercy of the device - hw_params.set_period_time_near(25_000, alsa::ValueOr::Nearest)?; - hw_params.set_buffer_time_near(100_000, alsa::ValueOr::Nearest)?; + let mut buffer_size = config.buffer_size; + + if let BufferSize::Fixed(v) = buffer_size { + if hw_params + .set_period_size_near((v / 4) as alsa::pcm::Frames, alsa::ValueOr::Nearest) + .is_err() + || hw_params + .set_buffer_size_near(v as alsa::pcm::Frames) + .is_err() + { + buffer_size = BufferSize::Default; } } + if let BufferSize::Default = buffer_size { + // These values together represent a moderate latency and wakeup interval. + // Without them, we are at the mercy of the device + hw_params.set_period_time_near(25_000, alsa::ValueOr::Nearest)?; + hw_params.set_buffer_time_near(100_000, alsa::ValueOr::Nearest)?; + } + pcm_handle.hw_params(&hw_params)?; Ok(hw_params.can_pause()) From 0c9c099cfa82f77587e3ea5e8c3aae1b2cea5f02 Mon Sep 17 00:00:00 2001 From: John Wells Date: Sat, 9 Aug 2025 13:17:37 -0400 Subject: [PATCH 3/7] ALSA: Add error handling to period/buffer size setup --- src/host/alsa/mod.rs | 78 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 61 insertions(+), 17 deletions(-) diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index 7999d02a1..8fa92f66b 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -5,9 +5,9 @@ use self::alsa::poll::Descriptors; use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data, - DefaultStreamConfigError, DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, - PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, - SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, + DefaultStreamConfigError, DeviceNameError, DevicesError, FrameCount, InputCallbackInfo, + OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, + StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, }; use std::cell::Cell; @@ -1104,30 +1104,74 @@ fn set_hw_params_from_format( hw_params.set_rate(config.sample_rate.0, alsa::ValueOr::Nearest)?; hw_params.set_channels(config.channels as u32)?; - let mut buffer_size = config.buffer_size; + let _ = set_hw_params_periods(&hw_params, config.buffer_size); + + pcm_handle.hw_params(&hw_params)?; + + Ok(hw_params.can_pause()) +} + +/// Returns true if the periods were reasonably set. A false result indicates the device default +/// configuration is being used. +fn set_hw_params_periods(hw_params: &alsa::pcm::HwParams, buffer_size: BufferSize) -> bool { + const FALLBACK_PERIOD_TIME: u32 = 25_000; + + // When the API is made available, this could rely on snd_pcm_hw_params_get_periods_min and + // snd_pcm_hw_params_get_periods_max + const PERIOD_COUNT: u32 = 2; + + /// Returns true if the buffer size was reasonably set. + fn set_hw_params_buffer_size(hw_params: &alsa::pcm::HwParams, buffer_size: FrameCount) -> bool { + // Desired period size + let period_size = (buffer_size / PERIOD_COUNT) as alsa::pcm::Frames; - if let BufferSize::Fixed(v) = buffer_size { if hw_params - .set_period_size_near((v / 4) as alsa::pcm::Frames, alsa::ValueOr::Nearest) + .set_period_size_near(period_size, alsa::ValueOr::Greater) .is_err() - || hw_params - .set_buffer_size_near(v as alsa::pcm::Frames) - .is_err() { - buffer_size = BufferSize::Default; + return false; + } + + // Actual period size + let period_size = if let Ok(period_size) = hw_params.get_period_size() { + period_size + } else { + return false; + }; + + hw_params + .set_buffer_size_near(period_size * PERIOD_COUNT as alsa::pcm::Frames) + .is_ok() + } + + if let BufferSize::Fixed(val) = buffer_size { + if set_hw_params_buffer_size(hw_params, val) { + return true; } } - if let BufferSize::Default = buffer_size { - // These values together represent a moderate latency and wakeup interval. - // Without them, we are at the mercy of the device - hw_params.set_period_time_near(25_000, alsa::ValueOr::Nearest)?; - hw_params.set_buffer_time_near(100_000, alsa::ValueOr::Nearest)?; + if hw_params + .set_period_time_near(FALLBACK_PERIOD_TIME, alsa::ValueOr::Nearest) + .is_err() + { + return false; } - pcm_handle.hw_params(&hw_params)?; + let period_size = if let Ok(period_size) = hw_params.get_period_size() { + period_size + } else { + return false; + }; - Ok(hw_params.can_pause()) + // We should not fail if the driver is unhappy here. + // `default` pcm sometimes fails here, but there no reason to as we attempt to provide a size or + // minimum number of periods. + hw_params + .set_buffer_size(period_size * PERIOD_COUNT as alsa::pcm::Frames) + .is_ok() + || hw_params + .set_periods(PERIOD_COUNT, alsa::ValueOr::Greater) + .is_ok() } fn set_sw_params_from_format( From 5086bdbb989ffeb0e21b9f965e9420a144aef1a6 Mon Sep 17 00:00:00 2001 From: John Wells Date: Sat, 9 Aug 2025 16:57:42 -0400 Subject: [PATCH 4/7] Fix merge issue --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7076584c3..6f7445c65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Unreleased -- ALSA(process_output): pass `silent=true` to `PCM.try_recover`, so it doesn't write to stderr +- ALSA(process_output): pass `silent=true` to `PCM.try_recover`, so it doesn't write to stderr. - ALSA: Fix buffer size selection. (error 22) - CoreAudio: `Device::supported_configs` now returns a single element containing the available sample rate range when all elements have the same `mMinimum` and `mMaximum` values (which is the most common case). - iOS: Fix example by properly activating audio session. From 4da16f13b41595effed6bf6f8ca49cd26a035c3f Mon Sep 17 00:00:00 2001 From: John Wells Date: Mon, 18 Aug 2025 09:53:22 -0400 Subject: [PATCH 5/7] Clamp ALSA buffer size to valid CPAL values --- CHANGELOG.md | 2 +- src/host/alsa/mod.rs | 104 ++++++++++++++++++++++++++++++++----------- 2 files changed, 79 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 537ded42d..a5b1124fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Unreleased - ALSA(process_output): Pass `silent=true` to `PCM.try_recover`, so it doesn't write to stderr. -- ALSA: Fix buffer size selection. (error 22) +- ALSA: Fix buffer and period size selection by rounding to supported values. Actual buffer size may be different from the requested size or may be a device-specified default size. Additionally sets ALSA "periods" to 2 (previously 4). (error 22) - CoreAudio: `Device::supported_configs` now returns a single element containing the available sample rate range when all elements have the same `mMinimum` and `mMaximum` values (which is the most common case). - CoreAudio: Detect default audio device lazily when building a stream, instead of during device enumeration. - iOS: Fix example by properly activating audio session. diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index 8fa92f66b..306e4f49f 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -13,6 +13,7 @@ use crate::{ use std::cell::Cell; use std::cmp; use std::convert::TryInto; +use std::ops::RangeInclusive; use std::sync::{Arc, Mutex}; use std::thread::{self, JoinHandle}; use std::time::Duration; @@ -25,6 +26,9 @@ pub type SupportedOutputConfigs = VecIntoIter; mod enumerate; +const VALID_BUFFER_SIZE: RangeInclusive = + 1..=FrameCount::MAX as alsa::pcm::Frames; + /// The default linux, dragonfly, freebsd and netbsd host type. #[derive(Debug)] pub struct Host; @@ -413,12 +417,10 @@ impl Device { }) .collect::>(); - let min_buffer_size = hw_params.get_buffer_size_min()?; - let max_buffer_size = hw_params.get_buffer_size_max()?; - + let (min_buffer_size, max_buffer_size) = hw_params_buffer_size_min_max(&hw_params); let buffer_size_range = SupportedBufferSize::Range { - min: min_buffer_size as u32, - max: max_buffer_size as u32, + min: min_buffer_size, + max: max_buffer_size, }; let mut output = Vec::with_capacity( @@ -1040,6 +1042,35 @@ impl StreamTrait for Stream { } } +// Overly safe clamp because alsa Frames are i64 +fn clamp_frame_count(buffer_size: alsa::pcm::Frames) -> FrameCount { + buffer_size.clamp(1, FrameCount::MAX as _) as _ +} + +fn hw_params_buffer_size_min_max(hw_params: &alsa::pcm::HwParams) -> (FrameCount, FrameCount) { + let min_buf = hw_params + .get_buffer_size_min() + .map(clamp_frame_count) + .unwrap_or(1); + let max_buf = hw_params + .get_buffer_size_max() + .map(clamp_frame_count) + .unwrap_or(FrameCount::MAX); + (min_buf, max_buf) +} + +fn hw_params_period_size_min_max(hw_params: &alsa::pcm::HwParams) -> (FrameCount, FrameCount) { + let min_buf = hw_params + .get_period_size_min() + .map(clamp_frame_count) + .unwrap_or(1); + let max_buf = hw_params + .get_period_size_max() + .map(clamp_frame_count) + .unwrap_or(FrameCount::MAX); + (min_buf, max_buf) +} + fn set_hw_params_from_format( pcm_handle: &alsa::pcm::PCM, config: &StreamConfig, @@ -1116,32 +1147,43 @@ fn set_hw_params_from_format( fn set_hw_params_periods(hw_params: &alsa::pcm::HwParams, buffer_size: BufferSize) -> bool { const FALLBACK_PERIOD_TIME: u32 = 25_000; - // When the API is made available, this could rely on snd_pcm_hw_params_get_periods_min and - // snd_pcm_hw_params_get_periods_max + // TODO: When the API is made available, this could rely on snd_pcm_hw_params_get_periods_min + // and snd_pcm_hw_params_get_periods_max const PERIOD_COUNT: u32 = 2; /// Returns true if the buffer size was reasonably set. - fn set_hw_params_buffer_size(hw_params: &alsa::pcm::HwParams, buffer_size: FrameCount) -> bool { - // Desired period size - let period_size = (buffer_size / PERIOD_COUNT) as alsa::pcm::Frames; + fn set_hw_params_buffer_size( + hw_params: &alsa::pcm::HwParams, + mut buffer_size: FrameCount, + ) -> bool { + buffer_size = { + let (min_buffer_size, max_buffer_size) = hw_params_buffer_size_min_max(hw_params); + buffer_size.clamp(min_buffer_size, max_buffer_size) + }; - if hw_params - .set_period_size_near(period_size, alsa::ValueOr::Greater) - .is_err() - { - return false; - } + // Desired period size + let period_size = { + let (min_period_size, max_period_size) = hw_params_period_size_min_max(hw_params); + (buffer_size / PERIOD_COUNT).clamp(min_period_size, max_period_size) + }; // Actual period size - let period_size = if let Ok(period_size) = hw_params.get_period_size() { - period_size - } else { + let Ok(period_size) = + hw_params.set_period_size_near(period_size as _, alsa::ValueOr::Greater) + else { return false; }; - hw_params - .set_buffer_size_near(period_size * PERIOD_COUNT as alsa::pcm::Frames) - .is_ok() + if let Ok(buffer_size) = + hw_params.set_buffer_size_near(period_size * PERIOD_COUNT as alsa::pcm::Frames) + { + // Double-check the set size is within the CPAL range + if VALID_BUFFER_SIZE.contains(&buffer_size) { + return true; + } + } + + false } if let BufferSize::Fixed(val) = buffer_size { @@ -1166,12 +1208,22 @@ fn set_hw_params_periods(hw_params: &alsa::pcm::HwParams, buffer_size: BufferSiz // We should not fail if the driver is unhappy here. // `default` pcm sometimes fails here, but there no reason to as we attempt to provide a size or // minimum number of periods. + if let Ok(buffer_size) = + hw_params.set_buffer_size_near(period_size * PERIOD_COUNT as alsa::pcm::Frames) + { + // Double-check the set size is within the CPAL range + if VALID_BUFFER_SIZE.contains(&buffer_size) { + return true; + } + } + + hw_params.set_buffer_size_min(1).unwrap_or_default(); + hw_params + .set_buffer_size_max(FrameCount::MAX as _) + .unwrap_or_default(); hw_params - .set_buffer_size(period_size * PERIOD_COUNT as alsa::pcm::Frames) + .set_periods(PERIOD_COUNT, alsa::ValueOr::Nearest) .is_ok() - || hw_params - .set_periods(PERIOD_COUNT, alsa::ValueOr::Greater) - .is_ok() } fn set_sw_params_from_format( From 7910693ed68ac0e208443afab44ed393401e659c Mon Sep 17 00:00:00 2001 From: John Wells Date: Tue, 26 Aug 2025 08:49:29 -0400 Subject: [PATCH 6/7] Always set ALSA period count --- src/host/alsa/mod.rs | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index 306e4f49f..6e1b92c58 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -1151,9 +1151,29 @@ fn set_hw_params_periods(hw_params: &alsa::pcm::HwParams, buffer_size: BufferSiz // and snd_pcm_hw_params_get_periods_max const PERIOD_COUNT: u32 = 2; + // Restrict the configuration space to contain only one periods count + hw_params + .set_periods(PERIOD_COUNT, alsa::ValueOr::Nearest) + .unwrap_or_default(); + + let Some(period_count) = hw_params + .get_periods() + .ok() + .filter(|&period_count| period_count > 0) + else { + return false; + }; + /// Returns true if the buffer size was reasonably set. + /// + /// The buffer is a ring buffer. The buffer size always has to be greater than one period size. + /// Commonly this is 2*period size, but some hardware can do 8 periods per buffer. It is also + /// possible for the buffer size to not be an integer multiple of the period size. + /// + /// See: https://www.alsa-project.org/wiki/FramesPeriods fn set_hw_params_buffer_size( hw_params: &alsa::pcm::HwParams, + period_count: u32, mut buffer_size: FrameCount, ) -> bool { buffer_size = { @@ -1164,7 +1184,7 @@ fn set_hw_params_periods(hw_params: &alsa::pcm::HwParams, buffer_size: BufferSiz // Desired period size let period_size = { let (min_period_size, max_period_size) = hw_params_period_size_min_max(hw_params); - (buffer_size / PERIOD_COUNT).clamp(min_period_size, max_period_size) + (buffer_size / period_count).clamp(min_period_size, max_period_size) }; // Actual period size @@ -1175,7 +1195,7 @@ fn set_hw_params_periods(hw_params: &alsa::pcm::HwParams, buffer_size: BufferSiz }; if let Ok(buffer_size) = - hw_params.set_buffer_size_near(period_size * PERIOD_COUNT as alsa::pcm::Frames) + hw_params.set_buffer_size_near(period_size * period_count as alsa::pcm::Frames) { // Double-check the set size is within the CPAL range if VALID_BUFFER_SIZE.contains(&buffer_size) { @@ -1187,7 +1207,7 @@ fn set_hw_params_periods(hw_params: &alsa::pcm::HwParams, buffer_size: BufferSiz } if let BufferSize::Fixed(val) = buffer_size { - if set_hw_params_buffer_size(hw_params, val) { + if set_hw_params_buffer_size(hw_params, period_count, val) { return true; } } @@ -1209,7 +1229,7 @@ fn set_hw_params_periods(hw_params: &alsa::pcm::HwParams, buffer_size: BufferSiz // `default` pcm sometimes fails here, but there no reason to as we attempt to provide a size or // minimum number of periods. if let Ok(buffer_size) = - hw_params.set_buffer_size_near(period_size * PERIOD_COUNT as alsa::pcm::Frames) + hw_params.set_buffer_size_near(period_size * period_count as alsa::pcm::Frames) { // Double-check the set size is within the CPAL range if VALID_BUFFER_SIZE.contains(&buffer_size) { @@ -1221,9 +1241,8 @@ fn set_hw_params_periods(hw_params: &alsa::pcm::HwParams, buffer_size: BufferSiz hw_params .set_buffer_size_max(FrameCount::MAX as _) .unwrap_or_default(); - hw_params - .set_periods(PERIOD_COUNT, alsa::ValueOr::Nearest) - .is_ok() + + true } fn set_sw_params_from_format( From a8ef3ef9ad89a803931305ff796f2440b028ddab Mon Sep 17 00:00:00 2001 From: John Wells Date: Sat, 30 Aug 2025 12:35:36 -0400 Subject: [PATCH 7/7] Fail if unable to set requested buffer size --- src/host/alsa/mod.rs | 47 +++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index 6e1b92c58..30ceb14e8 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -1135,7 +1135,14 @@ fn set_hw_params_from_format( hw_params.set_rate(config.sample_rate.0, alsa::ValueOr::Nearest)?; hw_params.set_channels(config.channels as u32)?; - let _ = set_hw_params_periods(&hw_params, config.buffer_size); + if !set_hw_params_periods(&hw_params, config.buffer_size) { + return Err(BackendSpecificError { + description: format!( + "Buffer size '{:?}' is not supported by this backend", + config.buffer_size + ), + }); + } pcm_handle.hw_params(&hw_params)?; @@ -1194,22 +1201,18 @@ fn set_hw_params_periods(hw_params: &alsa::pcm::HwParams, buffer_size: BufferSiz return false; }; - if let Ok(buffer_size) = + let Ok(buffer_size) = hw_params.set_buffer_size_near(period_size * period_count as alsa::pcm::Frames) - { - // Double-check the set size is within the CPAL range - if VALID_BUFFER_SIZE.contains(&buffer_size) { - return true; - } - } + else { + return false; + }; - false + // Double-check the set size is within the CPAL range + VALID_BUFFER_SIZE.contains(&buffer_size) } if let BufferSize::Fixed(val) = buffer_size { - if set_hw_params_buffer_size(hw_params, period_count, val) { - return true; - } + return set_hw_params_buffer_size(hw_params, period_count, val); } if hw_params @@ -1228,21 +1231,15 @@ fn set_hw_params_periods(hw_params: &alsa::pcm::HwParams, buffer_size: BufferSiz // We should not fail if the driver is unhappy here. // `default` pcm sometimes fails here, but there no reason to as we attempt to provide a size or // minimum number of periods. - if let Ok(buffer_size) = + let Ok(buffer_size) = hw_params.set_buffer_size_near(period_size * period_count as alsa::pcm::Frames) - { - // Double-check the set size is within the CPAL range - if VALID_BUFFER_SIZE.contains(&buffer_size) { - return true; - } - } - - hw_params.set_buffer_size_min(1).unwrap_or_default(); - hw_params - .set_buffer_size_max(FrameCount::MAX as _) - .unwrap_or_default(); + else { + return hw_params.set_buffer_size_min(1).is_ok() + && hw_params.set_buffer_size_max(FrameCount::MAX as _).is_ok(); + }; - true + // Double-check the set size is within the CPAL range + VALID_BUFFER_SIZE.contains(&buffer_size) } fn set_sw_params_from_format(