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