From 8f976dc371e2734390db291a1885ad792949c2ea Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Fri, 8 May 2026 15:41:19 +0200 Subject: [PATCH] refactor(audio): unify loudness/replay-gain prep via TrackGainInputs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src-tauri/src/audio/commands.rs | 57 ++++++---------------------- src-tauri/src/audio/helpers.rs | 66 ++++++++++++++++++++++----------- 2 files changed, 55 insertions(+), 68 deletions(-) diff --git a/src-tauri/src/audio/commands.rs b/src-tauri/src/audio/commands.rs index d21b1085..6aefe874 100644 --- a/src-tauri/src/audio/commands.rs +++ b/src-tauri/src/audio/commands.rs @@ -152,32 +152,12 @@ pub async fn audio_play( return Ok(()); } - let target_lufs = f32::from_bits(state.normalization_target_lufs.load(Ordering::Relaxed)); - 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_inputs = resolve_track_gain_inputs(&state, &app, &url, logical_trim.as_deref(), loudness_gain_db); let (gain_linear, effective_volume) = compute_gain( - norm_mode, + gain_inputs.norm_mode, replay_gain_db, replay_gain_peak, - startup_loudness_gain_db, + gain_inputs.effective_loudness_db, pre_gain_db, fallback_db, volume, @@ -186,22 +166,22 @@ pub async fn audio_play( 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}", playback_identity(&url), - normalization_engine_name(norm_mode), + normalization_engine_name(gain_inputs.norm_mode), replay_gain_db, replay_gain_peak, - resolved_loudness_gain_db, + gain_inputs.cache_loudness_db, gain_linear, current_gain_db, - target_lufs, + gain_inputs.target_lufs, volume, effective_volume ); maybe_emit_normalization_state( &app, NormalizationStatePayload { - engine: normalization_engine_name(norm_mode).to_string(), + engine: normalization_engine_name(gain_inputs.norm_mode).to_string(), 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. // the still-playing current source). Volume for the chained track is // 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 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_inputs = resolve_track_gain_inputs(&state, &app, &url, logical_trim.as_deref(), loudness_gain_db); let (gain_linear, _effective_volume) = compute_gain( - norm_mode, + gain_inputs.norm_mode, replay_gain_db, replay_gain_peak, - chain_loudness_db, + gain_inputs.effective_loudness_db, pre_gain_db, fallback_db, volume, diff --git a/src-tauri/src/audio/helpers.rs b/src-tauri/src/audio/helpers.rs index 15e48aeb..40a2a915 100644 --- a/src-tauri/src/audio/helpers.rs +++ b/src-tauri/src/audio/helpers.rs @@ -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`); -/// after `analysis:loudness-partial`, `audio_update_replay_gain` passes `true` so finite -/// JS gain applies until SQLite catches up. Must never return `None` or `compute_gain` uses unity. -pub(crate) fn loudness_gain_db_or_startup( +/// Resolved gain inputs that both `audio_play` and `audio_chain_preload` need +/// before calling [`compute_gain`]. Bundles the engine state reads + cache +/// resolution in one shot so the call sites don't drift apart on subtle +/// 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, + /// 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, +} + +/// 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, url: &str, - target_lufs: f32, logical_track_id: Option<&str>, - pre_analysis_attenuation_db: f32, - allow_js_when_uncached: bool, - js_gain_db: Option, -) -> Option { - let resolved = resolve_loudness_gain_from_cache_impl( - app, - url, + js_loudness_gain_db: Option, +) -> TrackGainInputs { + let target_lufs = f32::from_bits(state.normalization_target_lufs.load(Ordering::Relaxed)); + let norm_mode = state.normalization_engine.load(Ordering::Relaxed); + let pre_analysis_db = loudness_pre_analysis_db_for_engine(state); + let cache_loudness_db = resolve_loudness_gain_from_cache(app, url, target_lufs, logical_track_id); + let effective_loudness_db = if norm_mode == 2 { + loudness_gain_db_after_resolve( + cache_loudness_db, + target_lufs, + pre_analysis_db, + false, + js_loudness_gain_db, + ) + } else { + cache_loudness_db + }; + TrackGainInputs { target_lufs, - logical_track_id, - ResolveLoudnessCacheOpts::default(), - ); - loudness_gain_db_after_resolve( - resolved, - target_lufs, - pre_analysis_attenuation_db, - allow_js_when_uncached, - js_gain_db, - ) + norm_mode, + cache_loudness_db, + effective_loudness_db, + } } #[inline]