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::{ActionContext, 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, &ctx).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    pub fn next(&mut self) -> T {
75        let diff = self.target.sub(self.current);
76        let step = diff.mul(self.coeff);
77        self.current = self.current.add(step);
78        self.current
79    }
80}
81
82impl<T: Transcendental> Algorithm<T> for ParamSmoother<T> {
83    fn process(
84        &mut self,
85        _input: Option<&[T]>,
86        output: &mut [T],
87        _ctx: &ActionContext,
88    ) -> ProcessResult<()> {
89        for sample in output.iter_mut() {
90            *sample = self.next();
91        }
92        Ok(())
93    }
94
95    fn apply_command(&mut self, value: T) {
96        self.target = value;
97    }
98
99    fn init(&mut self, _sample_rate: f32) {}
100
101    fn reset(&mut self) {
102        self.current = T::ZERO;
103        self.target = T::ZERO;
104    }
105
106    fn metadata(&self) -> AlgorithmMetadata {
107        AlgorithmMetadata {
108            name: "ParamSmoother",
109            category: AlgorithmCategory::Utility,
110            description: "One-pole smoother for zipper-free parameter transitions",
111            author: "Rill",
112            version: "0.1.0",
113        }
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use rill_core::time::ClockTick;
121
122    #[test]
123    fn test_smoother_basic() {
124        let mut s = ParamSmoother::new(0.5f32);
125        let tick = ClockTick::default();
126        let ctx = ActionContext::new(&tick);
127
128        s.apply_command(1.0);
129        let mut buf = [0.0f32; 4];
130        s.process(None, &mut buf, &ctx).unwrap();
131        // 0 + (1-0)*0.5 = 0.5
132        assert!((buf[0] - 0.5).abs() < 1e-6);
133        // 0.5 + (1-0.5)*0.5 = 0.75
134        assert!((buf[1] - 0.75).abs() < 1e-6);
135    }
136
137    #[test]
138    fn test_smoother_snap() {
139        let mut s = ParamSmoother::new(0.1f32);
140        s.snap_to(42.0);
141        assert!((s.current() - 42.0).abs() < 1e-6);
142        assert!((s.target() - 42.0).abs() < 1e-6);
143    }
144
145    #[test]
146    fn test_smoother_empty_block() {
147        let mut s = ParamSmoother::new(0.1f32);
148        let tick = ClockTick::default();
149        let ctx = ActionContext::new(&tick);
150        let buf: &mut [f32] = &mut [];
151        assert!(s.process(None, buf, &ctx).is_ok());
152    }
153}