feat(audio): add quality limiting based on device capabilities
- Add limit_quality_to_device setting (enabled by default) - Add device_max_sample_rate field to store selected device's max rate - Implement limit_quality_for_device() to restrict quality based on device - Apply quality limiting in play_track and prefetch_track commands - Add set_audio_device_max_sample_rate Tauri command This ensures bit-perfect playback by not requesting tracks that exceed the device's sample rate capabilities. When enabled: - Devices <= 48kHz: limited to Lossless (44.1kHz) - Devices <= 96kHz: limited to HiRes (up to 96kHz) - Devices > 96kHz: allow UltraHiRes (up to 192kHz)
This commit is contained in:
1 parent
be4786e6d3
commit
6d53907233
3 files changed
+95
-3
No files matched your search
@@ -24,6 +24,49 @@ fn parse_quality(quality_str: Option<&str>) -> Quality {
|
||||
}
|
||||
}
|
||||
|
||||
/// Limit quality based on device's max sample rate
|
||||
/// This ensures bit-perfect playback by not requesting tracks that exceed device capabilities
|
||||
fn limit_quality_for_device(quality: Quality, max_sample_rate: Option<u32>) -> Quality {
|
||||
let Some(max_rate) = max_sample_rate else {
|
||||
return quality; // No limit if device max rate unknown
|
||||
};
|
||||
|
||||
// Quality mapping:
|
||||
// - UltraHiRes (27): up to 192kHz - requires max_rate > 96000
|
||||
// - HiRes (7): up to 96kHz - requires max_rate > 48000
|
||||
// - Lossless (6): 44.1kHz - works with any device
|
||||
// - Mp3 (5): compressed - works with any device
|
||||
|
||||
if max_rate <= 48000 {
|
||||
// Device only supports up to 48kHz, limit to CD quality (44.1kHz)
|
||||
match quality {
|
||||
Quality::UltraHiRes | Quality::HiRes => {
|
||||
log::info!(
|
||||
"[Quality Limit] Device max {}Hz, limiting {} to Lossless (44.1kHz)",
|
||||
max_rate, quality.display_name()
|
||||
);
|
||||
Quality::Lossless
|
||||
}
|
||||
_ => quality,
|
||||
}
|
||||
} else if max_rate <= 96000 {
|
||||
// Device supports up to 96kHz, limit to HiRes
|
||||
match quality {
|
||||
Quality::UltraHiRes => {
|
||||
log::info!(
|
||||
"[Quality Limit] Device max {}Hz, limiting Hi-Res+ to Hi-Res (96kHz)",
|
||||
max_rate
|
||||
);
|
||||
Quality::HiRes
|
||||
}
|
||||
_ => quality,
|
||||
}
|
||||
} else {
|
||||
// Device supports > 96kHz, allow all qualities
|
||||
quality
|
||||
}
|
||||
}
|
||||
|
||||
/// Result from play_track command with format info
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct PlayTrackResult {
|
||||
@@ -42,7 +85,18 @@ pub async fn play_track(
|
||||
offline_cache: State<'_, OfflineCacheState>,
|
||||
audio_settings: State<'_, AudioSettingsState>,
|
||||
) -> Result<PlayTrackResult, String> {
|
||||
let preferred_quality = parse_quality(quality.as_deref());
|
||||
let mut preferred_quality = parse_quality(quality.as_deref());
|
||||
|
||||
// Apply quality limiting if enabled
|
||||
{
|
||||
let store = audio_settings.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
if let Ok(settings) = store.get_settings() {
|
||||
if settings.limit_quality_to_device {
|
||||
preferred_quality = limit_quality_for_device(preferred_quality, settings.device_max_sample_rate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Command: play_track {} (duration: {:?}s, quality_str={:?}, parsed={:?}, format_id={})",
|
||||
track_id, duration_secs, quality, preferred_quality, preferred_quality.id()
|
||||
@@ -276,8 +330,20 @@ pub async fn prefetch_track(
|
||||
quality: Option<String>,
|
||||
state: State<'_, AppState>,
|
||||
offline_cache: State<'_, OfflineCacheState>,
|
||||
audio_settings: State<'_, AudioSettingsState>,
|
||||
) -> Result<(), String> {
|
||||
let preferred_quality = parse_quality(quality.as_deref());
|
||||
let mut preferred_quality = parse_quality(quality.as_deref());
|
||||
|
||||
// Apply quality limiting if enabled
|
||||
{
|
||||
let store = audio_settings.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
if let Ok(settings) = store.get_settings() {
|
||||
if settings.limit_quality_to_device {
|
||||
preferred_quality = limit_quality_for_device(preferred_quality, settings.device_max_sample_rate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Command: prefetch_track {} (quality_str={:?}, parsed={:?}, format_id={})",
|
||||
track_id, quality, preferred_quality, preferred_quality.id()
|
||||
|
||||
@@ -26,6 +26,9 @@ pub struct AudioSettings {
|
||||
/// This ensures bit-perfect playback by avoiding tracks that exceed device capabilities.
|
||||
/// Default: true (recommended for bit-perfect setups)
|
||||
pub limit_quality_to_device: bool,
|
||||
/// Cached max sample rate of the selected device (set when device is selected)
|
||||
/// Used when limit_quality_to_device is true
|
||||
pub device_max_sample_rate: Option<u32>,
|
||||
}
|
||||
|
||||
impl Default for AudioSettings {
|
||||
@@ -42,6 +45,7 @@ impl Default for AudioSettings {
|
||||
stream_buffer_seconds: 3, // 3 seconds initial buffer
|
||||
streaming_only: false, // Disabled by default (cache tracks for instant replay)
|
||||
limit_quality_to_device: true, // Enabled by default for bit-perfect guarantee
|
||||
device_max_sample_rate: None, // Set when device is selected
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,6 +92,7 @@ impl AudioSettingsStore {
|
||||
let _ = conn.execute("ALTER TABLE audio_settings ADD COLUMN stream_buffer_seconds INTEGER DEFAULT 3", []);
|
||||
let _ = conn.execute("ALTER TABLE audio_settings ADD COLUMN streaming_only INTEGER DEFAULT 0", []);
|
||||
let _ = conn.execute("ALTER TABLE audio_settings ADD COLUMN limit_quality_to_device INTEGER DEFAULT 1", []);
|
||||
let _ = conn.execute("ALTER TABLE audio_settings ADD COLUMN device_max_sample_rate INTEGER", []);
|
||||
|
||||
Ok(Self { conn })
|
||||
}
|
||||
@@ -95,7 +100,7 @@ impl AudioSettingsStore {
|
||||
pub fn get_settings(&self) -> Result<AudioSettings, String> {
|
||||
self.conn
|
||||
.query_row(
|
||||
"SELECT output_device, exclusive_mode, dac_passthrough, preferred_sample_rate, backend_type, alsa_plugin, alsa_hardware_volume, stream_first_track, stream_buffer_seconds, streaming_only, limit_quality_to_device FROM audio_settings WHERE id = 1",
|
||||
"SELECT output_device, exclusive_mode, dac_passthrough, preferred_sample_rate, backend_type, alsa_plugin, alsa_hardware_volume, stream_first_track, stream_buffer_seconds, streaming_only, limit_quality_to_device, device_max_sample_rate FROM audio_settings WHERE id = 1",
|
||||
[],
|
||||
|row| {
|
||||
// Parse backend_type from JSON string
|
||||
@@ -120,6 +125,7 @@ impl AudioSettingsStore {
|
||||
stream_buffer_seconds: row.get::<_, Option<i64>>(8)?.unwrap_or(3) as u8,
|
||||
streaming_only: row.get::<_, Option<i64>>(9)?.unwrap_or(0) != 0,
|
||||
limit_quality_to_device: row.get::<_, Option<i64>>(10)?.unwrap_or(1) != 0,
|
||||
device_max_sample_rate: row.get::<_, Option<i64>>(11)?.map(|r| r as u32),
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -247,6 +253,16 @@ impl AudioSettingsStore {
|
||||
.map_err(|e| format!("Failed to set limit quality to device: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_device_max_sample_rate(&self, rate: Option<u32>) -> Result<(), String> {
|
||||
self.conn
|
||||
.execute(
|
||||
"UPDATE audio_settings SET device_max_sample_rate = ?1 WHERE id = 1",
|
||||
params![rate.map(|r| r as i64)],
|
||||
)
|
||||
.map_err(|e| format!("Failed to set device max sample rate: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Thread-safe wrapper
|
||||
@@ -369,3 +385,12 @@ pub fn set_audio_limit_quality_to_device(
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
store.set_limit_quality_to_device(enabled)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_audio_device_max_sample_rate(
|
||||
state: tauri::State<'_, AudioSettingsState>,
|
||||
rate: Option<u32>,
|
||||
) -> Result<(), String> {
|
||||
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||
store.set_device_max_sample_rate(rate)
|
||||
}
|
||||
@@ -786,6 +786,7 @@ pub fn run() {
|
||||
config::audio_settings::set_audio_stream_buffer_seconds,
|
||||
config::audio_settings::set_audio_streaming_only,
|
||||
config::audio_settings::set_audio_limit_quality_to_device,
|
||||
config::audio_settings::set_audio_device_max_sample_rate,
|
||||
// Audio backend commands
|
||||
commands::get_available_backends,
|
||||
commands::get_devices_for_backend,
|
||||
|
||||
Reference in new issue
Block a user