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.
This commit is contained in:
vicrodh committed 2026-02-28 07:48:03 -06:00
1 parent b054f51410
commit 5ef58f6121
3 files changed
+55 -22

No files matched your search

+17 -8
View File
@@ -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))?;
+19 -6
View File
@@ -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)
+19 -8
View File
@@ -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)