feat(settings): make sync audio on startup opt-in

The reload_settings() call at bootstrap is now gated behind a new
sync_audio_on_startup setting (default OFF). Most users don't need
it; it exists for edge cases where Player::new() holds stale settings
(e.g., after Flatpak updates changing DB schema).

Adds toggle in Settings > Audio with i18n for all 4 locales.
This commit is contained in:
vicrodh committed 2026-02-22 19:54:12 -06:00
1 parent 0dea116c69
commit 0ee8b9db0e
8 files changed
+92 -12

No files matched your search

+39 -10
View File
@@ -568,16 +568,26 @@ pub async fn runtime_bootstrap(
return Err(RuntimeError::RuntimeDegraded(reason));
}
// Step 6: Sync audio settings once after session activation.
// Per-user stores are now ready; reload settings so the audio
// thread has fresh DB values for DAC passthrough, backend, etc.
if let Some(bridge) = core_bridge.try_get().await {
let player = bridge.player();
if let Ok(guard) = audio_settings.store.lock() {
if let Some(store) = guard.as_ref() {
if let Ok(fresh) = store.get_settings() {
log::info!("[Runtime] Syncing audio settings to player after session activation");
let _ = player.reload_settings(convert_to_qbz_audio_settings(&fresh));
// Step 6: Optionally sync audio settings after session activation.
// Only runs if user has enabled sync_audio_on_startup (opt-in).
// Useful when Player::new() may hold stale settings (e.g., Flatpak updates).
let should_sync = audio_settings
.store
.lock()
.ok()
.and_then(|g| g.as_ref().and_then(|s| s.get_settings().ok()))
.map(|s| s.sync_audio_on_startup)
.unwrap_or(false);
if should_sync {
if let Some(bridge) = core_bridge.try_get().await {
let player = bridge.player();
if let Ok(guard) = audio_settings.store.lock() {
if let Some(store) = guard.as_ref() {
if let Ok(fresh) = store.get_settings() {
log::info!("[Runtime] Syncing audio settings to player (sync_audio_on_startup=true)");
let _ = player.reload_settings(convert_to_qbz_audio_settings(&fresh));
}
}
}
}
@@ -6395,6 +6405,25 @@ pub fn v2_set_audio_pw_force_bitperfect(
.map_err(RuntimeError::Internal)
}
/// Set sync audio settings on startup (V2)
#[tauri::command]
pub fn v2_set_sync_audio_on_startup(
enabled: bool,
state: State<'_, AudioSettingsState>,
) -> Result<(), RuntimeError> {
log::info!("[V2] set_sync_audio_on_startup: {}", enabled);
let guard = state
.store
.lock()
.map_err(|e| RuntimeError::Internal(format!("Lock error: {}", e)))?;
let store = guard
.as_ref()
.ok_or(RuntimeError::UserSessionNotActivated)?;
store
.set_sync_audio_on_startup(enabled)
.map_err(RuntimeError::Internal)
}
/// Set preferred sample rate (V2)
#[tauri::command]
pub fn v2_set_audio_sample_rate(
+24 -2
View File
@@ -47,6 +47,10 @@ pub struct AudioSettings {
/// When true, force PipeWire clock.force-quantum alongside clock.force-rate for bit-perfect.
/// Reset both to 0 on stop. PipeWire-only, requires dac_passthrough.
pub pw_force_bitperfect: bool,
/// When true, reload audio settings from DB into the player on app startup.
/// Useful when Player::new() may hold stale settings (e.g., after Flatpak updates).
/// Default: false (most users don't need this).
pub sync_audio_on_startup: bool,
}
impl Default for AudioSettings {
@@ -69,6 +73,7 @@ impl Default for AudioSettings {
normalization_target_lufs: -14.0, // Spotify/YouTube standard
gapless_enabled: false, // Off by default — user opts in
pw_force_bitperfect: false, // Off by default — experimental PipeWire feature
sync_audio_on_startup: false, // Off by default — opt-in for stale-settings edge case
}
}
}
@@ -157,6 +162,10 @@ impl AudioSettingsStore {
"ALTER TABLE audio_settings ADD COLUMN pw_force_bitperfect INTEGER DEFAULT 0",
[],
);
let _ = conn.execute(
"ALTER TABLE audio_settings ADD COLUMN sync_audio_on_startup INTEGER DEFAULT 0",
[],
);
Ok(Self { conn })
}
@@ -175,7 +184,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, device_max_sample_rate, normalization_enabled, normalization_target_lufs, gapless_enabled, device_sample_rate_limits, pw_force_bitperfect 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, normalization_enabled, normalization_target_lufs, gapless_enabled, device_sample_rate_limits, pw_force_bitperfect, sync_audio_on_startup FROM audio_settings WHERE id = 1",
[],
|row| {
// Parse backend_type from JSON string
@@ -212,6 +221,7 @@ impl AudioSettingsStore {
normalization_target_lufs: row.get::<_, Option<f64>>(13)?.unwrap_or(-14.0) as f32,
gapless_enabled: row.get::<_, Option<i64>>(14)?.unwrap_or(0) != 0,
pw_force_bitperfect: row.get::<_, Option<i64>>(16)?.unwrap_or(0) != 0,
sync_audio_on_startup: row.get::<_, Option<i64>>(17)?.unwrap_or(0) != 0,
})
},
)
@@ -435,6 +445,16 @@ impl AudioSettingsStore {
Ok(())
}
pub fn set_sync_audio_on_startup(&self, enabled: bool) -> Result<(), String> {
self.conn
.execute(
"UPDATE audio_settings SET sync_audio_on_startup = ?1 WHERE id = 1",
params![enabled as i64],
)
.map_err(|e| format!("Failed to set sync_audio_on_startup: {}", e))?;
Ok(())
}
pub fn set_normalization_target_lufs(&self, target: f32) -> Result<(), String> {
self.conn
.execute(
@@ -482,7 +502,8 @@ impl AudioSettingsStore {
normalization_target_lufs = ?14,
gapless_enabled = ?15,
device_sample_rate_limits = ?16,
pw_force_bitperfect = ?17
pw_force_bitperfect = ?17,
sync_audio_on_startup = ?18
WHERE id = 1",
params![
defaults.output_device,
@@ -502,6 +523,7 @@ impl AudioSettingsStore {
defaults.gapless_enabled as i64,
limits_json,
defaults.pw_force_bitperfect as i64,
defaults.sync_audio_on_startup as i64,
],
)
.map_err(|e| format!("Failed to reset audio settings: {}", e))?;
+1
View File
@@ -1004,6 +1004,7 @@ pub fn run() {
commands_v2::v2_set_audio_exclusive_mode,
commands_v2::v2_set_audio_dac_passthrough,
commands_v2::v2_set_audio_pw_force_bitperfect,
commands_v2::v2_set_sync_audio_on_startup,
commands_v2::v2_set_audio_sample_rate,
commands_v2::v2_set_audio_backend_type,
commands_v2::v2_set_audio_alsa_plugin,
@@ -587,6 +587,7 @@
let exclusiveMode = $state(false);
let dacPassthrough = $state(false);
let pwForceBitperfect = $state(false);
let syncAudioOnStartup = $state(false);
let selectedBackend = $state<string>('Auto');
let selectedAlsaPlugin = $state<string>('hw (Direct Hardware)');
let alsaHardwareVolume = $state(false);
@@ -2088,6 +2089,7 @@
device_max_sample_rate: number | null;
gapless_enabled: boolean;
pw_force_bitperfect: boolean;
sync_audio_on_startup: boolean;
}
interface BackendInfo {
@@ -2233,6 +2235,7 @@
exclusiveMode = settings.exclusive_mode;
dacPassthrough = settings.dac_passthrough;
pwForceBitperfect = settings.pw_force_bitperfect;
syncAudioOnStartup = settings.sync_audio_on_startup;
// Load backend and plugin settings
if (settings.backend_type) {
@@ -2389,6 +2392,16 @@
}
}
async function handleSyncAudioOnStartupChange(enabled: boolean) {
syncAudioOnStartup = enabled;
try {
await invoke('v2_set_sync_audio_on_startup', { enabled });
console.log('[Audio] Sync audio on startup changed:', enabled);
} catch (err) {
console.error('[Audio] Failed to change sync audio on startup:', err);
}
}
async function handleBackendChange(backendName: string) {
selectedBackend = backendName;
@@ -3434,6 +3447,13 @@
<small class="setting-note">{$t('settings.audio.pwForceBitperfectNote')}</small>
{/if}
{/if}
<div class="setting-row">
<div class="setting-info">
<span class="setting-label">{$t('settings.audio.syncAudioOnStartup')}</span>
<span class="setting-desc">{$t('settings.audio.syncAudioOnStartupDesc')}</span>
</div>
<Toggle enabled={syncAudioOnStartup} onchange={handleSyncAudioOnStartupChange} />
</div>
<div class="setting-row">
<span class="setting-label">{$t('settings.audio.currentSampleRate')}</span>
<span class="setting-value" class:muted={!hardwareStatus?.is_active}>
+2
View File
@@ -566,6 +566,8 @@
"pwForceBitperfect": "Bit-Perfect erzwingen (Experimentell)",
"pwForceBitperfectDesc": "Erzwingt PipeWire-Quantum und Abtastrate passend zum Track, Zurücksetzen beim Stoppen. Erfordert DAC-Passthrough.",
"pwForceBitperfectNote": "Experimentell. Kann auf einigen Systemen Audio-Artefakte verursachen. Betrifft nur das PipeWire-Backend.",
"syncAudioOnStartup": "Audio-Einstellungen beim Start synchronisieren",
"syncAudioOnStartupDesc": "Audiokonfiguration beim App-Start aus der Datenbank neu laden. Aktiviere dies, wenn dein Audiogerät oder deine Einstellungen nach dem Starten falsch erscheinen.",
"audioBackend": "Audio-Backend",
"audioBackendDesc": "Wähle Audiosystem: Auto (empfohlen), PipeWire (modern), ALSA Direct (bitperfekt, exklusiv) oder PulseAudio (veraltet).",
"currentSampleRate": "Aktuelle Abtastrate",
+2
View File
@@ -583,6 +583,8 @@
"pwForceBitperfect": "Force Bit-Perfect (Experimental)",
"pwForceBitperfectDesc": "Forces PipeWire quantum and sample rate to match the track, resetting on stop. Requires DAC Passthrough.",
"pwForceBitperfectNote": "Experimental. May cause audio glitches on some systems. Only affects PipeWire backend.",
"syncAudioOnStartup": "Sync audio settings on startup",
"syncAudioOnStartupDesc": "Reload audio configuration from database when the app starts. Enable if your audio device or settings appear incorrect after launching.",
"audioBackend": "Audio Backend",
"audioBackendDesc": "Choose audio system: Auto (recommended), PipeWire (modern), ALSA Direct (bit-perfect, exclusive), or PulseAudio (legacy).",
"currentSampleRate": "Current Sample Rate",
+2
View File
@@ -583,6 +583,8 @@
"pwForceBitperfect": "Forzar Bit-Perfect (Experimental)",
"pwForceBitperfectDesc": "Fuerza el quantum y la tasa de muestreo de PipeWire para coincidir con la pista, restableciendo al detener. Requiere Passthrough DAC.",
"pwForceBitperfectNote": "Experimental. Puede causar artefactos de audio en algunos sistemas. Solo afecta al backend PipeWire.",
"syncAudioOnStartup": "Sincronizar configuración de audio al iniciar",
"syncAudioOnStartupDesc": "Recarga la configuración de audio desde la base de datos al iniciar la app. Actívalo si tu dispositivo o configuración de audio aparecen incorrectos al abrir.",
"audioBackend": "Backend de Audio",
"audioBackendDesc": "Elige el sistema de audio: Auto (recomendado), PipeWire (moderno), ALSA Direct (bit-perfect, exclusivo), o PulseAudio (legacy).",
"currentSampleRate": "Tasa de Muestreo Actual",
+2
View File
@@ -566,6 +566,8 @@
"pwForceBitperfect": "Forcer Bit-Perfect (Expérimental)",
"pwForceBitperfectDesc": "Force le quantum et le taux d'échantillonnage PipeWire pour correspondre à la piste, réinitialisation à l'arrêt. Nécessite le Passthrough DAC.",
"pwForceBitperfectNote": "Expérimental. Peut causer des artefacts audio sur certains systèmes. N'affecte que le backend PipeWire.",
"syncAudioOnStartup": "Synchroniser la configuration audio au démarrage",
"syncAudioOnStartupDesc": "Recharge la configuration audio depuis la base de données au démarrage de l'application. Activez si votre appareil ou vos paramètres audio semblent incorrects après le lancement.",
"audioBackend": "Backend audio",
"audioBackendDesc": "Choisissez le système audio : Auto (recommandé), PipeWire (moderne), ALSA Direct (bit-perfect, exclusif) ou PulseAudio (ancien).",
"currentSampleRate": "Taux d'échantillonnage actuel",