Skip to main content

rill_core_dsp/
smoothing.rs

1//! ParamSmoother — one-pole smoother that implements `Algorithm<T>`.
2//!
3//! This is useful for smoothing parameter changes to avoid zipper noise.
4
5use rill_core::math::Transcendental;
6use rill_core::traits::ProcessResult;
7use rill_core::traits::{Algorithm, AlgorithmCategory, AlgorithmMetadata};
8
9/// One-pole exponential smoother that implements `Algorithm<T>`.
10///
11/// Receives target values via `apply_command(value)`. Each `process()` call
12/// steps the current value toward the target using the smoothing coefficient.
13///
14/// # Example
15/// ```rust
16/// use rill_core_dsp::smoothing::ParamSmoother;
17/// use rill_core::traits::Algorithm;
18/// use rill_core::time::ClockTick;
19/// use rill_core::traits::ActionContext;
20///
21/// let mut smoother = ParamSmoother::new(0.1);
22/// let tick = ClockTick::default();
23/// let ctx = ActionContext::new(&tick);
24///
25/// smoother.apply_command(1.0);
26/// let mut output = [0.0f32; 4];
27/// smoother.process(None, &mut output).unwrap();
28/// // output approaches 1.0 via exponential smoothing
29/// ```
30#[derive(Debug, Clone)]
31pub struct ParamSmoother<T: Transcendental> {
32    /// Current (smoothed) value
33    current: T,
34    /// Target value
35    target: T,
36    /// Smoothing coefficient (0.0 = no smoothing, 1.0 = instant)
37    coeff: T,
38}
39
40impl<T: Transcendental> ParamSmoother<T> {
41    /// Create a new smoother with the given coefficient.
42    ///
43    /// `coeff` should be in (0, 1]. Lower values = slower smoothing.
44    pub fn new(coeff: T) -> Self {
45        Self {
46            current: T::ZERO,
47            target: T::ZERO,
48            coeff,
49        }
50    }
51
52    /// Set the smoothing coefficient.
53    pub fn set_coeff(&mut self, coeff: T) {
54        self.coeff = coeff;
55    }
56
57    /// Get the current smoothed value (without processing).
58    pub fn current(&self) -> T {
59        self.current
60    }
61
62    /// Get the current target value.
63    pub fn target(&self) -> T {
64        self.target
65    }
66
67    /// Immediately snap to a value (skip smoothing).
68    pub fn snap_to(&mut self, value: T) {
69        self.current = value;
70        self.target = value;
71    }
72
73    /// Process a single sample value (useful outside the Algorithm interface).
74    #[allow(clippy::should_implement_trait)]
75    pub fn next(&mut self) -> T {
76        let diff = self.target.sub(self.current);
77        let step = diff.mul(self.coeff);
78        self.current = self.current.add(step);
79        self.current
80    }
81}
82
83impl<T: Transcendental> Algorithm<T> for ParamSmoother<T> {
84    fn process(&mut self, _input: Option<&[T]>, output: &mut [T]) -> ProcessResult<()> {
85        for sample in output.iter_mut() {
86            *sample = self.next();
87        }
88        Ok(())
89    }
90
91    fn apply_command(&mut self, value: T) {
92        self.target = value;
93    }
94
95    fn init(&mut self, _sample_rate: f32) {}
96
97    fn reset(&mut self) {
98        self.current = T::ZERO;
99        self.target = T::ZERO;
100    }
101
102    fn metadata(&self) -> AlgorithmMetadata {
103        AlgorithmMetadata {
104            name: "ParamSmoother",
105            category: AlgorithmCategory::Utility,
106            description: "One-pole smoother for zipper-free parameter transitions",
107            author: "Rill",
108            version: "0.1.0",
109        }
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_smoother_basic() {
119        let mut s = ParamSmoother::new(0.5f32);
120
121        s.apply_command(1.0);
122        let mut buf = [0.0f32; 4];
123        s.process(None, &mut buf).unwrap();
124        // 0 + (1-0)*0.5 = 0.5
125        assert!((buf[0] - 0.5).abs() < 1e-6);
126        // 0.5 + (1-0.5)*0.5 = 0.75
127        assert!((buf[1] - 0.75).abs() < 1e-6);
128    }
129
130    #[test]
131    fn test_smoother_snap() {
132        let mut s = ParamSmoother::new(0.1f32);
133        s.snap_to(42.0);
134        assert!((s.current() - 42.0).abs() < 1e-6);
135        assert!((s.target() - 42.0).abs() < 1e-6);
136    }
137
138    #[test]
139    fn test_smoother_empty_block() {
140        let mut s = ParamSmoother::new(0.1f32);
141        let buf: &mut [f32] = &mut [];
142        assert!(s.process(None, buf).is_ok());
143    }
144}