fix(audio): fallback to plughw instead of CPAL when DAC rate unsupported (#288)

When /proc/asound reports that the selected hw: device does not
natively support the track's sample rate (e.g., 96 kHz track on a
48 kHz-only Vanatoo Transparent Zero), the previous code returned
None from try_create_direct_stream. The caller then dispatched to
CPAL's create_output_stream, where the CPAL ALSA host could not
locate the device by its /proc/asound name and failed with the
misleading error 'Device not found'.

The same function already has a plughw: fallback path further down
that wraps the hw device with ALSA's plug plugin — the latter
resamples in ALSA space and is the correct behavior for rate
mismatches. This change makes the /proc/asound pre-check set a
local flag that skips the hw attempt, falling through to the
existing plughw branch instead of returning None.

Sound now comes out. Bit-perfect is lost (by necessity) and the
mode reported back to the player is PluginFallback, which lets the
UI surface the degradation (wired up in Fix 4).

cc
This commit is contained in:
vicrodh committed 2026-04-13 19:17:59 -06:00
1 parent d7c8119953
commit cba5b76bb3
1 file changed
+23 -11
+23 -11
View File
@@ -564,33 +564,45 @@ impl AlsaBackend {
// before opening the PCM device. This avoids the "device busy" issue
// where ALSA Direct opens the device, fails to configure the rate, and
// leaves the device in a state CPAL can't open afterwards.
//
// When the rate is unsupported, we skip the hw: attempt and fall through
// to the plughw: path below, which lets ALSA auto-resample in kernel/
// userspace. Falling back to CPAL is not an option because CPAL's device
// enumeration does not reliably expose raw hw: devices by the same name
// the UI stored (front:CARD=X,DEV=Y vs hw:CARD=X,DEV=Y), and the lookup
// fails with a misleading "Device not found" error. See issue #288.
let mut hw_rate_unsupported = false;
if let Some(card_name) = extract_card_name_from_device(device_id) {
if let Some(hw_rates) = get_hw_supported_rates(&card_name) {
if !hw_rates.contains(&config.sample_rate) {
log::info!(
"[ALSA Backend] Hardware rates for '{}': {:?}. {}Hz not supported, skipping ALSA Direct",
"[ALSA Backend] Hardware rates for '{}': {:?}. {}Hz not supported natively — falling back to plughw for software resample",
card_name, hw_rates, config.sample_rate
);
return None;
hw_rate_unsupported = true;
} else {
log::info!(
"[ALSA Backend] Hardware confirms support for {}Hz (card '{}', rates: {:?})",
config.sample_rate,
card_name,
hw_rates
);
}
log::info!(
"[ALSA Backend] Hardware confirms support for {}Hz (card '{}', rates: {:?})",
config.sample_rate,
card_name,
hw_rates
);
}
}
// Respect ALSA plugin selection from settings
// Respect ALSA plugin selection from settings. When /proc/asound already
// told us the hw device won't accept the rate, skip straight to plughw
// even if the user prefers Hw — plughw with resample is the only path
// that produces sound.
let try_hw_first = match config.alsa_plugin {
Some(AlsaPlugin::Hw) => true,
Some(AlsaPlugin::Hw) => !hw_rate_unsupported,
Some(AlsaPlugin::PlugHw) => false, // Skip hw, go directly to plughw
Some(AlsaPlugin::Pcm) => {
log::info!("[ALSA Backend] PCM mode selected, not using direct ALSA");
return None; // Use CPAL instead
}
None => true, // Default: try hw first
None => !hw_rate_unsupported, // Default: try hw first if rate is supported
};
if try_hw_first {