Add audio settings storage and commands

Backend:
- Create config/audio_settings.rs with SQLite persistence
- Store output_device, exclusive_mode, dac_passthrough, sample_rate
- Add Tauri commands: get_audio_settings, set_audio_*
- Register state and commands in lib.rs

Note: Player modification to USE these settings is pending.
See HANDOFF.md for implementation details.
This commit is contained in:
vicrodh committed 2026-01-11 03:46:21 -06:00
1 parent ddc9497286
commit 29079458cf
8 files changed
+220 -2

No files matched your search

+163
View File
@@ -0,0 +1,163 @@
//! Audio settings persistence
//!
//! Stores user preferences for audio output device, exclusive mode, and DAC passthrough.
use rusqlite::{Connection, params};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AudioSettings {
pub output_device: Option<String>, // None = system default
pub exclusive_mode: bool,
pub dac_passthrough: bool,
pub preferred_sample_rate: Option<u32>, // None = auto
}
pub struct AudioSettingsStore {
conn: Connection,
}
impl AudioSettingsStore {
pub fn new() -> Result<Self, String> {
let data_dir = dirs::data_dir()
.ok_or("Could not determine data directory")?
.join("qbz-nix");
std::fs::create_dir_all(&data_dir)
.map_err(|e| format!("Failed to create data directory: {}", e))?;
let db_path = data_dir.join("audio_settings.db");
let conn = Connection::open(&db_path)
.map_err(|e| format!("Failed to open audio settings database: {}", e))?;
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS audio_settings (
id INTEGER PRIMARY KEY CHECK (id = 1),
output_device TEXT,
exclusive_mode INTEGER NOT NULL DEFAULT 0,
dac_passthrough INTEGER NOT NULL DEFAULT 0,
preferred_sample_rate INTEGER
);
INSERT OR IGNORE INTO audio_settings (id, exclusive_mode, dac_passthrough)
VALUES (1, 0, 0);"
).map_err(|e| format!("Failed to create audio settings table: {}", e))?;
Ok(Self { conn })
}
pub fn get_settings(&self) -> Result<AudioSettings, String> {
self.conn
.query_row(
"SELECT output_device, exclusive_mode, dac_passthrough, preferred_sample_rate FROM audio_settings WHERE id = 1",
[],
|row| {
Ok(AudioSettings {
output_device: row.get(0)?,
exclusive_mode: row.get::<_, i64>(1)? != 0,
dac_passthrough: row.get::<_, i64>(2)? != 0,
preferred_sample_rate: row.get(3)?,
})
},
)
.map_err(|e| format!("Failed to get audio settings: {}", e))
}
pub fn set_output_device(&self, device: Option<&str>) -> Result<(), String> {
self.conn
.execute(
"UPDATE audio_settings SET output_device = ?1 WHERE id = 1",
params![device],
)
.map_err(|e| format!("Failed to set output device: {}", e))?;
Ok(())
}
pub fn set_exclusive_mode(&self, enabled: bool) -> Result<(), String> {
self.conn
.execute(
"UPDATE audio_settings SET exclusive_mode = ?1 WHERE id = 1",
params![enabled as i64],
)
.map_err(|e| format!("Failed to set exclusive mode: {}", e))?;
Ok(())
}
pub fn set_dac_passthrough(&self, enabled: bool) -> Result<(), String> {
self.conn
.execute(
"UPDATE audio_settings SET dac_passthrough = ?1 WHERE id = 1",
params![enabled as i64],
)
.map_err(|e| format!("Failed to set DAC passthrough: {}", e))?;
Ok(())
}
pub fn set_sample_rate(&self, rate: Option<u32>) -> Result<(), String> {
self.conn
.execute(
"UPDATE audio_settings SET preferred_sample_rate = ?1 WHERE id = 1",
params![rate.map(|r| r as i64)],
)
.map_err(|e| format!("Failed to set sample rate: {}", e))?;
Ok(())
}
}
/// Thread-safe wrapper
pub struct AudioSettingsState {
pub store: Arc<Mutex<AudioSettingsStore>>,
}
impl AudioSettingsState {
pub fn new() -> Result<Self, String> {
Ok(Self {
store: Arc::new(Mutex::new(AudioSettingsStore::new()?)),
})
}
}
// Tauri commands
#[tauri::command]
pub fn get_audio_settings(
state: tauri::State<'_, AudioSettingsState>,
) -> Result<AudioSettings, String> {
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
store.get_settings()
}
#[tauri::command]
pub fn set_audio_output_device(
state: tauri::State<'_, AudioSettingsState>,
device: Option<String>,
) -> Result<(), String> {
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
store.set_output_device(device.as_deref())
}
#[tauri::command]
pub fn set_audio_exclusive_mode(
state: tauri::State<'_, AudioSettingsState>,
enabled: bool,
) -> Result<(), String> {
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
store.set_exclusive_mode(enabled)
}
#[tauri::command]
pub fn set_audio_dac_passthrough(
state: tauri::State<'_, AudioSettingsState>,
enabled: bool,
) -> Result<(), String> {
let store = state.store.lock().map_err(|e| format!("Lock error: {}", e))?;
store.set_dac_passthrough(enabled)
}
#[tauri::command]
pub fn set_audio_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_sample_rate(rate)
}
+11 -2
View File
@@ -7,5 +7,14 @@
//! - Local playlists
//! - Cached favorites
// TODO: Implement configuration storage
// pub mod store;
pub mod audio_settings;
pub use audio_settings::{
AudioSettings,
AudioSettingsState,
get_audio_settings,
set_audio_output_device,
set_audio_exclusive_mode,
set_audio_dac_passthrough,
set_audio_sample_rate,
};
+10
View File
@@ -124,6 +124,9 @@ pub fn run() {
// Initialize session store state
let session_store_state = session_store::SessionStoreState::new()
.expect("Failed to initialize session store");
// Initialize audio settings state
let audio_settings_state = config::audio_settings::AudioSettingsState::new()
.expect("Failed to initialize audio settings");
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
@@ -184,6 +187,7 @@ pub fn run() {
.manage(reco_state)
.manage(api_cache_state)
.manage(session_store_state)
.manage(audio_settings_state)
.invoke_handler(tauri::generate_handler![
// Auth commands
commands::init_client,
@@ -359,6 +363,12 @@ pub fn run() {
session_store::save_session_position,
session_store::save_session_playback_mode,
session_store::clear_session,
// Audio settings commands
config::audio_settings::get_audio_settings,
config::audio_settings::set_audio_output_device,
config::audio_settings::set_audio_exclusive_mode,
config::audio_settings::set_audio_dac_passthrough,
config::audio_settings::set_audio_sample_rate,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");