Skip to main content

math_audio_dsp/
smoothing.rs

1// ============================================================================
2// Parameter Smoothing
3// ============================================================================
4
5/// Simple one-pole smoothing filter for control parameters to prevent zipper noise.
6#[derive(Debug, Clone, Copy)]
7pub struct Smoother {
8    target: f32,
9    current: f32,
10    coeff: f32,
11}
12
13#[allow(dead_code)]
14impl Smoother {
15    /// Create a new smoother
16    /// time_ms: Smoothing time constant (e.g., 10ms - 50ms)
17    pub fn new(value: f32, time_ms: f32, sample_rate: u32) -> Self {
18        let coeff = Self::calculate_coeff(time_ms, sample_rate);
19        Self {
20            target: value,
21            current: value,
22            coeff,
23        }
24    }
25
26    fn calculate_coeff(time_ms: f32, sample_rate: u32) -> f32 {
27        if time_ms <= 0.0 || sample_rate == 0 {
28            0.0
29        } else {
30            // Standard one-pole coeff: e^(-1 / (tau * fs))
31            // time_ms is roughly time to reach ~63% of target
32            (-1.0 / (time_ms * 0.001 * sample_rate as f32)).exp()
33        }
34    }
35
36    pub fn set_time(&mut self, time_ms: f32, sample_rate: u32) {
37        self.coeff = Self::calculate_coeff(time_ms, sample_rate);
38    }
39
40    /// Set new target value
41    pub fn set_target(&mut self, value: f32) {
42        self.target = value;
43        // If smoothing is disabled (coeff = 0), jump immediately
44        if self.coeff == 0.0 {
45            self.current = value;
46        }
47    }
48
49    /// Process N sample steps at once (updates current value)
50    #[inline]
51    pub fn next_n(&mut self, n: usize) -> f32 {
52        if self.coeff == 0.0 || (self.current - self.target).abs() < 1e-5 || n == 0 {
53            self.current = self.target;
54        } else {
55            // current = target + coeff^n * (current - target)
56            let block_coeff = self.coeff.powi(n as i32);
57            self.current = self.target + block_coeff * (self.current - self.target);
58        }
59        self.current
60    }
61
62    /// Process one sample step (updates current value)
63    #[inline]
64    pub fn advance(&mut self) -> f32 {
65        self.next_n(1)
66    }
67
68    /// Get current smoothed value
69    #[inline]
70    pub fn current(&self) -> f32 {
71        self.current
72    }
73
74    /// Get target value
75    #[inline]
76    pub fn target(&self) -> f32 {
77        self.target
78    }
79
80    /// Process one sample step (updates current value) - per-sample smoothing
81    /// Returns the smoothed value for this sample
82    #[inline]
83    pub fn process_sample(&mut self, sample: f32) -> f32 {
84        // Apply smoothing to parameter changes, then process input with smoothed gain
85        // This gives smooth parameter transitions AND smooth gain application
86        if (self.current - self.target).abs() < 1e-5 {
87            self.current = self.target;
88        } else {
89            self.current = self.target + self.coeff * (self.current - self.target);
90        }
91        sample * self.current
92    }
93
94    /// Reset to value immediately
95    pub fn reset(&mut self, value: f32) {
96        self.target = value;
97        self.current = value;
98    }
99}
100
101/// Linear smoother for control parameters.
102/// Changes the value by a constant amount per sample.
103#[derive(Debug, Clone, Copy)]
104pub struct LinearSmoother {
105    target: f32,
106    current: f32,
107    step: f32,
108    sample_rate: u32,
109    time_ms: f32,
110}
111
112impl LinearSmoother {
113    pub fn new(value: f32, time_ms: f32, sample_rate: u32) -> Self {
114        Self {
115            target: value,
116            current: value,
117            step: 0.0,
118            sample_rate,
119            time_ms,
120        }
121    }
122
123    pub fn set_target(&mut self, value: f32) {
124        self.target = value;
125        if self.time_ms <= 0.0 {
126            self.current = value;
127            self.step = 0.0;
128        } else {
129            let samples = (self.time_ms * 0.001 * self.sample_rate as f32).max(1.0);
130            self.step = (self.target - self.current) / samples;
131        }
132    }
133
134    #[inline]
135    pub fn advance(&mut self) -> f32 {
136        self.next_n(1)
137    }
138
139    #[inline]
140    pub fn next_n(&mut self, n: usize) -> f32 {
141        if n == 0 {
142            return self.current;
143        }
144        let total_step = self.step * n as f32;
145        if (self.current - self.target).abs() <= total_step.abs() {
146            self.current = self.target;
147            self.step = 0.0;
148        } else {
149            self.current += total_step;
150        }
151        self.current
152    }
153
154    #[allow(dead_code)]
155    pub fn reset(&mut self, value: f32) {
156        self.target = value;
157        self.current = value;
158        self.step = 0.0;
159    }
160
161    #[allow(dead_code)]
162    pub fn current(&self) -> f32 {
163        self.current
164    }
165
166    pub fn target(&self) -> f32 {
167        self.target
168    }
169}
170
171/// Logarithmic smoother for frequency or dB parameters.
172/// Changes the value by a constant ratio per sample.
173#[derive(Debug, Clone, Copy)]
174pub struct LogSmoother {
175    target: f32,
176    current: f32,
177    ratio: f32,
178    sample_rate: u32,
179    time_ms: f32,
180}
181
182impl LogSmoother {
183    pub fn new(value: f32, time_ms: f32, sample_rate: u32) -> Self {
184        Self {
185            target: value.max(1e-7),
186            current: value.max(1e-7),
187            ratio: 1.0,
188            sample_rate,
189            time_ms,
190        }
191    }
192
193    pub fn set_target(&mut self, value: f32) {
194        self.target = value.max(1e-7);
195        if self.time_ms <= 0.0 {
196            self.current = self.target;
197            self.ratio = 1.0;
198        } else {
199            let samples = (self.time_ms * 0.001 * self.sample_rate as f32).max(1.0);
200            // target = current * ratio^samples  => ratio = (target/current)^(1/samples)
201            self.ratio = (self.target / self.current).powf(1.0 / samples);
202        }
203    }
204
205    #[inline]
206    pub fn advance(&mut self) -> f32 {
207        self.next_n(1)
208    }
209
210    #[inline]
211    pub fn next_n(&mut self, n: usize) -> f32 {
212        if self.ratio == 1.0 || n == 0 {
213            return self.current;
214        }
215
216        self.current *= self.ratio.powi(n as i32);
217
218        // Check if we passed the target
219        if (self.ratio > 1.0 && self.current >= self.target)
220            || (self.ratio < 1.0 && self.current <= self.target)
221        {
222            self.current = self.target;
223            self.ratio = 1.0;
224        }
225
226        self.current
227    }
228
229    #[allow(dead_code)]
230    pub fn reset(&mut self, value: f32) {
231        self.target = value.max(1e-7);
232        self.current = self.target;
233        self.ratio = 1.0;
234    }
235
236    #[allow(dead_code)]
237    pub fn current(&self) -> f32 {
238        self.current
239    }
240
241    pub fn target(&self) -> f32 {
242        self.target
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn test_exponential_smoother() {
252        let mut s = Smoother::new(0.0, 10.0, 1000); // 10ms at 1kHz = 10 samples
253        s.set_target(1.0);
254
255        let first = s.advance();
256        assert!(first > 0.0 && first < 1.0);
257
258        for _ in 0..100 {
259            s.advance();
260        }
261        assert!((s.current() - 1.0).abs() < 1e-4);
262    }
263
264    #[test]
265    fn test_linear_smoother() {
266        let mut s = LinearSmoother::new(0.0, 10.0, 1000); // 10 samples
267        s.set_target(1.0);
268
269        assert!((s.advance() - 0.1).abs() < 1e-6);
270        assert!((s.advance() - 0.2).abs() < 1e-6);
271
272        for _ in 0..7 {
273            s.advance();
274        }
275        assert!((s.advance() - 1.0).abs() < 1e-6);
276        assert!((s.advance() - 1.0).abs() < 1e-6);
277    }
278
279    #[test]
280    fn test_log_smoother() {
281        let mut s = LogSmoother::new(100.0, 10.0, 1000); // 10 samples
282        s.set_target(1000.0);
283
284        let first = s.advance();
285        // 100 * (1000/100)^(1/10) = 100 * 10^0.1 approx 125.89
286        assert!((first - 125.89).abs() < 0.1);
287
288        for _ in 0..8 {
289            s.advance();
290        }
291        assert!((s.advance() - 1000.0).abs() < 1e-4);
292        assert!((s.advance() - 1000.0).abs() < 1e-6);
293    }
294
295    #[test]
296    fn test_smoother_sample_rate_zero_no_panic() {
297        // sample_rate=0 should produce coeff=0 (instant jump, no smoothing)
298        let mut s = Smoother::new(1.0, 50.0, 0);
299        assert_eq!(s.current(), 1.0);
300        s.set_target(2.0);
301        // With coeff=0, set_target jumps immediately
302        assert_eq!(s.current(), 2.0);
303        // advance should also be stable
304        let val = s.advance();
305        assert_eq!(val, 2.0);
306        assert!(!val.is_nan());
307        assert!(!val.is_infinite());
308    }
309
310    #[test]
311    fn test_smoother_set_time_sample_rate_zero() {
312        let mut s = Smoother::new(1.0, 10.0, 48000);
313        s.set_time(10.0, 0);
314        s.set_target(5.0);
315        // coeff=0 → instant jump
316        assert_eq!(s.current(), 5.0);
317    }
318}