From e2ca5812645d477ba76e876098f99b8a1fe6fd37 Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Fri, 8 May 2026 13:41:24 +0200 Subject: [PATCH] refactor(audio): extract audio-stage settings into mix_commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pull the six configuration setters out of audio/commands.rs into audio/mix_commands.rs (~170 LOC): - audio_set_volume (with replay-gain-aware ramp) - audio_update_replay_gain (resolves cache-backed loudness, computes gain, ramps sink, emits NormalizationStatePayload) - audio_set_eq (10-band gains + pre-gain) - audio_set_crossfade - audio_set_gapless - audio_set_normalization These are pure AudioEngine state mutations + (for normalization) an ipc emit. They don't drive playback; collecting them in one place makes commands.rs more focused on the playback orchestrators. lib.rs invoke_handler updated. audio/commands.rs: 1784 → 1614 LOC. --- src-tauri/src/audio/commands.rs | 168 ------------------------- src-tauri/src/audio/mix_commands.rs | 182 ++++++++++++++++++++++++++++ src-tauri/src/audio/mod.rs | 1 + src-tauri/src/lib.rs | 12 +- 4 files changed, 189 insertions(+), 174 deletions(-) create mode 100644 src-tauri/src/audio/mix_commands.rs diff --git a/src-tauri/src/audio/commands.rs b/src-tauri/src/audio/commands.rs index 1178a9d7..888fc2e6 100644 --- a/src-tauri/src/audio/commands.rs +++ b/src-tauri/src/audio/commands.rs @@ -1388,125 +1388,8 @@ pub fn audio_seek(seconds: f64, state: State<'_, AudioEngine>) -> Result<(), Str Ok(()) } -#[tauri::command] -pub fn audio_set_volume(volume: f32, state: State<'_, AudioEngine>) { - let mut cur = state.current.lock().unwrap(); - let prev_effective = (cur.base_volume * cur.replay_gain_linear * MASTER_HEADROOM).clamp(0.0, 1.0); - cur.base_volume = volume.clamp(0.0, 1.0); - if let Some(sink) = &cur.sink { - let next_effective = (cur.base_volume * cur.replay_gain_linear * MASTER_HEADROOM).clamp(0.0, 1.0); - ramp_sink_volume(Arc::clone(sink), prev_effective, next_effective); - } -} - -#[tauri::command] -pub fn audio_update_replay_gain( - volume: f32, - replay_gain_db: Option, - replay_gain_peak: Option, - loudness_gain_db: Option, - pre_gain_db: f32, - fallback_db: f32, - app: AppHandle, - state: State<'_, AudioEngine>, -) { - let norm_mode = state.normalization_engine.load(Ordering::Relaxed); - let target_lufs = f32::from_bits(state.normalization_target_lufs.load(Ordering::Relaxed)); - let pre_analysis_db = loudness_pre_analysis_db_for_engine(&state); - let url_for_loudness = if norm_mode == 2 { - state.current_playback_url.lock().unwrap().clone() - } else { - None - }; - let logical_for_loudness = state - .current_analysis_track_id - .lock() - .ok() - .and_then(|g| (*g).clone()) - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()); - // If `current_playback_url` is not pinned yet, still honour JS `loudness_gain_db` - // for the uncached path (`effective_loudness_db` / UI gain follow from `compute_gain`). - let cache_loudness = url_for_loudness.as_deref().and_then(|u| { - resolve_loudness_gain_from_cache_impl( - &app, - u, - target_lufs, - logical_for_loudness.as_deref(), - ResolveLoudnessCacheOpts { - touch_waveform: false, - log_soft_misses: false, - }, - ) - }); - let effective_loudness_db = if norm_mode == 2 { - match url_for_loudness.as_deref() { - Some(_u) => loudness_gain_db_after_resolve( - cache_loudness, - target_lufs, - pre_analysis_db, - true, - loudness_gain_db, - ), - None => { - loudness_gain_db.or(Some(loudness_gain_placeholder_until_cache( - target_lufs, - pre_analysis_db, - ))) - } - } - } else { - loudness_gain_db - }; - let (gain_linear, effective) = compute_gain( - norm_mode, - replay_gain_db, - replay_gain_peak, - effective_loudness_db, - pre_gain_db, - fallback_db, - volume, - ); - let current_gain_db = loudness_ui_current_gain_db(gain_linear); - crate::app_deprintln!( - "[normalization] audio_update_replay_gain engine={} replay_gain_db={:?} replay_gain_peak={:?} loudness_gain_db={:?} gain_linear={:.4} current_gain_db={:?} target_lufs={:.2} volume={:.3} effective={:.3}", - normalization_engine_name(norm_mode), - replay_gain_db, - replay_gain_peak, - loudness_gain_db, - gain_linear, - current_gain_db, - target_lufs, - volume, - effective - ); - let mut cur = state.current.lock().unwrap(); - let prev_effective = (cur.base_volume * cur.replay_gain_linear * MASTER_HEADROOM).clamp(0.0, 1.0); - cur.replay_gain_linear = gain_linear; - cur.base_volume = volume.clamp(0.0, 1.0); - if let Some(sink) = &cur.sink { - ramp_sink_volume(Arc::clone(sink), prev_effective, effective); - } - drop(cur); - maybe_emit_normalization_state( - &app, - NormalizationStatePayload { - engine: normalization_engine_name(norm_mode).to_string(), - current_gain_db, - target_lufs, - }, - ); -} -#[tauri::command] -pub fn audio_set_eq(gains: [f32; 10], enabled: bool, pre_gain: f32, state: State<'_, AudioEngine>) { - state.eq_enabled.store(enabled, Ordering::Relaxed); - state.eq_pre_gain.store(pre_gain.clamp(-30.0, 6.0).to_bits(), Ordering::Relaxed); - for (i, &gain) in gains.iter().enumerate() { - state.eq_gains[i].store(gain.clamp(-12.0, 12.0).to_bits(), Ordering::Relaxed); - } -} #[tauri::command] pub async fn audio_preload( @@ -1731,54 +1614,3 @@ pub async fn audio_play_radio( } -#[tauri::command] -pub fn audio_set_crossfade(enabled: bool, secs: f32, state: State<'_, AudioEngine>) { - state.crossfade_enabled.store(enabled, Ordering::Relaxed); - state.crossfade_secs.store(secs.clamp(0.1, 12.0).to_bits(), Ordering::Relaxed); -} - -#[tauri::command] -pub fn audio_set_gapless(enabled: bool, state: State<'_, AudioEngine>) { - state.gapless_enabled.store(enabled, Ordering::Relaxed); -} - -#[tauri::command] -pub fn audio_set_normalization( - engine: String, - target_lufs: f32, - pre_analysis_attenuation_db: f32, - app: AppHandle, - state: State<'_, AudioEngine>, -) { - let mode = match engine.as_str() { - "replaygain" => 1, - "loudness" => 2, - _ => 0, - }; - state.normalization_engine.store(mode, Ordering::Relaxed); - let target = target_lufs.clamp(-30.0, -8.0); - state - .normalization_target_lufs - .store(target.to_bits(), Ordering::Relaxed); - let pre = pre_analysis_attenuation_db.clamp(-24.0, 0.0).min(0.0); - state - .loudness_pre_analysis_attenuation_db - .store(pre.to_bits(), Ordering::Relaxed); - crate::app_deprintln!( - "[normalization] audio_set_normalization requested_engine={} resolved_engine={} target_lufs={:.2} pre_analysis_db={:.2}", - engine, - normalization_engine_name(mode), - target, - pre - ); - maybe_emit_normalization_state( - &app, - NormalizationStatePayload { - engine: normalization_engine_name(mode).to_string(), - // At mode-switch time the effective track gain may not be recalculated yet. - // Emit `None` and let audio_play/audio_update_replay_gain publish actual value. - current_gain_db: None, - target_lufs: target, - }, - ); -} diff --git a/src-tauri/src/audio/mix_commands.rs b/src-tauri/src/audio/mix_commands.rs new file mode 100644 index 00000000..c0fbb786 --- /dev/null +++ b/src-tauri/src/audio/mix_commands.rs @@ -0,0 +1,182 @@ +//! Audio-stage settings commands: volume, replay-gain / loudness normalization, +//! 10-band EQ, crossfade, gapless. + +use std::sync::Arc; +use std::sync::atomic::Ordering; + +use tauri::{AppHandle, State}; + +use super::engine::AudioEngine; +use super::helpers::*; +use super::ipc::{maybe_emit_normalization_state, NormalizationStatePayload}; + +#[tauri::command] +pub fn audio_set_volume(volume: f32, state: State<'_, AudioEngine>) { + let mut cur = state.current.lock().unwrap(); + let prev_effective = (cur.base_volume * cur.replay_gain_linear * MASTER_HEADROOM).clamp(0.0, 1.0); + cur.base_volume = volume.clamp(0.0, 1.0); + if let Some(sink) = &cur.sink { + let next_effective = (cur.base_volume * cur.replay_gain_linear * MASTER_HEADROOM).clamp(0.0, 1.0); + ramp_sink_volume(Arc::clone(sink), prev_effective, next_effective); + } +} + +#[tauri::command] +pub fn audio_update_replay_gain( + volume: f32, + replay_gain_db: Option, + replay_gain_peak: Option, + loudness_gain_db: Option, + pre_gain_db: f32, + fallback_db: f32, + app: AppHandle, + state: State<'_, AudioEngine>, +) { + let norm_mode = state.normalization_engine.load(Ordering::Relaxed); + let target_lufs = f32::from_bits(state.normalization_target_lufs.load(Ordering::Relaxed)); + let pre_analysis_db = loudness_pre_analysis_db_for_engine(&state); + let url_for_loudness = if norm_mode == 2 { + state.current_playback_url.lock().unwrap().clone() + } else { + None + }; + let logical_for_loudness = state + .current_analysis_track_id + .lock() + .ok() + .and_then(|g| (*g).clone()) + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()); + // If `current_playback_url` is not pinned yet, still honour JS `loudness_gain_db` + // for the uncached path (`effective_loudness_db` / UI gain follow from `compute_gain`). + let cache_loudness = url_for_loudness.as_deref().and_then(|u| { + resolve_loudness_gain_from_cache_impl( + &app, + u, + target_lufs, + logical_for_loudness.as_deref(), + ResolveLoudnessCacheOpts { + touch_waveform: false, + log_soft_misses: false, + }, + ) + }); + let effective_loudness_db = if norm_mode == 2 { + match url_for_loudness.as_deref() { + Some(_u) => loudness_gain_db_after_resolve( + cache_loudness, + target_lufs, + pre_analysis_db, + true, + loudness_gain_db, + ), + None => { + loudness_gain_db.or(Some(loudness_gain_placeholder_until_cache( + target_lufs, + pre_analysis_db, + ))) + } + } + } else { + loudness_gain_db + }; + let (gain_linear, effective) = compute_gain( + norm_mode, + replay_gain_db, + replay_gain_peak, + effective_loudness_db, + pre_gain_db, + fallback_db, + volume, + ); + let current_gain_db = loudness_ui_current_gain_db(gain_linear); + crate::app_deprintln!( + "[normalization] audio_update_replay_gain engine={} replay_gain_db={:?} replay_gain_peak={:?} loudness_gain_db={:?} gain_linear={:.4} current_gain_db={:?} target_lufs={:.2} volume={:.3} effective={:.3}", + normalization_engine_name(norm_mode), + replay_gain_db, + replay_gain_peak, + loudness_gain_db, + gain_linear, + current_gain_db, + target_lufs, + volume, + effective + ); + let mut cur = state.current.lock().unwrap(); + let prev_effective = (cur.base_volume * cur.replay_gain_linear * MASTER_HEADROOM).clamp(0.0, 1.0); + cur.replay_gain_linear = gain_linear; + cur.base_volume = volume.clamp(0.0, 1.0); + if let Some(sink) = &cur.sink { + ramp_sink_volume(Arc::clone(sink), prev_effective, effective); + } + drop(cur); + maybe_emit_normalization_state( + &app, + NormalizationStatePayload { + engine: normalization_engine_name(norm_mode).to_string(), + current_gain_db, + target_lufs, + }, + ); +} + +#[tauri::command] +pub fn audio_set_eq(gains: [f32; 10], enabled: bool, pre_gain: f32, state: State<'_, AudioEngine>) { + state.eq_enabled.store(enabled, Ordering::Relaxed); + state.eq_pre_gain.store(pre_gain.clamp(-30.0, 6.0).to_bits(), Ordering::Relaxed); + for (i, &gain) in gains.iter().enumerate() { + state.eq_gains[i].store(gain.clamp(-12.0, 12.0).to_bits(), Ordering::Relaxed); + } +} + +#[tauri::command] +pub fn audio_set_crossfade(enabled: bool, secs: f32, state: State<'_, AudioEngine>) { + state.crossfade_enabled.store(enabled, Ordering::Relaxed); + state.crossfade_secs.store(secs.clamp(0.1, 12.0).to_bits(), Ordering::Relaxed); +} + +#[tauri::command] +pub fn audio_set_gapless(enabled: bool, state: State<'_, AudioEngine>) { + state.gapless_enabled.store(enabled, Ordering::Relaxed); +} + +#[tauri::command] +pub fn audio_set_normalization( + engine: String, + target_lufs: f32, + pre_analysis_attenuation_db: f32, + app: AppHandle, + state: State<'_, AudioEngine>, +) { + let mode = match engine.as_str() { + "replaygain" => 1, + "loudness" => 2, + _ => 0, + }; + state.normalization_engine.store(mode, Ordering::Relaxed); + let target = target_lufs.clamp(-30.0, -8.0); + state + .normalization_target_lufs + .store(target.to_bits(), Ordering::Relaxed); + let pre = pre_analysis_attenuation_db.clamp(-24.0, 0.0).min(0.0); + state + .loudness_pre_analysis_attenuation_db + .store(pre.to_bits(), Ordering::Relaxed); + crate::app_deprintln!( + "[normalization] audio_set_normalization requested_engine={} resolved_engine={} target_lufs={:.2} pre_analysis_db={:.2}", + engine, + normalization_engine_name(mode), + target, + pre + ); + maybe_emit_normalization_state( + &app, + NormalizationStatePayload { + engine: normalization_engine_name(mode).to_string(), + // At mode-switch time the effective track gain may not be recalculated yet. + // Emit `None` and let audio_play/audio_update_replay_gain publish actual value. + current_gain_db: None, + target_lufs: target, + }, + ); +} diff --git a/src-tauri/src/audio/mod.rs b/src-tauri/src/audio/mod.rs index 7b3d582b..da8029f0 100644 --- a/src-tauri/src/audio/mod.rs +++ b/src-tauri/src/audio/mod.rs @@ -9,6 +9,7 @@ pub mod commands; mod decode; mod dev_io; pub mod device_commands; +pub mod mix_commands; mod device_watcher; mod engine; mod power_resume; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 847caf87..69b421b3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -392,9 +392,9 @@ pub fn run() { audio::commands::audio_resume, audio::commands::audio_stop, audio::commands::audio_seek, - audio::commands::audio_set_volume, - audio::commands::audio_update_replay_gain, - audio::commands::audio_set_eq, + audio::mix_commands::audio_set_volume, + audio::mix_commands::audio_update_replay_gain, + audio::mix_commands::audio_set_eq, audio::autoeq_commands::autoeq_entries, audio::autoeq_commands::autoeq_fetch_profile, audio::commands::audio_preload, @@ -403,9 +403,9 @@ pub fn run() { audio::preview::audio_preview_stop, audio::preview::audio_preview_stop_silent, audio::preview::audio_preview_set_volume, - audio::commands::audio_set_crossfade, - audio::commands::audio_set_gapless, - audio::commands::audio_set_normalization, + audio::mix_commands::audio_set_crossfade, + audio::mix_commands::audio_set_gapless, + audio::mix_commands::audio_set_normalization, audio::device_commands::audio_list_devices, audio::device_commands::audio_canonicalize_selected_device, audio::device_commands::audio_default_output_device_name,