fix(audio): apply rate-scaled ALSA callback buffering

Apply the period after Rodio resets stream configuration, respect device bounds, and timestamp stream errors.

cx
This commit is contained in:
vicrodh committed 2026-09-08 20:54:37 -06:00
1 parent b2ca4d7be6
commit e0755fe2d6
1 file changed
+38 -6
+38 -6
View File
@@ -22,6 +22,17 @@ use std::collections::HashMap;
use std::fs;
use std::sync::Mutex;
/// Keep scheduling headroom in milliseconds as hi-res rates increase. CPAL
/// uses this as one period and allocates two periods on ALSA. Respect the
/// driver's actual bounds, not the synthetic SupportedStreamConfig below.
fn playback_period_frames(rate: u32, exclusive: bool, bounds: &SupportedBufferSize) -> u32 {
let target = (rate / if exclusive { 20 } else { 10 }).max(1);
match *bounds {
SupportedBufferSize::Range { min, max } if min > 0 && max >= min => target.clamp(min, max),
_ => target,
}
}
/// The PipeWire sink QBZ suspended to take a device exclusively (ALSA-direct
/// EBUSY retry / CPAL-exclusive). Recorded by *resolved name* so
/// `resume_suspended_sink` wakes the exact sink that was suspended — PipeWire
@@ -1317,12 +1328,14 @@ impl AudioBackend for AlsaBackend {
.map_err(|e| format!("Failed to get supported configs: {}", e))?;
let mut found_matching = false;
let mut buffer_bounds = SupportedBufferSize::Unknown;
for range in supported_configs {
if range.channels() == config.channels
&& config.sample_rate >= range.min_sample_rate()
&& config.sample_rate <= range.max_sample_rate()
{
found_matching = true;
buffer_bounds = *range.buffer_size();
log::info!(
"[ALSA Backend] Device supports {}Hz (range: {}-{}Hz)",
config.sample_rate,
@@ -1357,11 +1370,9 @@ impl AudioBackend for AlsaBackend {
let stream_config = StreamConfig {
channels: config.channels,
sample_rate: effective_rate,
buffer_size: if config.exclusive_mode {
BufferSize::Fixed(512)
} else {
BufferSize::Fixed(effective_rate / 10)
},
buffer_size: BufferSize::Fixed(playback_period_frames(
effective_rate, config.exclusive_mode, &buffer_bounds,
)),
};
// Create SupportedStreamConfig
@@ -1382,7 +1393,11 @@ impl AudioBackend for AlsaBackend {
std::thread::sleep(std::time::Duration::from_millis(200));
}
// Create MixerDeviceSink with custom config
log::info!(
"[ALSA Backend] Callback period: {:?} at {}Hz (driver bounds {:?})",
stream_config.buffer_size, effective_rate, buffer_bounds
);
// with_supported_config resets buffer_size, so apply the period last.
let mixer_sink = DeviceSinkBuilder::from_device(device)
.map_err(|e| {
if config.exclusive_mode {
@@ -1395,6 +1410,10 @@ impl AudioBackend for AlsaBackend {
}
})?
.with_supported_config(&supported_config)
.with_buffer_size(stream_config.buffer_size)
.with_error_callback(move |err| {
log::warn!("[ALSA Backend] Stream error at {}Hz: {}", effective_rate, err);
})
.open_stream()
.map_err(|e| {
if config.exclusive_mode {
@@ -1443,6 +1462,19 @@ impl AudioBackend for AlsaBackend {
mod tests {
use super::*;
#[test]
fn playback_period_retains_time_budget_at_hi_res_and_obeys_device_bounds() {
let wide = SupportedBufferSize::Range { min: 64, max: 65536 };
for rate in [44100, 48000, 96000, 192000, 384000] {
assert_eq!(playback_period_frames(rate, true, &wide) * 20, rate);
assert_eq!(playback_period_frames(rate, false, &wide) * 10, rate);
}
let narrow = SupportedBufferSize::Range { min: 256, max: 4096 };
assert_eq!(playback_period_frames(192000, true, &narrow), 4096);
assert_eq!(playback_period_frames(4000, true, &narrow), 256);
assert_eq!(playback_period_frames(192000, true, &SupportedBufferSize::Unknown), 9600);
}
#[test]
fn build_hw_fallback_id_rewrites_iec958_alias() {
// The exact case from issue #331 — HifiBerry Digi2 Pro on RPi OS.