refactor(audio): unify loudness/replay-gain prep via TrackGainInputs

audio_play and audio_chain_preload were both reading the same engine
state (target_lufs, norm_mode, pre_analysis_db) and resolving the
loudness cache, then feeding it into compute_gain — but with subtly
different intermediate steps. audio_play split the resolve+post-resolve
into two operations so it could log the cache value; chain_preload
used the bundled `loudness_gain_db_or_startup` wrapper.

Lift the shared logic into `resolve_track_gain_inputs(state, app, url,
logical_id, js_loudness_gain_db) -> TrackGainInputs` in helpers.rs.
The struct returns target_lufs, norm_mode, the cache-loudness value
(for logging), and the post-resolve effective loudness for compute_gain.
Both call sites now use the same helper; behaviour-preserving.

Drops the now-unused `loudness_gain_db_or_startup` (audio_chain_preload
was its only caller). audio/commands.rs: 676 → 642 LOC.
This commit is contained in:
Psychotoxical
2026-05-08 15:41:19 +02:00
parent 7a61d50c42
commit 8f976dc371
2 changed files with 55 additions and 68 deletions
+11 -46
View File
@@ -152,32 +152,12 @@ pub async fn audio_play(
return Ok(()); return Ok(());
} }
let target_lufs = f32::from_bits(state.normalization_target_lufs.load(Ordering::Relaxed)); let gain_inputs = resolve_track_gain_inputs(&state, &app, &url, logical_trim.as_deref(), loudness_gain_db);
let cache_loudness = resolve_loudness_gain_from_cache(
&app,
&url,
target_lufs,
logical_trim.as_deref(),
);
let resolved_loudness_gain_db = cache_loudness;
let norm_mode = state.normalization_engine.load(Ordering::Relaxed);
let pre_analysis_db = loudness_pre_analysis_db_for_engine(&state);
let startup_loudness_gain_db = if norm_mode == 2 {
loudness_gain_db_after_resolve(
cache_loudness,
target_lufs,
pre_analysis_db,
false,
loudness_gain_db,
)
} else {
cache_loudness
};
let (gain_linear, effective_volume) = compute_gain( let (gain_linear, effective_volume) = compute_gain(
norm_mode, gain_inputs.norm_mode,
replay_gain_db, replay_gain_db,
replay_gain_peak, replay_gain_peak,
startup_loudness_gain_db, gain_inputs.effective_loudness_db,
pre_gain_db, pre_gain_db,
fallback_db, fallback_db,
volume, volume,
@@ -186,22 +166,22 @@ pub async fn audio_play(
crate::app_deprintln!( crate::app_deprintln!(
"[normalization] audio_play track_id={:?} engine={} replay_gain_db={:?} replay_gain_peak={:?} loudness_gain_db={:?} gain_linear={:.4} current_gain_db={:?} target_lufs={:.2} volume={:.3} effective_volume={:.3}", "[normalization] audio_play track_id={:?} engine={} replay_gain_db={:?} replay_gain_peak={:?} loudness_gain_db={:?} gain_linear={:.4} current_gain_db={:?} target_lufs={:.2} volume={:.3} effective_volume={:.3}",
playback_identity(&url), playback_identity(&url),
normalization_engine_name(norm_mode), normalization_engine_name(gain_inputs.norm_mode),
replay_gain_db, replay_gain_db,
replay_gain_peak, replay_gain_peak,
resolved_loudness_gain_db, gain_inputs.cache_loudness_db,
gain_linear, gain_linear,
current_gain_db, current_gain_db,
target_lufs, gain_inputs.target_lufs,
volume, volume,
effective_volume effective_volume
); );
maybe_emit_normalization_state( maybe_emit_normalization_state(
&app, &app,
NormalizationStatePayload { NormalizationStatePayload {
engine: normalization_engine_name(norm_mode).to_string(), engine: normalization_engine_name(gain_inputs.norm_mode).to_string(),
current_gain_db, current_gain_db,
target_lufs, target_lufs: gain_inputs.target_lufs,
}, },
); );
@@ -574,27 +554,12 @@ pub async fn audio_chain_preload(
// current track ends, and `Sink::set_volume` affects the WHOLE Sink (incl. // current track ends, and `Sink::set_volume` affects the WHOLE Sink (incl.
// the still-playing current source). Volume for the chained track is // the still-playing current source). Volume for the chained track is
// applied at the gapless transition in `spawn_progress_task`, not here. // applied at the gapless transition in `spawn_progress_task`, not here.
let target_lufs = f32::from_bits(state.normalization_target_lufs.load(Ordering::Relaxed)); let gain_inputs = resolve_track_gain_inputs(&state, &app, &url, logical_trim.as_deref(), loudness_gain_db);
let norm_mode = state.normalization_engine.load(Ordering::Relaxed);
let pre_analysis_db = loudness_pre_analysis_db_for_engine(&state);
let chain_loudness_db = if norm_mode == 2 {
loudness_gain_db_or_startup(
&app,
&url,
target_lufs,
logical_trim.as_deref(),
pre_analysis_db,
false,
loudness_gain_db,
)
} else {
resolve_loudness_gain_from_cache(&app, &url, target_lufs, logical_trim.as_deref())
};
let (gain_linear, _effective_volume) = compute_gain( let (gain_linear, _effective_volume) = compute_gain(
norm_mode, gain_inputs.norm_mode,
replay_gain_db, replay_gain_db,
replay_gain_peak, replay_gain_peak,
chain_loudness_db, gain_inputs.effective_loudness_db,
pre_gain_db, pre_gain_db,
fallback_db, fallback_db,
volume, volume,
+42 -20
View File
@@ -438,32 +438,54 @@ pub(crate) fn loudness_gain_db_after_resolve(
} }
} }
/// LUFS: DB-backed integrated LUFS only at bind time (`allow_js_when_uncached = false`); /// Resolved gain inputs that both `audio_play` and `audio_chain_preload` need
/// after `analysis:loudness-partial`, `audio_update_replay_gain` passes `true` so finite /// before calling [`compute_gain`]. Bundles the engine state reads + cache
/// JS gain applies until SQLite catches up. Must never return `None` or `compute_gain` uses unity. /// resolution in one shot so the call sites don't drift apart on subtle
pub(crate) fn loudness_gain_db_or_startup( /// behaviour (e.g. one accidentally skipping the post-resolve step for
/// LUFS mode).
#[derive(Debug, Clone, Copy)]
pub(crate) struct TrackGainInputs {
pub(crate) target_lufs: f32,
pub(crate) norm_mode: u32,
/// Pre-resolve cache value — kept around for logging in `audio_play`.
pub(crate) cache_loudness_db: Option<f32>,
/// Value to feed into `compute_gain` — for LUFS mode this is the
/// post-`loudness_gain_db_after_resolve` value, otherwise the raw cache
/// resolution (or `None` when not in normalisation mode).
pub(crate) effective_loudness_db: Option<f32>,
}
/// Read engine state + resolve the loudness cache for a track that's about to
/// start playing. JS-supplied `loudness_gain_db` is **not** consulted at bind
/// time (only post-cache via `audio_update_replay_gain`).
pub(crate) fn resolve_track_gain_inputs(
state: &AudioEngine,
app: &AppHandle, app: &AppHandle,
url: &str, url: &str,
target_lufs: f32,
logical_track_id: Option<&str>, logical_track_id: Option<&str>,
pre_analysis_attenuation_db: f32, js_loudness_gain_db: Option<f32>,
allow_js_when_uncached: bool, ) -> TrackGainInputs {
js_gain_db: Option<f32>, let target_lufs = f32::from_bits(state.normalization_target_lufs.load(Ordering::Relaxed));
) -> Option<f32> { let norm_mode = state.normalization_engine.load(Ordering::Relaxed);
let resolved = resolve_loudness_gain_from_cache_impl( let pre_analysis_db = loudness_pre_analysis_db_for_engine(state);
app, let cache_loudness_db = resolve_loudness_gain_from_cache(app, url, target_lufs, logical_track_id);
url, let effective_loudness_db = if norm_mode == 2 {
target_lufs,
logical_track_id,
ResolveLoudnessCacheOpts::default(),
);
loudness_gain_db_after_resolve( loudness_gain_db_after_resolve(
resolved, cache_loudness_db,
target_lufs, target_lufs,
pre_analysis_attenuation_db, pre_analysis_db,
allow_js_when_uncached, false,
js_gain_db, js_loudness_gain_db,
) )
} else {
cache_loudness_db
};
TrackGainInputs {
target_lufs,
norm_mode,
cache_loudness_db,
effective_loudness_db,
}
} }
#[inline] #[inline]