Skip to main content

proteus_lib/playback/player/
effects.rs

1//! DSP-chain control and output metering accessors for `Player`.
2//!
3//! Methods in this module mutate effect configuration, trigger effect resets,
4//! and expose meter/metrics snapshots suitable for UI polling.
5
6use std::sync::atomic::Ordering;
7
8use crate::dsp::effects::convolution_reverb::{parse_impulse_response_string, ImpulseResponseSpec};
9use crate::{
10    dsp::effects::AudioEffect,
11    playback::engine::{DspChainMetrics, InlineEffectsUpdate},
12};
13
14use super::{Player, ReverbSettingsSnapshot};
15
16impl Player {
17    /// Override the impulse response used for convolution reverb.
18    ///
19    /// # Arguments
20    ///
21    /// * `spec` - Parsed IR selection/configuration to persist on the player.
22    pub fn set_impulse_response_spec(&mut self, spec: ImpulseResponseSpec) {
23        self.impulse_response_override = Some(spec.clone());
24        let mut prot = self.prot.lock().unwrap();
25        prot.set_impulse_response_spec(spec);
26        self.request_effects_reset();
27    }
28
29    /// Parse and apply an impulse response spec string.
30    ///
31    /// Invalid input is ignored and leaves the current override unchanged.
32    pub fn set_impulse_response_from_string(&mut self, value: &str) {
33        if let Some(spec) = parse_impulse_response_string(value) {
34            self.set_impulse_response_spec(spec);
35        }
36    }
37
38    /// Override the impulse response tail trim (dB).
39    ///
40    /// # Arguments
41    ///
42    /// * `tail_db` - Trim threshold in decibels applied to IR tails.
43    pub fn set_impulse_response_tail_db(&mut self, tail_db: f32) {
44        self.impulse_response_tail_override = Some(tail_db);
45        let mut prot = self.prot.lock().unwrap();
46        prot.set_impulse_response_tail_db(tail_db);
47        self.request_effects_reset();
48    }
49
50    /// Enable or disable supported reverb effects in the active chain.
51    ///
52    /// The toggle is applied to convolution and delay-reverb instances when
53    /// present.
54    pub fn set_reverb_enabled(&self, enabled: bool) {
55        let mut effects = self.effects.lock().unwrap();
56        if let Some(effect) = effects
57            .iter_mut()
58            .find_map(|effect| effect.as_convolution_reverb_mut())
59        {
60            effect.enabled = enabled;
61        }
62        if let Some(effect) = effects
63            .iter_mut()
64            .find_map(|effect| effect.as_delay_reverb_mut())
65        {
66            effect.enabled = enabled;
67        }
68    }
69
70    /// Set the reverb wet/dry mix (clamped to `[0.0, 1.0]`).
71    ///
72    /// The value is mapped across convolution, delay, and diffusion reverb
73    /// variants when those effects are part of the chain.
74    pub fn set_reverb_mix(&self, dry_wet: f32) {
75        let mut effects = self.effects.lock().unwrap();
76        if let Some(effect) = effects
77            .iter_mut()
78            .find_map(|effect| effect.as_convolution_reverb_mut())
79        {
80            effect.dry_wet = dry_wet.clamp(0.0, 1.0);
81        }
82        if let Some(effect) = effects
83            .iter_mut()
84            .find_map(|effect| effect.as_delay_reverb_mut())
85        {
86            effect.mix = dry_wet.clamp(0.0, 1.0);
87        }
88        if let Some(effect) = effects
89            .iter_mut()
90            .find_map(|effect| effect.as_diffusion_reverb_mut())
91        {
92            effect.mix = dry_wet.clamp(0.0, 1.0);
93        }
94    }
95
96    /// Retrieve the current reverb settings snapshot.
97    ///
98    /// Returns a disabled/zeroed snapshot when no known reverb effect exists.
99    pub fn get_reverb_settings(&self) -> ReverbSettingsSnapshot {
100        let effects = self.effects.lock().unwrap();
101        if let Some(effect) = effects
102            .iter()
103            .find_map(|effect| effect.as_convolution_reverb())
104        {
105            return ReverbSettingsSnapshot {
106                enabled: effect.enabled,
107                dry_wet: effect.dry_wet,
108            };
109        }
110        if let Some(effect) = effects
111            .iter()
112            .find_map(|effect| effect.as_diffusion_reverb())
113        {
114            return ReverbSettingsSnapshot {
115                enabled: effect.enabled,
116                dry_wet: effect.mix,
117            };
118        }
119        if let Some(effect) = effects.iter().find_map(|effect| effect.as_delay_reverb()) {
120            return ReverbSettingsSnapshot {
121                enabled: effect.enabled,
122                dry_wet: effect.mix,
123            };
124        }
125        ReverbSettingsSnapshot {
126            enabled: false,
127            dry_wet: 0.0,
128        }
129    }
130
131    /// Snapshot the active effect chain names.
132    ///
133    /// This is primarily intended for diagnostics and UI display.
134    #[allow(deprecated)]
135    pub fn get_effect_names(&self) -> Vec<String> {
136        let effects = self.effects.lock().unwrap();
137        effects
138            .iter()
139            .map(|effect| match effect {
140                AudioEffect::DelayReverb(_) => "DelayReverb".to_string(),
141                AudioEffect::BasicReverb(_) => "DelayReverb".to_string(),
142                AudioEffect::DiffusionReverb(_) => "DiffusionReverb".to_string(),
143                AudioEffect::ConvolutionReverb(_) => "ConvolutionReverb".to_string(),
144                AudioEffect::LowPassFilter(_) => "LowPassFilter".to_string(),
145                AudioEffect::HighPassFilter(_) => "HighPassFilter".to_string(),
146                AudioEffect::Distortion(_) => "Distortion".to_string(),
147                AudioEffect::Gain(_) => "Gain".to_string(),
148                AudioEffect::Compressor(_) => "Compressor".to_string(),
149                AudioEffect::Limiter(_) => "Limiter".to_string(),
150                AudioEffect::MultibandEq(_) => "MultibandEq".to_string(),
151            })
152            .collect()
153    }
154
155    /// Replace the active DSP effects chain.
156    ///
157    /// This method preserves legacy behavior: it forces an effect-state reset
158    /// and re-seeks to the current timestamp so the new chain is applied
159    /// immediately, which also refreshes the sink.
160    ///
161    /// # Arguments
162    ///
163    /// * `effects` - New ordered list of effects to apply.
164    pub fn set_effects(&mut self, effects: Vec<AudioEffect>) {
165        self.clear_inline_effects_update();
166        self.replace_effects_chain(effects);
167        self.request_effects_reset();
168
169        // Seeking to the current timestamp refreshes the sink so that
170        // the new effects are applied immediately.
171        if !self.thread_finished() {
172            let ts = self.get_time();
173            self.seek(ts);
174        }
175    }
176
177    /// Replace the active DSP effects chain inline during playback.
178    ///
179    /// Unlike [`Self::set_effects`], this does not reset effect internals,
180    /// clear effect tails, or rebuild the sink. The updated chain settings are
181    /// used for future chunks processed by the mixing thread, with a short
182    /// internal crossfade to reduce boundary clicks.
183    ///
184    /// # Arguments
185    ///
186    /// * `effects` - New ordered list of effects to apply.
187    pub fn set_effects_inline(&self, effects: Vec<AudioEffect>) {
188        if self.thread_finished() {
189            self.replace_effects_chain(effects);
190            return;
191        }
192
193        let transition_ms = {
194            let settings = self.buffer_settings.lock().unwrap();
195            settings.inline_effects_transition_ms.max(0.0)
196        };
197        let mut pending = self.inline_effects_update.lock().unwrap();
198        *pending = Some(InlineEffectsUpdate::new(effects, transition_ms));
199    }
200
201    /// Retrieve the latest DSP chain performance metrics.
202    ///
203    /// # Returns
204    ///
205    /// A copy of the most recent metrics updated by the playback thread.
206    pub fn get_dsp_metrics(&self) -> DspChainMetrics {
207        *self.dsp_metrics.lock().unwrap()
208    }
209
210    /// Retrieve the most recent per-channel peak levels.
211    pub fn get_levels(&self) -> Vec<f32> {
212        self.output_meter.lock().unwrap().levels()
213    }
214
215    /// Retrieve the most recent per-channel peak levels in dBFS.
216    pub fn get_levels_db(&self) -> Vec<f32> {
217        self.output_meter
218            .lock()
219            .unwrap()
220            .levels()
221            .into_iter()
222            .map(linear_to_dbfs)
223            .collect()
224    }
225
226    /// Retrieve the most recent per-channel average levels.
227    pub fn get_levels_avg(&self) -> Vec<f32> {
228        self.output_meter.lock().unwrap().averages()
229    }
230
231    /// Set the output meter refresh rate (frames per second).
232    pub fn set_output_meter_refresh_hz(&self, hz: f32) {
233        self.output_meter.lock().unwrap().set_refresh_hz(hz);
234    }
235
236    /// Bump the effects reset generation consumed by the runtime engine.
237    pub(super) fn request_effects_reset(&self) {
238        self.effects_reset.fetch_add(1, Ordering::SeqCst);
239    }
240
241    /// Drop any pending inline effects transition update.
242    pub(super) fn clear_inline_effects_update(&self) {
243        let mut pending = self.inline_effects_update.lock().unwrap();
244        pending.take();
245    }
246
247    /// Replace the currently active effect vector atomically.
248    fn replace_effects_chain(&self, effects: Vec<AudioEffect>) {
249        let mut guard = self.effects.lock().unwrap();
250        log::info!("updated effects chain: {} effect(s)", effects.len());
251        *guard = effects;
252    }
253}
254
255fn linear_to_dbfs(value: f32) -> f32 {
256    if value <= 0.0 {
257        f32::NEG_INFINITY
258    } else {
259        20.0 * value.log10()
260    }
261}