rill_core_dsp/
smoothing.rs1use rill_core::math::Transcendental;
6use rill_core::traits::ProcessResult;
7use rill_core::traits::{ActionContext, Algorithm, AlgorithmCategory, AlgorithmMetadata};
8
9#[derive(Debug, Clone)]
31pub struct ParamSmoother<T: Transcendental> {
32 current: T,
34 target: T,
36 coeff: T,
38}
39
40impl<T: Transcendental> ParamSmoother<T> {
41 pub fn new(coeff: T) -> Self {
45 Self {
46 current: T::ZERO,
47 target: T::ZERO,
48 coeff,
49 }
50 }
51
52 pub fn set_coeff(&mut self, coeff: T) {
54 self.coeff = coeff;
55 }
56
57 pub fn current(&self) -> T {
59 self.current
60 }
61
62 pub fn target(&self) -> T {
64 self.target
65 }
66
67 pub fn snap_to(&mut self, value: T) {
69 self.current = value;
70 self.target = value;
71 }
72
73 #[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 assert!((buf[0] - 0.5).abs() < 1e-6);
134 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}