Skip to main content

proteus_lib/dsp/effects/
gain.rs

1//! Simple gain effect.
2
3use serde::{Deserialize, Serialize};
4
5use super::level::deserialize_linear_gain;
6use super::EffectContext;
7
8const DEFAULT_GAIN: f32 = 1.0;
9
10/// Serialized configuration for gain parameters.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(default)]
13pub struct GainSettings {
14    #[serde(deserialize_with = "deserialize_linear_gain")]
15    pub gain: f32,
16}
17
18impl GainSettings {
19    /// Create a gain settings payload.
20    pub fn new(gain: f32) -> Self {
21        Self { gain }
22    }
23}
24
25impl Default for GainSettings {
26    fn default() -> Self {
27        Self { gain: DEFAULT_GAIN }
28    }
29}
30
31/// Configured gain effect.
32#[derive(Clone, Serialize, Deserialize)]
33#[serde(default)]
34pub struct GainEffect {
35    pub enabled: bool,
36    #[serde(flatten)]
37    pub settings: GainSettings,
38}
39
40impl std::fmt::Debug for GainEffect {
41    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        f.debug_struct("GainEffect")
43            .field("enabled", &self.enabled)
44            .field("settings", &self.settings)
45            .finish()
46    }
47}
48
49impl Default for GainEffect {
50    fn default() -> Self {
51        Self {
52            enabled: false,
53            settings: GainSettings::default(),
54        }
55    }
56}
57
58impl GainEffect {
59    /// Process interleaved samples through the gain effect.
60    ///
61    /// # Arguments
62    /// - `samples`: Interleaved input samples.
63    /// - `context`: Environment details (unused for this effect).
64    /// - `drain`: Unused for this effect.
65    ///
66    /// # Returns
67    /// Processed interleaved samples.
68    pub fn process(&mut self, samples: &[f32], _context: &EffectContext, _drain: bool) -> Vec<f32> {
69        if !self.enabled {
70            return samples.to_vec();
71        }
72
73        let gain = sanitize_gain(self.settings.gain);
74        if samples.is_empty() {
75            return Vec::new();
76        }
77
78        let mut out = Vec::with_capacity(samples.len());
79        for &sample in samples {
80            out.push(sample * gain);
81        }
82
83        out
84    }
85
86    /// Reset any internal state (none for gain).
87    pub fn reset_state(&mut self) {}
88}
89
90#[cfg(test)]
91mod tests {
92    use super::super::level::db_to_linear;
93    use super::*;
94
95    fn context() -> EffectContext {
96        EffectContext {
97            sample_rate: 44_100,
98            channels: 1,
99            container_path: None,
100            impulse_response_spec: None,
101            impulse_response_tail_db: -60.0,
102        }
103    }
104
105    #[test]
106    fn gain_disabled_passthrough() {
107        let mut effect = GainEffect::default();
108        let samples = vec![0.25_f32, -0.25, 0.5, -0.5];
109        let output = effect.process(&samples, &context(), false);
110        assert_eq!(output, samples);
111    }
112
113    #[test]
114    fn gain_scales_samples() {
115        let mut effect = GainEffect::default();
116        effect.enabled = true;
117        effect.settings.gain = 2.0;
118        let samples = vec![0.25_f32, -0.25, 0.5, -0.5];
119        let output = effect.process(&samples, &context(), false);
120        assert_eq!(output, vec![0.5_f32, -0.5, 1.0, -1.0]);
121    }
122
123    #[test]
124    fn gain_deserializes_db_strings() {
125        let json = r#"{"enabled":true,"gain":"6db"}"#;
126        let effect: GainEffect = serde_json::from_str(json).expect("deserialize gain");
127        let expected = db_to_linear(6.0);
128        assert!((effect.settings.gain - expected).abs() < 1e-6);
129    }
130
131    #[test]
132    fn gain_deserializes_negative_db_strings() {
133        let json = r#"{"enabled":true,"gain":"-2db"}"#;
134        let effect: GainEffect = serde_json::from_str(json).expect("deserialize gain");
135        let expected = db_to_linear(-2.0);
136        assert!((effect.settings.gain - expected).abs() < 1e-6);
137    }
138}
139
140fn sanitize_gain(gain: f32) -> f32 {
141    if gain.is_finite() {
142        gain
143    } else {
144        DEFAULT_GAIN
145    }
146}