Skip to main content

truce_params/
smooth.rs

1use std::sync::atomic::{AtomicU64, Ordering};
2
3/// Smoothing style for a parameter.
4#[derive(Clone, Copy, Debug)]
5pub enum SmoothingStyle {
6    None,
7    Linear(f64),
8    Exponential(f64),
9}
10
11/// Atomic f64 for smoother fields (same pattern as AtomicF64 in types.rs).
12struct SmoothAtomic {
13    bits: AtomicU64,
14}
15
16impl SmoothAtomic {
17    fn new(value: f64) -> Self {
18        Self {
19            bits: AtomicU64::new(value.to_bits()),
20        }
21    }
22
23    #[inline]
24    fn get(&self) -> f64 {
25        f64::from_bits(self.bits.load(Ordering::Relaxed))
26    }
27
28    #[inline]
29    fn set(&self, value: f64) {
30        self.bits.store(value.to_bits(), Ordering::Relaxed);
31    }
32}
33
34/// Per-parameter smoother. All methods take `&self` for interior mutability,
35/// enabling use through `Arc<Params>`. Thread safety relies on the host
36/// guarantee that `process()` and `reset()` never run concurrently.
37pub struct Smoother {
38    style: SmoothingStyle,
39    current: SmoothAtomic,
40    target: SmoothAtomic,
41    coeff: SmoothAtomic,
42    sample_rate: SmoothAtomic,
43}
44
45impl Smoother {
46    pub fn new(style: SmoothingStyle) -> Self {
47        Self {
48            style,
49            current: SmoothAtomic::new(0.0),
50            target: SmoothAtomic::new(0.0),
51            coeff: SmoothAtomic::new(0.0),
52            sample_rate: SmoothAtomic::new(44100.0),
53        }
54    }
55
56    pub fn set_sample_rate(&self, sr: f64) {
57        self.sample_rate.set(sr);
58        self.recalculate_coeff();
59    }
60
61    /// Snap to a value immediately (used on reset/init).
62    pub fn snap(&self, value: f64) {
63        self.current.set(value);
64        self.target.set(value);
65    }
66
67    /// Get next smoothed value, advancing one sample.
68    #[inline]
69    pub fn next(&self, target: f64) -> f32 {
70        self.target.set(target);
71        let current = self.current.get();
72        let coeff = self.coeff.get();
73
74        let new_current = match self.style {
75            SmoothingStyle::None => target,
76            SmoothingStyle::Linear(_) => {
77                let diff = target - current;
78                if diff.abs() < 1e-8 {
79                    target
80                } else {
81                    let step = diff * coeff;
82                    if step.abs() >= diff.abs() {
83                        target
84                    } else {
85                        current + step
86                    }
87                }
88            }
89            SmoothingStyle::Exponential(_) => current + coeff * (target - current),
90        };
91
92        self.current.set(new_current);
93        new_current as f32
94    }
95
96    /// Current smoothed value without advancing.
97    #[inline]
98    pub fn current(&self) -> f32 {
99        self.current.get() as f32
100    }
101
102    fn recalculate_coeff(&self) {
103        let sr = self.sample_rate.get();
104        let new_coeff = match self.style {
105            SmoothingStyle::None => 1.0,
106            SmoothingStyle::Linear(ms) => {
107                let samples = (ms / 1000.0) * sr;
108                if samples > 1.0 {
109                    1.0 / samples
110                } else {
111                    1.0
112                }
113            }
114            SmoothingStyle::Exponential(ms) => {
115                let samples = (ms / 1000.0) * sr;
116                if samples > 0.0 {
117                    1.0 - (-1.0 / samples).exp()
118                } else {
119                    1.0
120                }
121            }
122        };
123        self.coeff.set(new_coeff);
124    }
125}