feat(playback): loudness bind rules, analysis seeding, and normalization UI

Resolve integrated loudness from SQLite only at decode bind; keep pre-trim
until a row exists, then allow provisional gain from live updates. Pass
DB-stable hints from the web app into play and gapless preload.

Default pre-measurement attenuation -4.5 dB with an icon reset in Settings;
drop redundant normalization copy and shorten pre-trim descriptions.

analysis_cache seed_from_bytes returns an outcome and skips redundant
waveform work on cache hits; wire callers and related frontend/backend glue.
This commit is contained in:
Maxim Isaev
2026-04-26 02:29:54 +03:00
parent c6fc3ec844
commit 4b60495e38
16 changed files with 749 additions and 425 deletions
+53 -13
View File
@@ -1199,8 +1199,30 @@ fn analysis_get_waveform(
md5_16kb: String,
cache: tauri::State<'_, analysis_cache::AnalysisCache>,
) -> Result<Option<WaveformCachePayload>, String> {
let key = analysis_cache::TrackKey { track_id, md5_16kb };
let key = analysis_cache::TrackKey {
track_id: track_id.clone(),
md5_16kb: md5_16kb.clone(),
};
let row = cache.get_waveform(&key)?;
match &row {
Some(v) => {
crate::app_deprintln!(
"[analysis][waveform] db hit (exact key) track_id={} md5_16kb={} bins_len={} bin_count={} updated_at={}",
track_id,
md5_16kb,
v.bins.len(),
v.bin_count,
v.updated_at
);
}
None => {
crate::app_deprintln!(
"[analysis][waveform] db miss (exact key) track_id={} md5_16kb={}",
track_id,
md5_16kb
);
}
}
Ok(row.map(|v| WaveformCachePayload {
bins: v.bins,
bin_count: v.bin_count,
@@ -1217,6 +1239,23 @@ fn analysis_get_waveform_for_track(
cache: tauri::State<'_, analysis_cache::AnalysisCache>,
) -> Result<Option<WaveformCachePayload>, String> {
let row = cache.get_latest_waveform_for_track(&track_id)?;
match &row {
Some(v) => {
crate::app_deprintln!(
"[analysis][waveform] db hit track_id={} bins_len={} bin_count={} updated_at={}",
track_id,
v.bins.len(),
v.bin_count,
v.updated_at
);
}
None => {
crate::app_deprintln!(
"[analysis][waveform] db miss track_id={}",
track_id
);
}
}
Ok(row.map(|v| WaveformCachePayload {
bins: v.bins,
bin_count: v.bin_count,
@@ -1349,18 +1388,19 @@ async fn stream_to_file(response: reqwest::Response, dest_path: &std::path::Path
}
fn enqueue_analysis_seed(app: &tauri::AppHandle, track_id: &str, bytes: &[u8]) -> Result<bool, String> {
analysis_cache::seed_from_bytes(app, track_id, bytes)
.map_err(|e| {
crate::app_eprintln!("[analysis] failed to seed {}: {}", track_id, e);
e
})?;
let _ = app.emit(
"analysis:waveform-updated",
WaveformUpdatedPayload {
track_id: track_id.to_string(),
is_partial: false,
},
);
let outcome = analysis_cache::seed_from_bytes(app, track_id, bytes).map_err(|e| {
crate::app_eprintln!("[analysis] failed to seed {}: {}", track_id, e);
e
})?;
if outcome == analysis_cache::SeedFromBytesOutcome::Upserted {
let _ = app.emit(
"analysis:waveform-updated",
WaveformUpdatedPayload {
track_id: track_id.to_string(),
is_partial: false,
},
);
}
let has_loudness = app
.try_state::<analysis_cache::AnalysisCache>()
.and_then(|cache| cache.get_latest_loudness_for_track(track_id).ok().flatten())