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");
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg xmlns="http://www.w3.org/2000/svg"
aria-label="Apple Music" role="img"
viewBox="0 0 512 512"><rect
width="512" height="512"
rx="15%" fill="url(#g)"/><linearGradient id="g" x1=".5" y1=".99" x2=".5" y2=".02"><stop offset="0" stop-color="#FA233B"/><stop offset="1" stop-color="#FB5C74"/></linearGradient><path fill="#ffffff" d="M199 359V199q0-9 10-11l138-28q11-2 12 10v122q0 15-45 20c-57 9-48 105 30 79 30-11 35-40 35-69V88s0-20-17-15l-170 35s-13 2-13 18v203q0 15-45 20c-57 9-48 105 30 79 30-11 35-40 35-69"/></svg>

After

Width:  |  Height:  |  Size: 636 B

+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg xmlns="http://www.w3.org/2000/svg"
aria-label="Deezer" role="img"
fill="none" stroke-width="20"
viewBox="0 0 512 512"><rect
width="512" height="512"
rx="15%"
fill="#231f20"/><path stroke="#f1796e" d="M78 241h64m-64 26h64"/><path stroke="#ec2529" d="M78 293h64m-64 26h64m-64 26h64"/><path stroke="#f6dd05" d="M148 293h64m-64 26h64m-64 26h64"/><path stroke="#e78dcc" d="M218 241h64m-64 26h64m-64-52h64m-64-26h64"/><path stroke="#d81996" d="M218 293h64m-64 26h64m-64 26h64"/><path stroke="#d6e465" d="M288 241h64m-64 26h64"/><path stroke="#adda1a" d="M288 293h64m-64 26h64m-64 26h64"/><path stroke="#b0d8e6" d="M358 241h64m-64 26h64m-64-52h64m-64-26h64m-64-26h64"/><path stroke="#56bbe9" d="M358 293h64m-64 26h64m-64 26h64"/></svg>

After

Width:  |  Height:  |  Size: 852 B

+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" standalone="no"?>
<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 192 192" fill="none"><path stroke="#000000" stroke-linecap="round" stroke-width="8" d="M140.672 157.486a76.004 76.004 0 0 1-102.464-12.128 76.001 76.001 0 0 1 107.15-107.15 76.004 76.004 0 0 1 12.128 102.464"/><path stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="15" stroke-width="8" d="M124.807 142.105a54.364 54.364 0 1 1 17.296-17.296"/><path stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="15" stroke-width="8" d="M95.997 117.193c11.705 0 21.193-9.488 21.193-21.193 0-11.704-9.488-21.193-21.193-21.193-11.704 0-21.193 9.489-21.193 21.193 0 11.705 9.489 21.193 21.193 21.193Z"/><path fill="#000000" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="15" stroke-width="3.799" d="M95.997 98.103a2.103 2.103 0 1 0 0-4.205 2.103 2.103 0 0 0 0 4.205Z"/><path fill="#000000" d="m121.193 121.196 41.871 41.625Z"/><path stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="15" stroke-width="12" d="m121.193 121.196 41.871 41.625"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
␍<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg xmlns="http://www.w3.org/2000/svg"
aria-label="Spotify" role="img"
viewBox="0 0 512 512">
␍<rect
width="512" height="512"
rx="15%"
fill="#3bd75f"/>
␍<circle cx="256" cy="256" fill="#ffffff" r="192"/>
␍<g fill="none" stroke="#3bd75f" stroke-linecap="round">
␍<path d="m141 195c75-20 164-15 238 24" stroke-width="36"/>
␍<path d="m152 257c61-17 144-13 203 24" stroke-width="31"/>
␍<path d="m156 315c54-12 116-17 178 20" stroke-width="24"/>
␍</g>
␍</svg>

After

Width:  |  Height:  |  Size: 576 B

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path d="M16.016 5.323l-5.339 5.339-5.339-5.339-5.339 5.339 5.339 5.339 5.339-5.339 5.339 5.339-5.339 5.339 5.339 5.339 5.339-5.339-5.339-5.339 5.339-5.339zM21.391 10.661l5.302-5.307 5.307 5.307-5.307 5.307z"/>
</svg>

After

Width:  |  Height:  |  Size: 443 B