fix(audio): downgrade track quality when DAC rate unsupported (#288)

Previously the quality downgrade only ran when the requested quality
was UltraHiRes and only tried a single step down to HiRes. Since Qobuz
returns up to 96 kHz content for both UltraHiRes and HiRes tiers, that
single step was a no-op for DACs that top out at 48 kHz (e.g. Vanatoo
Transparent Zero, many entry-level USB DACs): playback still failed
with the misleading 'Device not found' error.

This change iterates down the quality ladder after checking the
actual stream sample rate, continuing to Lossless (44.1 kHz) when
HiRes still produces an unsupported rate. Applies to both the current
track (v2_play_track_with_options) and prefetched tracks
(spawn_v2_prefetch).

Lossy MP3 is never selected automatically — if even Lossless is
unsupported, the stream continues at the original rate and Fix 2
(software resample fallback) takes over.

cc
This commit is contained in:
vicrodh committed 2026-04-13 19:12:32 -06:00
1 parent 088d6b6f41
commit d7c8119953
1 file changed
+86 -32
+86 -32
View File
@@ -149,24 +149,54 @@ fn spawn_v2_prefetch_with_hw_check(
}
};
// Determine effective quality (may be downgraded for hardware compatibility)
// Determine effective quality (may be downgraded for hardware compatibility).
// Iterates down the quality ladder until the returned stream sample rate is
// something the hardware can play. Both UltraHiRes and HiRes can yield
// 96 kHz streams, so a single UltraHiRes→HiRes step is not enough for
// DACs that top out at 48 kHz — we continue to Lossless (44.1 kHz) if needed.
let effective_quality = {
let mut eq = quality;
#[cfg(target_os = "linux")]
if let Some(ref device_id) = hw_device_clone {
if quality == Quality::UltraHiRes {
let bridge_guard = bridge_clone.read().await;
if let Some(bridge) = bridge_guard.as_ref() {
if let Ok(stream_url) = bridge.get_stream_url(track_id, quality).await {
let track_rate = (stream_url.sampling_rate * 1000.0) as u32;
if qbz_audio::device_supports_sample_rate(device_id, track_rate)
== Some(false)
{
log::info!(
"[V2/PREFETCH] Track {} at {}Hz incompatible with hardware, prefetching at Hi-Res",
track_id, track_rate
);
eq = Quality::HiRes;
let bridge_guard = bridge_clone.read().await;
if let Some(bridge) = bridge_guard.as_ref() {
if let Ok(initial_url) = bridge.get_stream_url(track_id, quality).await {
let track_rate = (initial_url.sampling_rate * 1000.0) as u32;
if qbz_audio::device_supports_sample_rate(device_id, track_rate)
== Some(false)
{
for try_quality in [Quality::HiRes, Quality::Lossless] {
if try_quality >= quality {
continue;
}
match bridge.get_stream_url(track_id, try_quality).await {
Ok(alt_url) => {
let alt_rate =
(alt_url.sampling_rate * 1000.0) as u32;
if qbz_audio::device_supports_sample_rate(
device_id, alt_rate,
) != Some(false)
{
log::info!(
"[V2/PREFETCH] Track {} at {}Hz incompatible with hardware, prefetching at {:?} ({}Hz)",
track_id,
track_rate,
try_quality,
alt_rate
);
eq = try_quality;
break;
}
}
Err(e) => {
log::debug!(
"[V2/PREFETCH] Failed to probe {:?} for track {}: {}",
try_quality,
track_id,
e
);
}
}
}
}
}
@@ -1040,25 +1070,49 @@ pub async fn v2_play_track(
);
// Smart quality downgrade for ALSA Direct: if the hardware doesn't support
// the track's sample rate, re-request at a lower quality that IS supported.
// This avoids resampling and keeps bit-perfect playback.
// the track's sample rate, re-request at progressively lower qualities until
// one returns a stream the DAC can play. Both UltraHiRes and HiRes may yield
// 96 kHz content, so a single downgrade step is not always enough (e.g., a
// 48 kHz-only DAC requires falling through to Lossless at 44.1 kHz).
if let Some(ref device_id) = hw_device_id {
if final_quality == Quality::UltraHiRes {
let track_rate = (stream_url.sampling_rate * 1000.0) as u32;
if qbz_audio::device_supports_sample_rate(device_id, track_rate) == Some(false) {
log::info!(
"[V2/Quality] Hardware doesn't support {}Hz, downgrading to Hi-Res",
track_rate
);
stream_url = bridge_guard
.get_stream_url(track_id, Quality::HiRes)
.await
.map_err(RuntimeError::Internal)?;
log::info!(
"[V2/Quality] Got fallback stream URL (format_id={}, rate={}kHz)",
stream_url.format_id,
stream_url.sampling_rate
);
let track_rate = (stream_url.sampling_rate * 1000.0) as u32;
if qbz_audio::device_supports_sample_rate(device_id, track_rate) == Some(false) {
log::info!(
"[V2/Quality] Hardware doesn't support {}Hz, searching for compatible quality tier",
track_rate
);
for try_quality in [Quality::HiRes, Quality::Lossless] {
if try_quality >= final_quality {
continue;
}
match bridge_guard.get_stream_url(track_id, try_quality).await {
Ok(alt_url) => {
let alt_rate = (alt_url.sampling_rate * 1000.0) as u32;
if qbz_audio::device_supports_sample_rate(device_id, alt_rate)
!= Some(false)
{
log::info!(
"[V2/Quality] Falling back to {:?} ({}kHz) for hardware compatibility",
try_quality,
alt_url.sampling_rate
);
stream_url = alt_url;
break;
}
log::debug!(
"[V2/Quality] {:?} returns {}Hz — still incompatible, trying next tier",
try_quality,
alt_rate
);
}
Err(e) => {
log::warn!(
"[V2/Quality] Failed to probe {:?} stream URL: {}",
try_quality,
e
);
}
}
}
}
}