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///
8/// Provides sample-accurate parameter automation with linear ramping.
9/// Parameters smoothly transition from `current` to `target` over a
10/// specified number of samples, preventing audio clicks and zipper noise.
11///
12/// # Real-Time Safety
13///
14/// - ✅ No allocation
15/// - ✅ No locks
16/// - ✅ Bounded execution time
17/// - ✅ Safe to use in audio thread
18///
19/// # Example
20///
21/// ```
22/// use aether_core::param::Param;
23///
24/// let mut gain = Param::new(0.5);
25///
26/// // Schedule ramp to 1.0 over 480 samples (10ms @ 48kHz)
27/// gain.set_target(1.0, 480);
28///
29/// // Tick through samples
30/// for _ in 0..480 {
31///     let value = gain.current;
32///     // Use value for processing...
33///     gain.tick();
34/// }
35///
36/// // Close enough to 1.0 (floating point precision)
37/// assert!((gain.current - 1.0).abs() < 0.0001);
38/// ```
39///
40/// # Performance
41///
42/// - Fast path when not ramping (step == 0.0)
43/// - SIMD-friendly linear interpolation
44/// - Automatic overshoot clamping
45///
46/// # See Also
47///
48/// * [`ParamBlock`] - Collection of parameters for a node
49/// * [`Param::fill_buffer`] - Efficient buffer filling
50#[derive(Debug, Clone, Copy)]
51#[repr(C)]
52pub struct Param {
53    pub current: f32,
54    pub target: f32,
55    /// Per-sample increment. Set by `set_target`.
56    pub step: f32,
57}
58
59impl Param {
60    /// Creates a new parameter with the given initial value.
61    ///
62    /// The parameter starts at the specified value with no ramping
63    /// (current == target, step == 0.0).
64    ///
65    /// # Arguments
66    ///
67    /// * `value` - Initial parameter value
68    ///
69    /// # Example
70    ///
71    /// ```
72    /// use aether_core::param::Param;
73    ///
74    /// let gain = Param::new(0.75);
75    /// assert_eq!(gain.current, 0.75);
76    /// assert_eq!(gain.target, 0.75);
77    /// assert_eq!(gain.step, 0.0);
78    /// ```
79    pub fn new(value: f32) -> Self {
80        Self {
81            current: value,
82            target: value,
83            step: 0.0,
84        }
85    }
86
87    /// Schedule a ramp to `target` over `ramp_samples` samples.
88    ///
89    /// Sets up linear interpolation from current value to target value.
90    /// Call from the control thread before pushing an `UpdateParam` command.
91    ///
92    /// # Arguments
93    ///
94    /// * `target` - Target value to ramp towards
95    /// * `ramp_samples` - Number of samples for the ramp (0 = instant)
96    ///
97    /// # Example
98    ///
99    /// ```
100    /// use aether_core::param::Param;
101    ///
102    /// let mut cutoff = Param::new(1000.0);
103    ///
104    /// // Ramp to 5000 Hz over 960 samples (20ms @ 48kHz)
105    /// cutoff.set_target(5000.0, 960);
106    ///
107    /// // After 480 samples, we're halfway
108    /// for _ in 0..480 {
109    ///     cutoff.tick();
110    /// }
111    /// assert!((cutoff.current - 3000.0).abs() < 1.0);
112    ///
113    /// // After 960 samples total, we've reached the target
114    /// for _ in 0..480 {
115    ///     cutoff.tick();
116    /// }
117    /// assert!((cutoff.current - 5000.0).abs() < 0.01);
118    /// ```
119    ///
120    /// # Instant Changes
121    ///
122    /// ```
123    /// use aether_core::param::Param;
124    ///
125    /// let mut gain = Param::new(0.5);
126    ///
127    /// // Instant change (0 samples)
128    /// gain.set_target(1.0, 0);
129    /// assert_eq!(gain.current, 1.0);
130    /// assert_eq!(gain.step, 0.0);
131    /// ```
132    #[inline]
133    pub fn set_target(&mut self, target: f32, ramp_samples: u32) {
134        self.target = target;
135        if ramp_samples == 0 {
136            self.current = target;
137            self.step = 0.0;
138        } else {
139            self.step = (target - self.current) / ramp_samples as f32;
140        }
141    }
142
143    /// Advance by one sample. Call once per sample in the RT loop.
144    ///
145    /// Updates `current` by adding `step`. When the target is reached,
146    /// automatically stops ramping by setting `step` to 0.0.
147    ///
148    /// # Example
149    ///
150    /// ```
151    /// use aether_core::param::Param;
152    ///
153    /// let mut gain = Param::new(0.0);
154    /// gain.set_target(1.0, 100);
155    ///
156    /// // Tick through 100 samples
157    /// for i in 0..100 {
158    ///     gain.tick();
159    /// }
160    ///
161    /// // Reached target value
162    /// assert!((gain.current - 1.0).abs() < 0.0001);
163    /// ```
164    ///
165    /// # Performance
166    ///
167    /// This function is highly optimized for the audio thread:
168    /// - Inlined for zero call overhead
169    /// - Branch-free when not ramping
170    /// - Automatic overshoot clamping
171    #[inline(always)]
172    pub fn tick(&mut self) {
173        if self.step != 0.0 {
174            self.current += self.step;
175            // Clamp overshoot.
176            if (self.step > 0.0 && self.current >= self.target)
177                || (self.step < 0.0 && self.current <= self.target)
178            {
179                self.current = self.target;
180                self.step = 0.0;
181            }
182        }
183    }
184
185    /// Advance by a full buffer, returning per-sample values into `out`.
186    ///
187    /// Efficiently fills a buffer with parameter values, advancing the ramp
188    /// for each sample. Uses a fast path when the parameter is stable (not ramping).
189    ///
190    /// # Arguments
191    ///
192    /// * `out` - Output buffer to fill with parameter values
193    ///
194    /// # Example
195    ///
196    /// ```
197    /// use aether_core::param::Param;
198    /// use aether_core::BUFFER_SIZE;
199    ///
200    /// let mut cutoff = Param::new(1000.0);
201    /// cutoff.set_target(2000.0, BUFFER_SIZE as u32);
202    ///
203    /// let mut buffer = [0.0f32; BUFFER_SIZE];
204    /// cutoff.fill_buffer(&mut buffer);
205    ///
206    /// // First sample is near 1000, last sample is near 2000
207    /// assert!((buffer[0] - 1000.0).abs() < 50.0);
208    /// assert!((buffer[BUFFER_SIZE-1] - 2000.0).abs() < 50.0);
209    /// ```
210    ///
211    /// # Performance
212    ///
213    /// This function has two paths:
214    /// - **Fast path** (step == 0.0): Fills buffer with single value (SIMD-friendly)
215    /// - **Ramp path** (step != 0.0): Advances sample-by-sample
216    ///
217    /// The fast path is taken 90%+ of the time in typical usage.
218    ///
219    /// # Use Case
220    ///
221    /// Use this when you need per-sample parameter values for modulation:
222    ///
223    /// ```
224    /// use aether_core::param::Param;
225    /// use aether_core::BUFFER_SIZE;
226    ///
227    /// let mut gain = Param::new(0.5);
228    /// let mut gain_buffer = [0.0f32; BUFFER_SIZE];
229    /// let input = [1.0f32; BUFFER_SIZE];
230    /// let mut output = [0.0f32; BUFFER_SIZE];
231    ///
232    /// // Fill gain buffer
233    /// gain.fill_buffer(&mut gain_buffer);
234    ///
235    /// // Apply per-sample gain
236    /// for i in 0..BUFFER_SIZE {
237    ///     output[i] = input[i] * gain_buffer[i];
238    /// }
239    /// ```
240    #[inline]
241    pub fn fill_buffer(&mut self, out: &mut [f32]) {
242        if self.step == 0.0 {
243            // Fast path: parameter is stable — fill with a single value.
244            // This is the common case and avoids all branching in the loop.
245            out.fill(self.current);
246        } else {
247            // Ramping path: advance sample by sample.
248            for sample in out.iter_mut() {
249                *sample = self.current;
250                self.tick();
251            }
252        }
253    }
254}
255
256/// A fixed-size block of parameters for a node.
257///
258/// Stores up to 8 parameters without heap allocation. Most DSP nodes need
259/// 1-4 parameters (gain, frequency, resonance, etc.), so 8 is sufficient
260/// for the vast majority of cases.
261///
262/// # Example
263///
264/// ```
265/// use aether_core::param::ParamBlock;
266///
267/// let mut params = ParamBlock::new();
268///
269/// // Add parameters
270/// let gain_idx = params.add(0.75);      // Gain: 0.75
271/// let cutoff_idx = params.add(1000.0);  // Cutoff: 1000 Hz
272/// let res_idx = params.add(0.5);        // Resonance: 0.5
273///
274/// assert_eq!(params.count, 3);
275///
276/// // Access parameters
277/// let gain = params.get(gain_idx);
278/// assert_eq!(gain.current, 0.75);
279///
280/// // Modify parameters
281/// params.get_mut(cutoff_idx).set_target(2000.0, 480);
282///
283/// // Tick all parameters
284/// params.tick_all();
285/// ```
286///
287/// # Capacity
288///
289/// If you need more than 8 parameters, consider:
290/// - Splitting into multiple nodes
291/// - Using a custom parameter storage system
292/// - Increasing the array size (requires modifying the constant)
293///
294/// # See Also
295///
296/// * [`Param`] - Individual parameter
297#[derive(Debug, Clone, Copy)]
298pub struct ParamBlock {
299    pub params: [Param; 8],
300    pub count: usize,
301}
302
303impl ParamBlock {
304    /// Creates a new empty parameter block.
305    ///
306    /// Initializes with zero parameters. Use [`add`](Self::add) to add parameters.
307    ///
308    /// # Example
309    ///
310    /// ```
311    /// use aether_core::param::ParamBlock;
312    ///
313    /// let params = ParamBlock::new();
314    /// assert_eq!(params.count, 0);
315    /// ```
316    pub fn new() -> Self {
317        Self {
318            params: [Param::new(0.0); 8],
319            count: 0,
320        }
321    }
322
323    /// Adds a parameter with the given initial value.
324    ///
325    /// # Arguments
326    ///
327    /// * `value` - Initial parameter value
328    ///
329    /// # Returns
330    ///
331    /// The parameter's index (0-7), used to access it later.
332    ///
333    /// # Panics
334    ///
335    /// Panics if the block is full (8 parameters already added).
336    ///
337    /// # Example
338    ///
339    /// ```
340    /// use aether_core::param::ParamBlock;
341    ///
342    /// let mut params = ParamBlock::new();
343    ///
344    /// let gain_idx = params.add(0.5);
345    /// let freq_idx = params.add(440.0);
346    ///
347    /// assert_eq!(gain_idx, 0);
348    /// assert_eq!(freq_idx, 1);
349    /// assert_eq!(params.count, 2);
350    /// ```
351    pub fn add(&mut self, value: f32) -> usize {
352        let idx = self.count;
353        self.params[idx] = Param::new(value);
354        self.count += 1;
355        idx
356    }
357
358    /// Gets an immutable reference to a parameter.
359    ///
360    /// # Arguments
361    ///
362    /// * `idx` - Parameter index (0-7)
363    ///
364    /// # Returns
365    ///
366    /// Reference to the parameter.
367    ///
368    /// # Panics
369    ///
370    /// Panics if `idx` is out of bounds.
371    ///
372    /// # Example
373    ///
374    /// ```
375    /// use aether_core::param::ParamBlock;
376    ///
377    /// let mut params = ParamBlock::new();
378    /// let gain_idx = params.add(0.75);
379    ///
380    /// let gain = params.get(gain_idx);
381    /// assert_eq!(gain.current, 0.75);
382    /// ```
383    #[inline(always)]
384    pub fn get(&self, idx: usize) -> &Param {
385        &self.params[idx]
386    }
387
388    /// Gets a mutable reference to a parameter.
389    ///
390    /// # Arguments
391    ///
392    /// * `idx` - Parameter index (0-7)
393    ///
394    /// # Returns
395    ///
396    /// Mutable reference to the parameter.
397    ///
398    /// # Panics
399    ///
400    /// Panics if `idx` is out of bounds.
401    ///
402    /// # Example
403    ///
404    /// ```
405    /// use aether_core::param::ParamBlock;
406    ///
407    /// let mut params = ParamBlock::new();
408    /// let cutoff_idx = params.add(1000.0);
409    ///
410    /// // Schedule a ramp
411    /// params.get_mut(cutoff_idx).set_target(2000.0, 480);
412    /// ```
413    #[inline(always)]
414    pub fn get_mut(&mut self, idx: usize) -> &mut Param {
415        &mut self.params[idx]
416    }
417
418    /// Tick all active params by one sample.
419    ///
420    /// Advances all parameters in the block by one sample. Call this once
421    /// per sample in your node's `process()` function.
422    ///
423    /// # Example
424    ///
425    /// ```
426    /// use aether_core::param::ParamBlock;
427    /// use aether_core::BUFFER_SIZE;
428    ///
429    /// let mut params = ParamBlock::new();
430    /// let gain_idx = params.add(0.0);
431    /// params.get_mut(gain_idx).set_target(1.0, BUFFER_SIZE as u32);
432    ///
433    /// // Tick through buffer
434    /// for _ in 0..BUFFER_SIZE {
435    ///     let gain_value = params.get(gain_idx).current;
436    ///     // Use gain_value for processing...
437    ///     params.tick_all();
438    /// }
439    ///
440    /// assert_eq!(params.get(gain_idx).current, 1.0);
441    /// ```
442    ///
443    /// # Performance
444    ///
445    /// This function is highly optimized:
446    /// - Inlined for zero call overhead
447    /// - Only ticks active parameters (count)
448    /// - Each tick is branch-free when not ramping
449    #[inline(always)]
450    pub fn tick_all(&mut self) {
451        for p in self.params[..self.count].iter_mut() {
452            p.tick();
453        }
454    }
455}
456
457impl Default for ParamBlock {
458    fn default() -> Self {
459        Self::new()
460    }
461}