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:
1 file changed
+38
-6
@@ -22,6 +22,17 @@ use std::collections::HashMap;
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::sync::Mutex;
|
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
|
/// The PipeWire sink QBZ suspended to take a device exclusively (ALSA-direct
|
||||||
/// EBUSY retry / CPAL-exclusive). Recorded by *resolved name* so
|
/// EBUSY retry / CPAL-exclusive). Recorded by *resolved name* so
|
||||||
/// `resume_suspended_sink` wakes the exact sink that was suspended — PipeWire
|
/// `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))?;
|
.map_err(|e| format!("Failed to get supported configs: {}", e))?;
|
||||||
|
|
||||||
let mut found_matching = false;
|
let mut found_matching = false;
|
||||||
|
let mut buffer_bounds = SupportedBufferSize::Unknown;
|
||||||
for range in supported_configs {
|
for range in supported_configs {
|
||||||
if range.channels() == config.channels
|
if range.channels() == config.channels
|
||||||
&& config.sample_rate >= range.min_sample_rate()
|
&& config.sample_rate >= range.min_sample_rate()
|
||||||
&& config.sample_rate <= range.max_sample_rate()
|
&& config.sample_rate <= range.max_sample_rate()
|
||||||
{
|
{
|
||||||
found_matching = true;
|
found_matching = true;
|
||||||
|
buffer_bounds = *range.buffer_size();
|
||||||
log::info!(
|
log::info!(
|
||||||
"[ALSA Backend] Device supports {}Hz (range: {}-{}Hz)",
|
"[ALSA Backend] Device supports {}Hz (range: {}-{}Hz)",
|
||||||
config.sample_rate,
|
config.sample_rate,
|
||||||
@@ -1357,11 +1370,9 @@ impl AudioBackend for AlsaBackend {
|
|||||||
let stream_config = StreamConfig {
|
let stream_config = StreamConfig {
|
||||||
channels: config.channels,
|
channels: config.channels,
|
||||||
sample_rate: effective_rate,
|
sample_rate: effective_rate,
|
||||||
buffer_size: if config.exclusive_mode {
|
buffer_size: BufferSize::Fixed(playback_period_frames(
|
||||||
BufferSize::Fixed(512)
|
effective_rate, config.exclusive_mode, &buffer_bounds,
|
||||||
} else {
|
)),
|
||||||
BufferSize::Fixed(effective_rate / 10)
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create SupportedStreamConfig
|
// Create SupportedStreamConfig
|
||||||
@@ -1382,7 +1393,11 @@ impl AudioBackend for AlsaBackend {
|
|||||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
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)
|
let mixer_sink = DeviceSinkBuilder::from_device(device)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
if config.exclusive_mode {
|
if config.exclusive_mode {
|
||||||
@@ -1395,6 +1410,10 @@ impl AudioBackend for AlsaBackend {
|
|||||||
}
|
}
|
||||||
})?
|
})?
|
||||||
.with_supported_config(&supported_config)
|
.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()
|
.open_stream()
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
if config.exclusive_mode {
|
if config.exclusive_mode {
|
||||||
@@ -1443,6 +1462,19 @@ impl AudioBackend for AlsaBackend {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn build_hw_fallback_id_rewrites_iec958_alias() {
|
fn build_hw_fallback_id_rewrites_iec958_alias() {
|
||||||
// The exact case from issue #331 — HifiBerry Digi2 Pro on RPi OS.
|
// The exact case from issue #331 — HifiBerry Digi2 Pro on RPi OS.
|
||||||
|
|||||||
Reference in new issue
Block a user