From 24c73f81f387ad8be707736f7223757e1cb38518 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 14 Oct 2025 22:10:46 +0200 Subject: [PATCH 01/11] fix: BufferSize::Default behavior on PipeWire-ALSA Avoid setting period count when BufferSize::Default is used, allowing PipeWire-ALSA to select optimal buffer and period sizes. This prevents excessively large periods and high latency on PipeWire-ALSA devices. --- src/host/alsa/mod.rs | 53 +++++++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index 075a26e9b..832bfdf97 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -49,6 +49,27 @@ use crate::{ // // This mirrors the behavior documented in the cpal API where `BufferSize::Fixed(x)` // requests but does not guarantee a specific callback size. +// +// ## BufferSize::Default Behavior +// +// When `BufferSize::Default` is specified, cpal does NOT set explicit period size or +// period count constraints, allowing the device/driver to choose sensible defaults. +// +// **Why not set defaults?** Different audio systems have different behaviors: +// +// - **Native ALSA hardware**: Typically chooses reasonable defaults (e.g., 1024-2048 +// frame periods with 2-4 periods) +// +// - **PipeWire-ALSA plugin**: Allocates a large buffer (~1M frames at 48kHz) but uses +// small periods (~1024 frames). Critically, if you request `set_periods(2)` without +// specifying period size, PipeWire calculates period = buffer/2, resulting in +// pathologically large periods (~524K frames = 10 seconds). See issue #1029. +// +// By not constraining period configuration, PipeWire-ALSA can use its optimized defaults +// (small periods with many-period buffer), while native ALSA hardware uses its own defaults. +// +// **Startup latency**: Regardless of buffer size, cpal uses double-buffering for startup +// (start_threshold = 2 periods), ensuring low latency even with large multi-period buffers. pub type SupportedInputConfigs = VecIntoIter; pub type SupportedOutputConfigs = VecIntoIter; @@ -1288,18 +1309,23 @@ fn set_hw_params_from_format( hw_params.set_channels(config.channels as u32)?; // Configure period size based on buffer size request - // When BufferSize::Fixed(x) is specified, we request a period size of x frames - // to achieve approximately x-sized callbacks. ALSA may adjust this to the nearest - // supported value based on hardware constraints. - if let BufferSize::Fixed(buffer_frames) = config.buffer_size { - hw_params.set_period_size_near(buffer_frames as _, alsa::ValueOr::Nearest)?; + match config.buffer_size { + BufferSize::Fixed(buffer_frames) => { + // When BufferSize::Fixed(x) is specified, we request two periods with size of x frames + // to achieve approximately x-sized double-buffered callbacks. ALSA may adjust this to + // the nearest supported value based on hardware constraints. + hw_params.set_period_size_near(buffer_frames as _, alsa::ValueOr::Nearest)?; + // We shouldn't fail if the driver isn't happy here - some devices may use more periods. + let _ = hw_params.set_periods(2, alsa::ValueOr::Greater); + } + BufferSize::Default => { + // For BufferSize::Default, we don't set period size or count, allowing the device + // to choose sensible defaults. This is important for PipeWire-ALSA compatibility: + // setting periods=2 without a period size causes PipeWire to create massive periods + // (buffer_size/2), resulting in multi-second latency. See issue #1029. + } } - // We shouldn't fail if the driver isn't happy here. - // `default` pcm sometimes fails here, but there's no reason to as we - // provide a direction and 2 is strictly the minimum number of periods. - let _ = hw_params.set_periods(2, alsa::ValueOr::Greater); - // Apply hardware parameters pcm_handle.hw_params(&hw_params)?; @@ -1324,9 +1350,10 @@ fn set_sw_params_from_format( let start_threshold = match stream_type { alsa::Direction::Playback => { - // Start when ALSA buffer has enough data to maintain consistent playback - // while preserving user's expected latency across different period counts - buffer - period + // Use double-buffering (2 periods) to start playback, but cap at `buffer - period` + // to handle 2-period buffers gracefully (avoids requiring the entire buffer to be + // full). + cmp::min(2 * period, buffer.saturating_sub(period)) } alsa::Direction::Capture => 1, }; From f5f35f989f1d43456fdb1147e524b71006ad3cfd Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 14 Oct 2025 23:18:00 +0200 Subject: [PATCH 02/11] fix: Pipewire-ALSA filling complete buffer --- src/host/alsa/mod.rs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index 832bfdf97..00f8f7d19 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -764,13 +764,6 @@ fn output_stream_worker( let mut ctxt = StreamWorkerContext::new(&timeout, stream, &rx); - // As first period, always write one buffer with equilibrium values. - // This ensures we start with a full period of silence, giving the user their - // requested latency while avoiding underruns on the first callback. - if let Err(err) = stream.channel.io_bytes().writei(&ctxt.transfer_buffer) { - error_callback(err.into()); - } - loop { let flow = poll_descriptors_and_prepare_buffer(&rx, stream, &mut ctxt).unwrap_or_else(|err| { @@ -898,7 +891,10 @@ fn poll_descriptors_and_prepare_buffer( }; let available_samples = avail_frames * stream.conf.channels as usize; - // Only go on if there is at least one period's worth of space available. + // ALSA can have spurious wakeups where poll returns but avail < avail_min. + // This is documented to occur with dmix (timer-driven) and other plugins. + // Verify we have room for at least one full period before processing. + // See: https://bugzilla.kernel.org/show_bug.cgi?id=202499 if available_samples < stream.period_samples { return Ok(PollDescriptorsFlow::Continue); } @@ -1346,19 +1342,24 @@ fn set_sw_params_from_format( description: "initialization resulted in a null buffer".to_string(), }); } - sw_params.set_avail_min(period as alsa::pcm::Frames)?; - let start_threshold = match stream_type { alsa::Direction::Playback => { // Use double-buffering (2 periods) to start playback, but cap at `buffer - period` // to handle 2-period buffers gracefully (avoids requiring the entire buffer to be // full). - cmp::min(2 * period, buffer.saturating_sub(period)) + cmp::min(2 * period, buffer - period) } alsa::Direction::Capture => 1, }; sw_params.set_start_threshold(start_threshold.try_into().unwrap())?; + // Set avail_min to maintain our target latency (start_threshold). + // For large buffers (e.g., PipeWire's 1024 periods), setting avail_min to just 1 period + // causes poll to wake us continuously. Instead, only wake when we actually need to refill. + // For small buffers (2-3 periods), ensure small buffers wake every period. + let target_avail = cmp::max(period, buffer - start_threshold + 1); + sw_params.set_avail_min(target_avail as alsa::pcm::Frames)?; + period as usize * config.channels as usize }; From 67a1135913c556e792e69b5e98aee0fa007a814c Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 17 Oct 2025 21:45:33 +0200 Subject: [PATCH 03/11] fix: adjusting start_threshold logic --- src/host/alsa/mod.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index 00f8f7d19..4b8b812b5 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -1344,20 +1344,19 @@ fn set_sw_params_from_format( } let start_threshold = match stream_type { alsa::Direction::Playback => { - // Use double-buffering (2 periods) to start playback, but cap at `buffer - period` - // to handle 2-period buffers gracefully (avoids requiring the entire buffer to be - // full). - cmp::min(2 * period, buffer - period) + // Always use 2-period double-buffering: one period playing from hardware, one + // period queued in the software buffer. This ensures consistent low latency + // regardless of the total buffer size. + 2 * period } alsa::Direction::Capture => 1, }; sw_params.set_start_threshold(start_threshold.try_into().unwrap())?; - // Set avail_min to maintain our target latency (start_threshold). - // For large buffers (e.g., PipeWire's 1024 periods), setting avail_min to just 1 period - // causes poll to wake us continuously. Instead, only wake when we actually need to refill. - // For small buffers (2-3 periods), ensure small buffers wake every period. - let target_avail = cmp::max(period, buffer - start_threshold + 1); + // We want to wake when one period has been consumed from our 2-period target level. + // This prevents filling PipeWire-ALSA's huge buffer beyond 2 periods, which could lead + // to pathological latency (21+ seconds at 48 kHz with a 1M buffer). + let target_avail = buffer - period; sw_params.set_avail_min(target_avail as alsa::pcm::Frames)?; period as usize * config.channels as usize From af00525e2f1d54f9cb4e54e7ea098d8cf76257b7 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 28 Oct 2025 21:54:28 +0100 Subject: [PATCH 04/11] fix: PipeWire-ALSA buffer/period and avail_min handling Set buffer_size_near before period_size_near for Fixed buffer sizes to constrain total latency and avoid extremely large allocations (eg. PipeWire-ALSA creating ~1M-frame buffers). Then set period_size to keep double-buffering semantics. Adjust avail_min by direction: for playback wake when level drops to one period (buffer - period); for capture wake when one period is available (period) to prevent excessive capture latency. --- src/host/alsa/mod.rs | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index 4b8b812b5..8c6eadbec 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -1304,15 +1304,20 @@ 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)?; - // Configure period size based on buffer size request + // Configure buffer and period size based on buffer size request match config.buffer_size { BufferSize::Fixed(buffer_frames) => { - // When BufferSize::Fixed(x) is specified, we request two periods with size of x frames - // to achieve approximately x-sized double-buffered callbacks. ALSA may adjust this to - // the nearest supported value based on hardware constraints. + // When BufferSize::Fixed(x) is specified, we configure double-buffering with + // buffer_size = 2x and period_size = x. This provides consistent low-latency + // behavior across different ALSA implementations and hardware. + // + // Set buffer_size first to constrain total latency. Without this, some + // implementations (notably PipeWire-ALSA) allocate very large buffers (~1M frames), + // defeating the latency control that Fixed buffer size provides. + hw_params.set_buffer_size_near((2 * buffer_frames) as _, alsa::ValueOr::Nearest)?; + // Then set period_size to control callback granularity and ensure the buffer + // is divided into two periods for double-buffering. hw_params.set_period_size_near(buffer_frames as _, alsa::ValueOr::Nearest)?; - // We shouldn't fail if the driver isn't happy here - some devices may use more periods. - let _ = hw_params.set_periods(2, alsa::ValueOr::Greater); } BufferSize::Default => { // For BufferSize::Default, we don't set period size or count, allowing the device @@ -1353,10 +1358,22 @@ fn set_sw_params_from_format( }; sw_params.set_start_threshold(start_threshold.try_into().unwrap())?; - // We want to wake when one period has been consumed from our 2-period target level. - // This prevents filling PipeWire-ALSA's huge buffer beyond 2 periods, which could lead - // to pathological latency (21+ seconds at 48 kHz with a 1M buffer). - let target_avail = buffer - period; + // Set avail_min based on stream direction. For playback, "avail" means space available + // for writing (buffer_size - frames_queued). For capture, "avail" means data available + // for reading (frames_captured). These opposite semantics require different values. + let target_avail = match stream_type { + alsa::Direction::Playback => { + // Wake when buffer level drops to one period remaining (avail >= buffer - period). + // This maintains double-buffering by refilling when we're down to one period. + buffer - period + } + alsa::Direction::Capture => { + // Wake when one period of data is available to read (avail >= period). + // Using buffer - period here would cause excessive latency as capture would + // wait for nearly the entire buffer to fill before reading. + period + } + }; sw_params.set_avail_min(target_avail as alsa::pcm::Frames)?; period as usize * config.channels as usize From c8ba198a10a0d33eac2d3bd039d90d1d957806bb Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 28 Oct 2025 22:38:49 +0100 Subject: [PATCH 05/11] fix: drop ValueOr::Nearest from set_buffer_size_near --- 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 8c6eadbec..3bfc1a4d2 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -1314,7 +1314,7 @@ fn set_hw_params_from_format( // Set buffer_size first to constrain total latency. Without this, some // implementations (notably PipeWire-ALSA) allocate very large buffers (~1M frames), // defeating the latency control that Fixed buffer size provides. - hw_params.set_buffer_size_near((2 * buffer_frames) as _, alsa::ValueOr::Nearest)?; + hw_params.set_buffer_size_near((2 * buffer_frames) as _)?; // Then set period_size to control callback granularity and ensure the buffer // is divided into two periods for double-buffering. hw_params.set_period_size_near(buffer_frames as _, alsa::ValueOr::Nearest)?; From 14c376e7a11023a997d3673bfa1d932b57576485 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 29 Oct 2025 23:17:33 +0100 Subject: [PATCH 06/11] fix: blocking behavior when dropping ALSA streams Revert to v0.16 behavior: always terminate stream without attempting state-based drain or wait. Remove buffer-duration calculation and snd_pcm_wait usage to avoid delays during drop. --- src/host/alsa/mod.rs | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index 3bfc1a4d2..1a79405b6 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -1134,33 +1134,6 @@ impl Drop for Stream { self.inner.dropping.set(true); self.trigger.wakeup(); self.thread.take().unwrap().join().unwrap(); - - // State-based drop behavior: drain if playing, drop if paused. This allows audio to - // complete naturally when stopping during playback, but provides immediate termination - // when already paused. - match self.inner.channel.state() { - alsa::pcm::State::Running => { - // Audio is actively playing - attempt graceful drain. - if let Ok(()) = self.inner.channel.drain() { - // TODO: Use SND_PCM_WAIT_DRAIN (-10002) when alsa-rs supports it properly, - // although it requires ALSA 1.2.8+ which may not be available everywhere. - // For now, calculate timeout based on buffer latency. - let buffer_duration_ms = ((self.inner.period_frames as f64 * 1000.0) - / self.inner.conf.sample_rate.0 as f64) - as u32; - - // This is safe: snd_pcm_wait() checks device state first and returns - // immediately with error codes like -ENODEV for disconnected devices. - let _ = self.inner.channel.wait(Some(buffer_duration_ms)); - } - // If drain fails or device has errors, stream terminates naturally - } - _ => { - // Not actively playing (paused, stopped, etc.) - immediate drop and discard any - // buffered audio data for immediate termination. - let _ = self.inner.channel.drop(); - } - } } } From 52907be2918722483495a62fa8126535e9aebae2 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 31 Oct 2025 21:14:32 +0100 Subject: [PATCH 07/11] fix: constrain default buffer to two periods Query the device's period size and set buffer_size to 2 * period for BufferSize::Default. This prevents excessive memory allocation (e.g. PipeWire-ALSA) while respecting the device's period preference. --- src/host/alsa/mod.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index 1a79405b6..bf1499340 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -1283,20 +1283,15 @@ fn set_hw_params_from_format( // When BufferSize::Fixed(x) is specified, we configure double-buffering with // buffer_size = 2x and period_size = x. This provides consistent low-latency // behavior across different ALSA implementations and hardware. - // - // Set buffer_size first to constrain total latency. Without this, some - // implementations (notably PipeWire-ALSA) allocate very large buffers (~1M frames), - // defeating the latency control that Fixed buffer size provides. hw_params.set_buffer_size_near((2 * buffer_frames) as _)?; - // Then set period_size to control callback granularity and ensure the buffer - // is divided into two periods for double-buffering. hw_params.set_period_size_near(buffer_frames as _, alsa::ValueOr::Nearest)?; } BufferSize::Default => { - // For BufferSize::Default, we don't set period size or count, allowing the device - // to choose sensible defaults. This is important for PipeWire-ALSA compatibility: - // setting periods=2 without a period size causes PipeWire to create massive periods - // (buffer_size/2), resulting in multi-second latency. See issue #1029. + // For BufferSize::Default, let the device choose the period size, then configure + // the buffer to 2 periods. This prevents excessive memory allocation while + // respecting the device's period size preference. + let period = hw_params.get_period_size()?; + hw_params.set_buffer_size_near((2 * period) as _)?; } } From cfd869ddcd3dc69da45e262b04874eae07ed573b Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 31 Oct 2025 22:21:14 +0100 Subject: [PATCH 08/11] fix: use device minimum buffer size for Default This should always align with the two-period double-buffering strategy that we use anyway. --- src/host/alsa/mod.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index bf1499340..a50b37c0b 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -1287,11 +1287,12 @@ fn set_hw_params_from_format( hw_params.set_period_size_near(buffer_frames as _, alsa::ValueOr::Nearest)?; } BufferSize::Default => { - // For BufferSize::Default, let the device choose the period size, then configure - // the buffer to 2 periods. This prevents excessive memory allocation while - // respecting the device's period size preference. - let period = hw_params.get_period_size()?; - hw_params.set_buffer_size_near((2 * period) as _)?; + // For BufferSize::Default, try to use the minimum buffer size the device reports. + // This prevents excessive memory allocation (e.g., PipeWire's 524K frames) while + // respecting device constraints. + if let Ok(buffer_min) = hw_params.get_buffer_size_min() { + let _ = hw_params.set_buffer_size_near(buffer_min as _); + } } } From e15d1cc70e36d691fc259fcab02ed591a79850f0 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 31 Oct 2025 23:15:44 +0100 Subject: [PATCH 09/11] fix: use double-buffering for ALSA default buffers --- src/host/alsa/mod.rs | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index a50b37c0b..04af07219 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -1277,28 +1277,25 @@ 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)?; - // Configure buffer and period size based on buffer size request - match config.buffer_size { - BufferSize::Fixed(buffer_frames) => { - // When BufferSize::Fixed(x) is specified, we configure double-buffering with - // buffer_size = 2x and period_size = x. This provides consistent low-latency - // behavior across different ALSA implementations and hardware. - hw_params.set_buffer_size_near((2 * buffer_frames) as _)?; - hw_params.set_period_size_near(buffer_frames as _, alsa::ValueOr::Nearest)?; - } - BufferSize::Default => { - // For BufferSize::Default, try to use the minimum buffer size the device reports. - // This prevents excessive memory allocation (e.g., PipeWire's 524K frames) while - // respecting device constraints. - if let Ok(buffer_min) = hw_params.get_buffer_size_min() { - let _ = hw_params.set_buffer_size_near(buffer_min as _); - } - } + // When BufferSize::Fixed(x) is specified, we configure double-buffering with + // buffer_size = 2x and period_size = x. This provides consistent low-latency + // behavior across different ALSA implementations and hardware. + if let BufferSize::Fixed(buffer_frames) = config.buffer_size { + hw_params.set_buffer_size_near((2 * buffer_frames) as _)?; + hw_params.set_period_size_near(buffer_frames as _, alsa::ValueOr::Nearest)?; } // Apply hardware parameters pcm_handle.hw_params(&hw_params)?; + // For BufferSize::Default, trim the buffer to 2 periods for double-buffering + if config.buffer_size == BufferSize::Default { + if let Ok(period) = hw_params.get_period_size() { + hw_params.set_buffer_size_near(2 * period)?; + pcm_handle.hw_params(&hw_params)?; + } + } + Ok(hw_params.can_pause()) } From c94de35a351951c7f1e2e070fc009ab6fb381865 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 11 Nov 2025 20:18:04 -0500 Subject: [PATCH 10/11] fix: clear ALSA hw param setup for default buffer size - Introduce init_hw_params and a TryFrom implementation to centralize format and hw param initialization. - For BufferSize::Default, constrain both period and buffer to enforce double-buffering (2 periods), preventing pathologically large periods with PipeWire-ALSA. - Update docs to clarify ring buffer/period behaviors and add issue references. --- src/host/alsa/mod.rs | 144 ++++++++++++++++++++++++------------------- 1 file changed, 81 insertions(+), 63 deletions(-) diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index 04af07219..6f0db8e74 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -57,19 +57,21 @@ use crate::{ // // **Why not set defaults?** Different audio systems have different behaviors: // -// - **Native ALSA hardware**: Typically chooses reasonable defaults (e.g., 1024-2048 +// - **Native ALSA hardware**: Typically chooses reasonable defaults (e.g., 512-2048 // frame periods with 2-4 periods) // -// - **PipeWire-ALSA plugin**: Allocates a large buffer (~1M frames at 48kHz) but uses -// small periods (~1024 frames). Critically, if you request `set_periods(2)` without -// specifying period size, PipeWire calculates period = buffer/2, resulting in -// pathologically large periods (~524K frames = 10 seconds). See issue #1029. +// - **PipeWire-ALSA plugin**: Allocates a large ring buffer (~1M frames at 48kHz) but +// uses small periods (512-1024 frames). Critically, if you request `set_periods(2)` +// without specifying period size, PipeWire calculates period = buffer/2, resulting +// in pathologically large periods (~524K frames = 10 seconds). See issues #1029 and +// #1036. // // By not constraining period configuration, PipeWire-ALSA can use its optimized defaults // (small periods with many-period buffer), while native ALSA hardware uses its own defaults. // // **Startup latency**: Regardless of buffer size, cpal uses double-buffering for startup -// (start_threshold = 2 periods), ensuring low latency even with large multi-period buffers. +// (start_threshold = 2 periods), ensuring low latency even with large multi-period ring +// buffers. pub type SupportedInputConfigs = VecIntoIter; pub type SupportedOutputConfigs = VecIntoIter; @@ -1214,68 +1216,25 @@ fn fill_with_equilibrium(buffer: &mut [u8], sample_format: SampleFormat) { } } -fn set_hw_params_from_format( - pcm_handle: &alsa::pcm::PCM, +fn init_hw_params<'a>( + pcm_handle: &'a alsa::pcm::PCM, config: &StreamConfig, sample_format: SampleFormat, -) -> Result { +) -> Result, BackendSpecificError> { let hw_params = alsa::pcm::HwParams::any(pcm_handle)?; hw_params.set_access(alsa::pcm::Access::RWInterleaved)?; - - let sample_format = if cfg!(target_endian = "big") { - match sample_format { - SampleFormat::I8 => alsa::pcm::Format::S8, - SampleFormat::I16 => alsa::pcm::Format::S16BE, - SampleFormat::I24 => alsa::pcm::Format::S24BE, - SampleFormat::I32 => alsa::pcm::Format::S32BE, - // SampleFormat::I48 => alsa::pcm::Format::S48BE, - // SampleFormat::I64 => alsa::pcm::Format::S64BE, - SampleFormat::U8 => alsa::pcm::Format::U8, - SampleFormat::U16 => alsa::pcm::Format::U16BE, - SampleFormat::U24 => alsa::pcm::Format::U24BE, - SampleFormat::U32 => alsa::pcm::Format::U32BE, - // SampleFormat::U48 => alsa::pcm::Format::U48BE, - // SampleFormat::U64 => alsa::pcm::Format::U64BE, - SampleFormat::F32 => alsa::pcm::Format::FloatBE, - SampleFormat::F64 => alsa::pcm::Format::Float64BE, - sample_format => { - return Err(BackendSpecificError { - description: format!( - "Sample format '{sample_format}' is not supported by this backend" - ), - }) - } - } - } else { - match sample_format { - SampleFormat::I8 => alsa::pcm::Format::S8, - SampleFormat::I16 => alsa::pcm::Format::S16LE, - SampleFormat::I24 => alsa::pcm::Format::S24LE, - SampleFormat::I32 => alsa::pcm::Format::S32LE, - // SampleFormat::I48 => alsa::pcm::Format::S48LE, - // SampleFormat::I64 => alsa::pcm::Format::S64LE, - SampleFormat::U8 => alsa::pcm::Format::U8, - SampleFormat::U16 => alsa::pcm::Format::U16LE, - SampleFormat::U24 => alsa::pcm::Format::U24LE, - SampleFormat::U32 => alsa::pcm::Format::U32LE, - // SampleFormat::U48 => alsa::pcm::Format::U48LE, - // SampleFormat::U64 => alsa::pcm::Format::U64LE, - SampleFormat::F32 => alsa::pcm::Format::FloatLE, - SampleFormat::F64 => alsa::pcm::Format::Float64LE, - sample_format => { - return Err(BackendSpecificError { - description: format!( - "Sample format '{sample_format}' is not supported by this backend" - ), - }) - } - } - }; - - // Set the sample format, rate, and channels - if this fails, the format is not supported. - hw_params.set_format(sample_format)?; + hw_params.set_format(sample_format.try_into()?)?; hw_params.set_rate(config.sample_rate.0, alsa::ValueOr::Nearest)?; hw_params.set_channels(config.channels as u32)?; + Ok(hw_params) +} + +fn set_hw_params_from_format( + pcm_handle: &alsa::pcm::PCM, + config: &StreamConfig, + sample_format: SampleFormat, +) -> Result { + let hw_params = init_hw_params(pcm_handle, config, sample_format)?; // When BufferSize::Fixed(x) is specified, we configure double-buffering with // buffer_size = 2x and period_size = x. This provides consistent low-latency @@ -1288,10 +1247,19 @@ fn set_hw_params_from_format( // Apply hardware parameters pcm_handle.hw_params(&hw_params)?; - // For BufferSize::Default, trim the buffer to 2 periods for double-buffering + // For BufferSize::Default, constrain to device's configured period with 2-period buffering. + // PipeWire-ALSA picks a good period size but pairs it with many periods (huge buffer). + // We need to re-initialize hw_params and set BOTH period and buffer to constrain properly. if config.buffer_size == BufferSize::Default { if let Ok(period) = hw_params.get_period_size() { + // Re-initialize hw_params to clear previous constraints + let hw_params = init_hw_params(pcm_handle, config, sample_format)?; + + // Set both period (to device's chosen value) and buffer (to 2 periods) + hw_params.set_period_size_near(period as _, alsa::ValueOr::Nearest)?; hw_params.set_buffer_size_near(2 * period)?; + + // Re-apply with new constraints pcm_handle.hw_params(&hw_params)?; } } @@ -1359,6 +1327,56 @@ fn set_sw_params_from_format( Ok(period_samples) } +impl TryFrom for alsa::pcm::Format { + type Error = BackendSpecificError; + + #[cfg(target_endian = "big")] + fn try_from(sample_format: SampleFormat) -> Result { + Ok(match sample_format { + SampleFormat::I8 => alsa::pcm::Format::S8, + SampleFormat::I16 => alsa::pcm::Format::S16BE, + SampleFormat::I24 => alsa::pcm::Format::S24BE, + SampleFormat::I32 => alsa::pcm::Format::S32BE, + SampleFormat::U8 => alsa::pcm::Format::U8, + SampleFormat::U16 => alsa::pcm::Format::U16BE, + SampleFormat::U24 => alsa::pcm::Format::U24BE, + SampleFormat::U32 => alsa::pcm::Format::U32BE, + SampleFormat::F32 => alsa::pcm::Format::FloatBE, + SampleFormat::F64 => alsa::pcm::Format::Float64BE, + sample_format => { + return Err(BackendSpecificError { + description: format!( + "Sample format '{sample_format}' is not supported by this backend" + ), + }) + } + }) + } + + #[cfg(target_endian = "little")] + fn try_from(sample_format: SampleFormat) -> Result { + Ok(match sample_format { + SampleFormat::I8 => alsa::pcm::Format::S8, + SampleFormat::I16 => alsa::pcm::Format::S16LE, + SampleFormat::I24 => alsa::pcm::Format::S24LE, + SampleFormat::I32 => alsa::pcm::Format::S32LE, + SampleFormat::U8 => alsa::pcm::Format::U8, + SampleFormat::U16 => alsa::pcm::Format::U16LE, + SampleFormat::U24 => alsa::pcm::Format::U24LE, + SampleFormat::U32 => alsa::pcm::Format::U32LE, + SampleFormat::F32 => alsa::pcm::Format::FloatLE, + SampleFormat::F64 => alsa::pcm::Format::Float64LE, + sample_format => { + return Err(BackendSpecificError { + description: format!( + "Sample format '{sample_format}' is not supported by this backend" + ), + }) + } + }) + } +} + impl From for BackendSpecificError { fn from(err: alsa::Error) -> Self { BackendSpecificError { From 16915b3ee70814d7db58b2921c85b15171da72ad Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 11 Nov 2025 20:39:26 -0500 Subject: [PATCH 11/11] refactor: make ALSA frame casts explicit --- src/host/alsa/mod.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index 6f0db8e74..56df99815 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -1150,9 +1150,9 @@ impl StreamTrait for Stream { } } -// Overly safe clamp because alsa Frames are i64 +// Overly safe clamp because alsa Frames are i64 (64-bit) or i32 (32-bit) fn clamp_frame_count(buffer_size: alsa::pcm::Frames) -> FrameCount { - buffer_size.clamp(1, FrameCount::MAX as _) as _ + buffer_size.clamp(1, FrameCount::MAX as alsa::pcm::Frames) as FrameCount } fn hw_params_buffer_size_min_max(hw_params: &alsa::pcm::HwParams) -> (FrameCount, FrameCount) { @@ -1240,8 +1240,9 @@ fn set_hw_params_from_format( // buffer_size = 2x and period_size = x. This provides consistent low-latency // behavior across different ALSA implementations and hardware. if let BufferSize::Fixed(buffer_frames) = config.buffer_size { - hw_params.set_buffer_size_near((2 * buffer_frames) as _)?; - hw_params.set_period_size_near(buffer_frames as _, alsa::ValueOr::Nearest)?; + hw_params.set_buffer_size_near((2 * buffer_frames) as alsa::pcm::Frames)?; + hw_params + .set_period_size_near(buffer_frames as alsa::pcm::Frames, alsa::ValueOr::Nearest)?; } // Apply hardware parameters @@ -1256,7 +1257,7 @@ fn set_hw_params_from_format( let hw_params = init_hw_params(pcm_handle, config, sample_format)?; // Set both period (to device's chosen value) and buffer (to 2 periods) - hw_params.set_period_size_near(period as _, alsa::ValueOr::Nearest)?; + hw_params.set_period_size_near(period, alsa::ValueOr::Nearest)?; hw_params.set_buffer_size_near(2 * period)?; // Re-apply with new constraints