loudness: correct the gain during the track instead of only guessing at its start
A streamed first play is gained from a prefix; every later play is gained from the whole track. Measured across the 89 cached tracks, a 10s head asks for a median 3.4 dB too much gain and is more than 1 dB hot on 68 of them — that gap is the "first track is so much louder" complaint. The curve never flattens either: even a 60s head is still ~1 dB out with a 6 dB worst case, so no window size fixes it. Live traces then showed something worse. The hard deadline was anchored to the play REQUEST, so on a peer that took a moment to start sending, the 2.5s ran out before a single gating block existed and the track began at unity — no normalization at all, three plays out of four on one run. Releasing early bought nothing in exchange: playback is blocked on the same bytes, so the gate was discarding the measurement without making sound arrive sooner. - The deadline now starts at the first byte, with an absolute 4s ceiling from the request so a slow peer cannot hold audio that is ready to play. - The audio thread's slew drops 2.5 -> 0.25 dB/s. Mid-track correction was switched off in July because the glide was audible; at a tenth the rate it is not, and being 3 dB wrong for a whole track is the larger error. - The correction follows the running measurement from its first publish and may only ratchet DOWN until the track completes. A convergence test lived here and was deleted: an integrated average over 30s cannot move much when one more second is added, so "3 publishes within 0.3 dB" was measuring the inertia of a long average, fired at its floor every time, and declared settled while 1.5 and 2.1 dB from truth. The protection was never the gate — it is the slew rate and the ratchet. - Seeking lands the pending correction immediately. The slew is slow only because a gain change is audible against continuous music, and a seek is already a discontinuity. Observability: SOULSTREAM_LOUDNESS_TRACE=1 reports every commit with the seconds analyzed, which limit fired, the hold, and the bytes received; the debug overlay gains a SLEW line showing applied gain, target, distance and ETA, because one number cannot distinguish "settled" from "in flight". Also adds an #[ignore] corpus harness that sweeps head-window bias in 30s.
This commit is contained in:
1 parent
4f485b6f9a
commit
1e35c5e039
4 files changed
+502
-13
No files matched your search
@@ -82,6 +82,13 @@ pub struct DebugStats {
|
||||
/// Main player's live fade-gain (1.0 = full; <1 only during a preview
|
||||
/// open/close crossfade). Proves a fade never strands the volume.
|
||||
pub main_fade_gain: Option<f32>,
|
||||
/// The normalization gain the sink has RIGHT NOW, and the one the audio
|
||||
/// thread is slewing toward, both dB. Reported as a pair because the
|
||||
/// interesting state during a mid-track correction is the gap between
|
||||
/// them: equal means settled, different means a correction is in flight
|
||||
/// and the difference is how far it still has to travel.
|
||||
pub norm_now_db: Option<f32>,
|
||||
pub norm_target_db: Option<f32>,
|
||||
/// The preview voices' resting volume (mirrors the app volume; 0 = muted).
|
||||
pub preview_volume: Option<f32>,
|
||||
/// The active preview clip's loudness-normalization gain in dB.
|
||||
@@ -170,6 +177,12 @@ pub fn debug_stats(app: tauri::AppHandle, state: State<'_, DiagState>) -> DebugS
|
||||
normalization_enabled,
|
||||
target_lufs,
|
||||
loudness,
|
||||
norm_now_db: app
|
||||
.try_state::<crate::playback::PlayerState>()
|
||||
.map(|s| s.player.norm_gain_now_target_db().0),
|
||||
norm_target_db: app
|
||||
.try_state::<crate::playback::PlayerState>()
|
||||
.map(|s| s.player.norm_gain_now_target_db().1),
|
||||
preview_volume: app
|
||||
.try_state::<crate::preview::PreviewState>()
|
||||
.map(|s| crate::preview::target_volume(&s)),
|
||||
|
||||
Reference in new issue
Block a user