feat: normalize preview clips to -14 LUFS to match the library

Deezer preview clips are cut from the raw master (not pre-normalized), so
they played several dB hotter than the user's normalized -14 LUFS files —
measured across one artist the previews ranged -0.1 to -8.8 dB from target,
an 8.7 dB spread, so a flat offset would have been wrong either way.

Each clip is now measured with the same BS.1770 meter the main player uses
(preview.rs measure_clip_gain -> loudness::normalization_gain_db) right after
it is written in preview_resolve -- off the async worker, and fully off the
playback path on a prefetched neighbour. The linear gain is stored in the
lease and the reuse cache, and rides UNDER the app-volume mirror per voice
(effective = target_vol * clip_gain), so it composes with the slider, mute
and the crossfade (each clip crosses in at its own matched level). Gated on
the volume_normalization setting; reset on dispose. Debug overlay PREVIEW row
now shows the active clip's norm dB.

Verified live: per-clip gains -0.1..-8.8 dB applied; mute still zeroes the
preview; 39 app + 12 player tests pass.
This commit is contained in:
KeelrLeek committed 2026-07-23 17:12:04 -05:00
1 parent abc959ca2f
commit 28aba3a3b3
4 files changed
+109 -21

No files matched your search

+13
View File
@@ -129,6 +129,19 @@ fn gain_db(m: &Measured) -> f64 {
g
}
/// The same −14 LUFS normalization gain (dB) the main player uses, exposed
/// for the preview backend so Deezer clips are brought to the SAME loudness
/// as the user's own normalized tracks instead of playing at their raw
/// (much louder) master level.
pub fn normalization_gain_db(lufs: f64, peak: f32) -> f64 {
gain_db(&Measured {
lufs,
peak,
size: 0,
at_epoch_ms: 0,
})
}
/// Toggle from settings: applies to the CURRENT track immediately (gain to
/// unity when turning off; the cached gain back on when turning on is left
/// to the next track — the analyzer generation for the current one is gone).