Skip to main content

aether_core/
param.rs

1//! Sample-accurate parameter automation.
2//!
3//! Each Param smooths from `current` toward `target` over a fixed ramp.
4//! No allocations. No locks. Safe to read/write from the RT thread.
5
6/// A single smoothed parameter.
7/// Uses a linear ramp: current += step each sample until target is reached.
8#[derive(Debug, Clone, Copy)]
9#[repr(C)]
10pub struct Param {
11    pub current: f32,
12    pub target: f32,
13    /// Per-sample increment. Set by `set_target`.
14    pub step: f32,
15}
16
17impl Param {
18    pub fn new(value: f32) -> Self {
19        Self {
20            current: value,
21            target: value,
22            step: 0.0,
23        }
24    }
25
26    /// Schedule a ramp to `target` over `ramp_samples` samples.
27    /// Call from the control thread before pushing a `UpdateParam` command.
28    #[inline]
29    pub fn set_target(&mut self, target: f32, ramp_samples: u32) {
30        self.target = target;
31        if ramp_samples == 0 {
32            self.current = target;
33            self.step = 0.0;
34        } else {
35            self.step = (target - self.current) / ramp_samples as f32;
36        }
37    }
38
39    /// Advance by one sample. Call once per sample in the RT loop.
40    #[inline(always)]
41    pub fn tick(&mut self) {
42        if self.step != 0.0 {
43            self.current += self.step;
44            // Clamp overshoot.
45            if (self.step > 0.0 && self.current >= self.target)
46                || (self.step < 0.0 && self.current <= self.target)
47            {
48                self.current = self.target;
49                self.step = 0.0;
50            }
51        }
52    }
53
54    /// Advance by a full buffer, returning per-sample values into `out`.
55    /// Uses a fast path when the parameter is not ramping (step == 0).
56    #[inline]
57    pub fn fill_buffer(&mut self, out: &mut [f32]) {
58        if self.step == 0.0 {
59            // Fast path: parameter is stable — fill with a single value.
60            // This is the common case and avoids all branching in the loop.
61            out.fill(self.current);
62        } else {
63            // Ramping path: advance sample by sample.
64            for sample in out.iter_mut() {
65                *sample = self.current;
66                self.tick();
67            }
68        }
69    }
70}
71
72/// A fixed-size block of parameters for a node.
73/// Sized to fit common DSP nodes without heap allocation.
74#[derive(Debug, Clone, Copy)]
75pub struct ParamBlock {
76    pub params: [Param; 8],
77    pub count: usize,
78}
79
80impl ParamBlock {
81    pub fn new() -> Self {
82        Self {
83            params: [Param::new(0.0); 8],
84            count: 0,
85        }
86    }
87
88    pub fn add(&mut self, value: f32) -> usize {
89        let idx = self.count;
90        self.params[idx] = Param::new(value);
91        self.count += 1;
92        idx
93    }
94
95    #[inline(always)]
96    pub fn get(&self, idx: usize) -> &Param {
97        &self.params[idx]
98    }
99
100    #[inline(always)]
101    pub fn get_mut(&mut self, idx: usize) -> &mut Param {
102        &mut self.params[idx]
103    }
104
105    /// Tick all active params by one sample.
106    #[inline(always)]
107    pub fn tick_all(&mut self) {
108        for p in self.params[..self.count].iter_mut() {
109            p.tick();
110        }
111    }
112}
113
114impl Default for ParamBlock {
115    fn default() -> Self {
116        Self::new()
117    }
118}