Files
qbz/crates/qbz-audio/src/loudness_cache.rs
T
vicrodh 3c8fc35fcb feat(qbz-audio): implement Phase 2 - audio backend system
CRITICAL: Copied audio modules from qbz-nix WITHOUT modifying logic.
This code handles bit-perfect playback and is IMMUTABLE.

Copied modules:
- backend.rs: Backend trait and manager
- pipewire_backend.rs: PipeWire/PulseAudio backend
- alsa_backend.rs: ALSA backend with device enumeration
- pulse_backend.rs: PulseAudio fallback
- alsa_direct.rs: Direct ALSA hardware access (hw:X,Y)
- diagnostic.rs: Audio diagnostic tools
- loudness.rs: ReplayGain extraction
- loudness_analyzer.rs: EBU R128 analysis
- loudness_cache.rs: SQLite cache for loudness data
- dynamic_amplify.rs: Dynamic volume amplification
- analyzer_tap.rs: Audio analyzer tap

Dependencies:
- Uses vendored rodio from src-tauri/vendor/rodio
- symphonia-all feature for f32 pipeline
- ebur128 for loudness analysis
- rusqlite for caching

Also updated qbz-models to remove incomplete audio types
(AudioBackendType, AudioDevice) - these stay in qbz-audio.
2026-02-14 13:10:41 -06:00

92 lines
3.0 KiB
Rust

//! Loudness cache — persists EBU R128 measurements in SQLite.
//!
//! Follows the `AudioSettingsStore` pattern: database lives in
//! `dirs::data_dir()/qbz/loudness_cache.db`.
//!
//! Thread-safe via `Mutex<Connection>`.
use rusqlite::{params, Connection};
use std::sync::Mutex;
#[derive(Debug, Clone)]
pub struct CachedLoudness {
pub gain_db: f32,
pub peak: f32,
/// Source of the measurement: "ebur128" or "replaygain"
pub source: String,
}
pub struct LoudnessCache {
conn: Mutex<Connection>,
}
impl LoudnessCache {
pub fn new() -> Result<Self, String> {
let data_dir = dirs::data_dir()
.ok_or_else(|| "Could not determine data directory".to_string())?
.join("qbz");
std::fs::create_dir_all(&data_dir)
.map_err(|e| format!("Failed to create data directory: {}", e))?;
let db_path = data_dir.join("loudness_cache.db");
let conn = Connection::open(&db_path)
.map_err(|e| format!("Failed to open loudness cache database: {}", e))?;
conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")
.map_err(|e| format!("Failed to enable WAL for loudness cache database: {}", e))?;
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS track_loudness (
track_id INTEGER PRIMARY KEY,
gain_db REAL NOT NULL,
peak REAL NOT NULL DEFAULT 0.0,
source TEXT NOT NULL DEFAULT 'ebur128',
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
)",
)
.map_err(|e| format!("Failed to create loudness table: {}", e))?;
log::info!("[LoudnessCache] Opened at {}", db_path.display());
Ok(Self {
conn: Mutex::new(conn),
})
}
/// Look up cached loudness for a track.
pub fn get(&self, track_id: u64) -> Option<CachedLoudness> {
let conn = self.conn.lock().ok()?;
conn.query_row(
"SELECT gain_db, peak, source FROM track_loudness WHERE track_id = ?1",
params![track_id as i64],
|row| {
Ok(CachedLoudness {
gain_db: row.get::<_, f64>(0)? as f32,
peak: row.get::<_, f64>(1)? as f32,
source: row.get(2)?,
})
},
)
.ok()
}
/// Store or update loudness data for a track.
pub fn set(&self, track_id: u64, gain_db: f32, peak: f32, source: &str) {
if let Ok(conn) = self.conn.lock() {
let result = conn.execute(
"INSERT OR REPLACE INTO track_loudness (track_id, gain_db, peak, source, created_at)
VALUES (?1, ?2, ?3, ?4, strftime('%s', 'now'))",
params![track_id as i64, gain_db as f64, peak as f64, source],
);
if let Err(e) = result {
log::warn!(
"[LoudnessCache] Failed to store loudness for track {}: {}",
track_id,
e
);
}
}
}
}