From 5ef58f612152cb5a3011460ebb6b35d351393cec Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:15:41 -0600 Subject: [PATCH] fix: use with_buffer_size() after with_supported_config() to prevent underruns rodio 0.22's with_supported_config() resets buffer_size to Default via ..Default::default(), so our BufferSize settings in StreamConfig were never reaching CPAL. MixerDeviceSink has zero internal buffering, making CPAL's buffer the ONLY buffer between the mixer and audio hardware. Chain .with_buffer_size(Fixed(sample_rate/10)) AFTER with_supported_config() to actually pass ~100ms buffer to CPAL, matching old vendored cpal behavior. --- crates/qbz-audio/src/pipewire_backend.rs | 25 +++++++++++++++------- crates/qbz-player/src/player/mod.rs | 25 ++++++++++++++++------ src-tauri/src/player/mod.rs | 27 +++++++++++++++++------- 3 files changed, 55 insertions(+), 22 deletions(-) diff --git a/crates/qbz-audio/src/pipewire_backend.rs b/crates/qbz-audio/src/pipewire_backend.rs index 81b61fdcb..f67f8a8b7 100644 --- a/crates/qbz-audio/src/pipewire_backend.rs +++ b/crates/qbz-audio/src/pipewire_backend.rs @@ -246,17 +246,12 @@ impl AudioBackend for PipeWireBackend { ); // Create StreamConfig with desired sample rate + // Note: buffer_size here is unused — with_supported_config() resets it. + // The actual buffer size is set via with_buffer_size() below. let stream_config = StreamConfig { channels: config.channels, sample_rate: config.sample_rate, - buffer_size: if config.exclusive_mode { - BufferSize::Fixed(512) // Lower latency for exclusive mode - } else { - // PipeWire manages its own buffer scheduling via quantum; - // forcing a fixed buffer size here conflicts and causes underruns - // at high sample rates (192kHz) - BufferSize::Default - }, + buffer_size: BufferSize::Default, }; // Check if device supports this configuration @@ -296,10 +291,24 @@ impl AudioBackend for PipeWireBackend { SampleFormat::F32, ); + // Compute buffer size — must be applied AFTER with_supported_config() + // because that method resets buffer_size to Default via ..Default::default(). + // MixerDeviceSink has zero internal buffering, so CPAL's buffer is the + // ONLY buffer between the mixer and audio hardware. + let cpal_buffer_size = if config.exclusive_mode { + BufferSize::Fixed(512) // Low latency for exclusive mode + } else { + // ~100ms buffer, matching old vendored cpal period size. + // Prevents underruns at high sample rates (192kHz = 19200 frames). + BufferSize::Fixed(config.sample_rate / 10) + }; + log::info!("[PipeWire Backend] Buffer size: {:?}", cpal_buffer_size); + // Create MixerDeviceSink with custom config let mixer_sink = DeviceSinkBuilder::from_device(device) .map_err(|e| format!("Failed to create device sink builder: {}", e))? .with_supported_config(&supported_config) + .with_buffer_size(cpal_buffer_size) .open_stream() .map_err(|e| format!("Failed to create output stream at {}Hz: {}", config.sample_rate, e))?; diff --git a/crates/qbz-player/src/player/mod.rs b/crates/qbz-player/src/player/mod.rs index 5d9fbf6cc..6c6066bff 100644 --- a/crates/qbz-player/src/player/mod.rs +++ b/crates/qbz-player/src/player/mod.rs @@ -352,14 +352,12 @@ fn create_output_stream_with_config( ); // Create StreamConfig with desired sample rate + // Note: buffer_size here is unused — with_supported_config() resets it. + // The actual buffer size is set via with_buffer_size() below. let config = StreamConfig { channels, sample_rate, - buffer_size: if exclusive_mode { - BufferSize::Fixed(512) // Lower latency for exclusive mode - } else { - BufferSize::Default - }, + buffer_size: BufferSize::Default, }; // Check if device supports this configuration @@ -399,10 +397,25 @@ fn create_output_stream_with_config( SampleFormat::F32, ); + // Compute buffer size — must be applied AFTER with_supported_config() + // because that method resets buffer_size to Default via ..Default::default(). + // MixerDeviceSink has zero internal buffering, so CPAL's buffer is the + // ONLY buffer between the mixer and audio hardware. + let cpal_buffer_size = if exclusive_mode { + BufferSize::Fixed(512) // Low latency for exclusive mode + } else { + // ~100ms buffer, matching old vendored cpal period size. + // Prevents underruns at high sample rates (192kHz = 19200 frames). + BufferSize::Fixed(sample_rate / 10) + }; + log::info!("Buffer size: {:?}", cpal_buffer_size); + // Create MixerDeviceSink with custom config match DeviceSinkBuilder::from_device(device) { Ok(builder) => { - match builder.with_supported_config(&supported_config).open_stream() { + match builder.with_supported_config(&supported_config) + .with_buffer_size(cpal_buffer_size) + .open_stream() { Ok(mixer_sink) => { log::info!("MixerDeviceSink created successfully at {}Hz", sample_rate); Ok(mixer_sink) diff --git a/src-tauri/src/player/mod.rs b/src-tauri/src/player/mod.rs index 4db5c9dbb..5874b41f9 100644 --- a/src-tauri/src/player/mod.rs +++ b/src-tauri/src/player/mod.rs @@ -379,16 +379,12 @@ fn create_output_stream_with_config( ); // Create StreamConfig with desired sample rate + // Note: buffer_size here is unused — with_supported_config() resets it. + // The actual buffer size is set via with_buffer_size() below. let config = StreamConfig { channels, sample_rate, - buffer_size: if exclusive_mode { - BufferSize::Fixed(512) // Lower latency for exclusive mode - } else { - // PipeWire manages its own buffer scheduling; - // forcing a fixed size causes underruns at high sample rates - BufferSize::Default - }, + buffer_size: BufferSize::Default, }; // Check if device supports this configuration @@ -428,10 +424,25 @@ fn create_output_stream_with_config( SampleFormat::F32, ); + // Compute buffer size — must be applied AFTER with_supported_config() + // because that method resets buffer_size to Default via ..Default::default(). + // MixerDeviceSink has zero internal buffering, so CPAL's buffer is the + // ONLY buffer between the mixer and audio hardware. + let cpal_buffer_size = if exclusive_mode { + BufferSize::Fixed(512) // Low latency for exclusive mode + } else { + // ~100ms buffer, matching old vendored cpal period size. + // Prevents underruns at high sample rates (192kHz = 19200 frames). + BufferSize::Fixed(sample_rate / 10) + }; + log::info!("Buffer size: {:?}", cpal_buffer_size); + // Create MixerDeviceSink with custom config match DeviceSinkBuilder::from_device(device) { Ok(builder) => { - match builder.with_supported_config(&supported_config).open_stream() { + match builder.with_supported_config(&supported_config) + .with_buffer_size(cpal_buffer_size) + .open_stream() { Ok(mixer_sink) => { log::info!("MixerDeviceSink created successfully at {}Hz", sample_rate); Ok(mixer_sink)