From 39e8814848d77e5f0e581b861d588f2909ceef16 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Sun, 1 Feb 2026 19:23:56 -0600 Subject: [PATCH] feat(settings): add limit quality to device toggle and max sample rate tracking Frontend changes for quality limiting feature: - Add limitQualityToDevice state and handler - Add AudioSettings interface fields for limit_quality_to_device and device_max_sample_rate - Update handleBackendDeviceChange to send max_sample_rate when device changes - Update handleOutputDeviceChange to find max_sample_rate from backend devices - Add Settings UI toggle for "Limit Quality to Device" This completes the quality limiting feature that prevents requesting tracks that exceed the device's sample rate capabilities, ensuring bit-perfect playback. --- src-tauri/src/commands/playback.rs | 2 +- src/lib/components/views/SettingsView.svelte | 37 ++++++++++++++++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/commands/playback.rs b/src-tauri/src/commands/playback.rs index ff473f48a..371c71be3 100644 --- a/src-tauri/src/commands/playback.rs +++ b/src-tauri/src/commands/playback.rs @@ -43,7 +43,7 @@ fn limit_quality_for_device(quality: Quality, max_sample_rate: Option) -> Q Quality::UltraHiRes | Quality::HiRes => { log::info!( "[Quality Limit] Device max {}Hz, limiting {} to Lossless (44.1kHz)", - max_rate, quality.display_name() + max_rate, quality.label() ); Quality::Lossless } diff --git a/src/lib/components/views/SettingsView.svelte b/src/lib/components/views/SettingsView.svelte index 80cea4dbe..5535fccb7 100644 --- a/src/lib/components/views/SettingsView.svelte +++ b/src/lib/components/views/SettingsView.svelte @@ -389,6 +389,7 @@ let streamFirstTrack = $state(false); let streamBufferSeconds = $state(3); let streamingOnly = $state(false); + let limitQualityToDevice = $state(true); // Enabled by default for bit-perfect // Backend system state let availableBackends = $state([]); @@ -1085,6 +1086,8 @@ stream_first_track: boolean; stream_buffer_seconds: number; streaming_only: boolean; + limit_quality_to_device: boolean; + device_max_sample_rate: number | null; } interface BackendInfo { @@ -1236,6 +1239,7 @@ streamFirstTrack = settings.stream_first_track ?? false; streamBufferSeconds = settings.stream_buffer_seconds ?? 3; streamingOnly = settings.streaming_only ?? false; + limitQualityToDevice = settings.limit_quality_to_device ?? true; // Validate mutual exclusion: DAC Passthrough disables Gapless + Crossfade if (dacPassthrough) { @@ -1260,16 +1264,25 @@ const deviceName = sinkDescriptionToName.get(description); const deviceToStore = description === 'System Default' ? null : deviceName; + // Try to find max sample rate from backendDevices if available + // This enables quality limiting for PipeWire mode when possible + const matchingDevice = backendDevices.find(d => + d.name === deviceName || d.description === description + ); + const maxSampleRate = matchingDevice?.max_sample_rate ?? null; + try { // Save the preference await invoke('set_audio_output_device', { device: deviceToStore }); + // Store device's max sample rate for quality limiting + await invoke('set_audio_device_max_sample_rate', { rate: maxSampleRate }); // Reinitialize audio with the selected device // CRITICAL: Pass the actual CPAL device name, not null // CPAL can now find this device because we're using CPAL names await invoke('reinit_audio_device', { device: deviceToStore }); - console.log('[Audio] Output device changed:', description, '(device:', deviceName ?? 'default', ')'); + console.log('[Audio] Output device changed:', description, '(device:', deviceName ?? 'default', ', max_rate:', maxSampleRate ?? 'unknown', ')'); } catch (err) { console.error('[Audio] Failed to change audio output device:', err); } @@ -1434,17 +1447,30 @@ } } + async function handleLimitQualityToDeviceChange(enabled: boolean) { + limitQualityToDevice = enabled; + try { + await invoke('set_audio_limit_quality_to_device', { enabled }); + console.log('[Audio] Limit quality to device changed:', enabled); + } catch (err) { + console.error('[Audio] Failed to change limit quality to device:', err); + } + } + async function handleBackendDeviceChange(deviceName: string) { outputDevice = deviceName; // Get device ID from backendDevices using display name mapping const device = deviceByDisplayName.get(deviceName); const deviceId = deviceName === 'System Default' ? null : device?.id ?? null; + const maxSampleRate = device?.max_sample_rate ?? null; try { await invoke('set_audio_output_device', { device: deviceId }); + // Store device's max sample rate for quality limiting + await invoke('set_audio_device_max_sample_rate', { rate: maxSampleRate }); await invoke('reinit_audio_device', { device: deviceId }); - console.log('[Audio] Backend device changed:', deviceName, '(id:', deviceId ?? 'default', ')'); + console.log('[Audio] Backend device changed:', deviceName, '(id:', deviceId ?? 'default', ', max_rate:', maxSampleRate ?? 'unknown', ')'); // Stop current playback to prevent stuck/dead streams try { @@ -2064,6 +2090,13 @@ +
+
+ Limit Quality to Device + Prevents requesting tracks that exceed your device's max sample rate. Ensures bit-perfect playback by avoiding software resampling. +
+ +
{$t('settings.audio.currentSampleRate')}