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        let new_log = self.current.ln() + self.ratio.ln() * n as f32;
217        self.current = new_log.exp().clamp(1e-7, 1e6);
218
219        // Check if we passed the target
220        if (self.ratio > 1.0 && self.current >= self.target)
221            || (self.ratio < 1.0 && self.current <= self.target)
222        {
223            self.current = self.target;
224            self.ratio = 1.0;
225        }
226
227        self.current
228    }
229
230    #[allow(dead_code)]
231    pub fn reset(&mut self, value: f32) {
232        self.target = value.max(1e-7);
233        self.current = self.target;
234        self.ratio = 1.0;
235    }
236
237    #[allow(dead_code)]
238    pub fn current(&self) -> f32 {
239        self.current
240    }
241
242    pub fn target(&self) -> f32 {
243        self.target
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn test_exponential_smoother() {
253        let mut s = Smoother::new(0.0, 10.0, 1000); // 10ms at 1kHz = 10 samples
254        s.set_target(1.0);
255
256        let first = s.advance();
257        assert!(first > 0.0 && first < 1.0);
258
259        for _ in 0..100 {
260            s.advance();
261        }
262        assert!((s.current() - 1.0).abs() < 1e-4);
263    }
264
265    #[test]
266    fn test_linear_smoother() {
267        let mut s = LinearSmoother::new(0.0, 10.0, 1000); // 10 samples
268        s.set_target(1.0);
269
270        assert!((s.advance() - 0.1).abs() < 1e-6);
271        assert!((s.advance() - 0.2).abs() < 1e-6);
272
273        for _ in 0..7 {
274            s.advance();
275        }
276        assert!((s.advance() - 1.0).abs() < 1e-6);
277        assert!((s.advance() - 1.0).abs() < 1e-6);
278    }
279
280    #[test]
281    fn test_log_smoother() {
282        let mut s = LogSmoother::new(100.0, 10.0, 1000); // 10 samples
283        s.set_target(1000.0);
284
285        let first = s.advance();
286        // 100 * (1000/100)^(1/10) = 100 * 10^0.1 approx 125.89
287        assert!((first - 125.89).abs() < 0.1);
288
289        for _ in 0..8 {
290            s.advance();
291        }
292        assert!((s.advance() - 1000.0).abs() < 1e-3);
293        assert!((s.advance() - 1000.0).abs() < 1e-6);
294    }
295
296    #[test]
297    fn test_log_smoother_large_block_stays_finite() {
298        let mut s = LogSmoother::new(1e-7, 20.0, 48000);
299        s.set_target(1e6);
300
301        let value = s.next_n(4096);
302        assert!(
303            value.is_finite(),
304            "large-block log smoothing must not overflow to inf"
305        );
306        assert!(
307            value <= 1e6,
308            "large-block log smoothing should clamp at target range, got {value}"
309        );
310    }
311
312    #[test]
313    fn test_smoother_sample_rate_zero_no_panic() {
314        // sample_rate=0 should produce coeff=0 (instant jump, no smoothing)
315        let mut s = Smoother::new(1.0, 50.0, 0);
316        assert_eq!(s.current(), 1.0);
317        s.set_target(2.0);
318        // With coeff=0, set_target jumps immediately
319        assert_eq!(s.current(), 2.0);
320        // advance should also be stable
321        let val = s.advance();
322        assert_eq!(val, 2.0);
323        assert!(!val.is_nan());
324        assert!(!val.is_infinite());
325    }
326
327    #[test]
328    fn test_smoother_set_time_sample_rate_zero() {
329        let mut s = Smoother::new(1.0, 10.0, 48000);
330        s.set_time(10.0, 0);
331        s.set_target(5.0);
332        // coeff=0 → instant jump
333        assert_eq!(s.current(), 5.0);
334    }
335}