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    /// Schedule a ramp to `target` with validation, clamping to `[min, max]`.
144    ///
145    /// Like [`set_target`](Self::set_target), but clamps the target value to the
146    /// specified range and validates that it's finite (not NaN or Infinity).
147    ///
148    /// # Arguments
149    ///
150    /// * `target` - Target value to ramp towards
151    /// * `ramp_samples` - Number of samples for the ramp (0 = instant)
152    /// * `min` - Minimum allowed value
153    /// * `max` - Maximum allowed value
154    ///
155    /// # Returns
156    ///
157    /// The clamped target value that was actually set.
158    ///
159    /// # Example
160    ///
161    /// ```
162    /// use aether_core::param::Param;
163    ///
164    /// let mut gain = Param::new(0.5);
165    ///
166    /// // Clamp to [0.0, 1.0]
167    /// let actual = gain.set_target_clamped(1.5, 480, 0.0, 1.0);
168    /// assert_eq!(actual, 1.0); // Clamped to max
169    ///
170    /// // NaN is replaced with current value
171    /// let actual = gain.set_target_clamped(f32::NAN, 0, 0.0, 1.0);
172    /// assert_eq!(actual, gain.current);
173    /// ```
174    ///
175    /// # Safety
176    ///
177    /// This function ensures RT safety by:
178    /// - Replacing NaN/Infinity with the current value
179    /// - Clamping to valid range
180    /// - Preventing invalid audio state
181    #[inline]
182    pub fn set_target_clamped(
183        &mut self,
184        target: f32,
185        ramp_samples: u32,
186        min: f32,
187        max: f32,
188    ) -> f32 {
189        // Validate: replace NaN/Infinity with current value
190        let validated = if target.is_finite() {
191            target.clamp(min, max)
192        } else {
193            self.current
194        };
195
196        self.set_target(validated, ramp_samples);
197        validated
198    }
199
200    /// Advance by one sample. Call once per sample in the RT loop.
201    ///
202    /// Updates `current` by adding `step`. When the target is reached,
203    /// automatically stops ramping by setting `step` to 0.0.
204    ///
205    /// # Example
206    ///
207    /// ```
208    /// use aether_core::param::Param;
209    ///
210    /// let mut gain = Param::new(0.0);
211    /// gain.set_target(1.0, 100);
212    ///
213    /// // Tick through 100 samples
214    /// for i in 0..100 {
215    ///     gain.tick();
216    /// }
217    ///
218    /// // Reached target value
219    /// assert!((gain.current - 1.0).abs() < 0.0001);
220    /// ```
221    ///
222    /// # Performance
223    ///
224    /// This function is highly optimized for the audio thread:
225    /// - Inlined for zero call overhead
226    /// - Branch-free when not ramping
227    /// - Automatic overshoot clamping
228    #[inline(always)]
229    pub fn tick(&mut self) {
230        if self.step != 0.0 {
231            self.current += self.step;
232            // Clamp overshoot.
233            if (self.step > 0.0 && self.current >= self.target)
234                || (self.step < 0.0 && self.current <= self.target)
235            {
236                self.current = self.target;
237                self.step = 0.0;
238            }
239        }
240    }
241
242    /// Advance by a full buffer, returning per-sample values into `out`.
243    ///
244    /// Efficiently fills a buffer with parameter values, advancing the ramp
245    /// for each sample. Uses a fast path when the parameter is stable (not ramping).
246    ///
247    /// # Arguments
248    ///
249    /// * `out` - Output buffer to fill with parameter values
250    ///
251    /// # Example
252    ///
253    /// ```
254    /// use aether_core::param::Param;
255    /// use aether_core::BUFFER_SIZE;
256    ///
257    /// let mut cutoff = Param::new(1000.0);
258    /// cutoff.set_target(2000.0, BUFFER_SIZE as u32);
259    ///
260    /// let mut buffer = [0.0f32; BUFFER_SIZE];
261    /// cutoff.fill_buffer(&mut buffer);
262    ///
263    /// // First sample is near 1000, last sample is near 2000
264    /// assert!((buffer[0] - 1000.0).abs() < 50.0);
265    /// assert!((buffer[BUFFER_SIZE-1] - 2000.0).abs() < 50.0);
266    /// ```
267    ///
268    /// # Performance
269    ///
270    /// This function has two paths:
271    /// - **Fast path** (step == 0.0): Fills buffer with single value (SIMD-friendly)
272    /// - **Ramp path** (step != 0.0): Advances sample-by-sample
273    ///
274    /// The fast path is taken 90%+ of the time in typical usage.
275    ///
276    /// # Use Case
277    ///
278    /// Use this when you need per-sample parameter values for modulation:
279    ///
280    /// ```
281    /// use aether_core::param::Param;
282    /// use aether_core::BUFFER_SIZE;
283    ///
284    /// let mut gain = Param::new(0.5);
285    /// let mut gain_buffer = [0.0f32; BUFFER_SIZE];
286    /// let input = [1.0f32; BUFFER_SIZE];
287    /// let mut output = [0.0f32; BUFFER_SIZE];
288    ///
289    /// // Fill gain buffer
290    /// gain.fill_buffer(&mut gain_buffer);
291    ///
292    /// // Apply per-sample gain
293    /// for i in 0..BUFFER_SIZE {
294    ///     output[i] = input[i] * gain_buffer[i];
295    /// }
296    /// ```
297    #[inline]
298    pub fn fill_buffer(&mut self, out: &mut [f32]) {
299        if self.step == 0.0 {
300            // Fast path: parameter is stable — fill with a single value.
301            // This is the common case and avoids all branching in the loop.
302            out.fill(self.current);
303        } else {
304            // Ramping path: advance sample by sample.
305            for sample in out.iter_mut() {
306                *sample = self.current;
307                self.tick();
308            }
309        }
310    }
311}
312
313/// A fixed-size block of parameters for a node.
314///
315/// Stores up to 8 parameters without heap allocation. Most DSP nodes need
316/// 1-4 parameters (gain, frequency, resonance, etc.), so 8 is sufficient
317/// for the vast majority of cases.
318///
319/// # Example
320///
321/// ```
322/// use aether_core::param::ParamBlock;
323///
324/// let mut params = ParamBlock::new();
325///
326/// // Add parameters
327/// let gain_idx = params.add(0.75);      // Gain: 0.75
328/// let cutoff_idx = params.add(1000.0);  // Cutoff: 1000 Hz
329/// let res_idx = params.add(0.5);        // Resonance: 0.5
330///
331/// assert_eq!(params.count, 3);
332///
333/// // Access parameters
334/// let gain = params.get(gain_idx);
335/// assert_eq!(gain.current, 0.75);
336///
337/// // Modify parameters
338/// params.get_mut(cutoff_idx).set_target(2000.0, 480);
339///
340/// // Tick all parameters
341/// params.tick_all();
342/// ```
343///
344/// # Capacity
345///
346/// If you need more than 8 parameters, consider:
347/// - Splitting into multiple nodes
348/// - Using a custom parameter storage system
349/// - Increasing the array size (requires modifying the constant)
350///
351/// # See Also
352///
353/// * [`Param`] - Individual parameter
354#[derive(Debug, Clone, Copy)]
355pub struct ParamBlock {
356    pub params: [Param; 8],
357    pub count: usize,
358}
359
360impl ParamBlock {
361    /// Creates a new empty parameter block.
362    ///
363    /// Initializes with zero parameters. Use [`add`](Self::add) to add parameters.
364    ///
365    /// # Example
366    ///
367    /// ```
368    /// use aether_core::param::ParamBlock;
369    ///
370    /// let params = ParamBlock::new();
371    /// assert_eq!(params.count, 0);
372    /// ```
373    pub fn new() -> Self {
374        Self {
375            params: [Param::new(0.0); 8],
376            count: 0,
377        }
378    }
379
380    /// Adds a parameter with the given initial value.
381    ///
382    /// # Arguments
383    ///
384    /// * `value` - Initial parameter value
385    ///
386    /// # Returns
387    ///
388    /// The parameter's index (0-7), used to access it later.
389    ///
390    /// # Panics
391    ///
392    /// Panics if the block is full (8 parameters already added).
393    ///
394    /// # Example
395    ///
396    /// ```
397    /// use aether_core::param::ParamBlock;
398    ///
399    /// let mut params = ParamBlock::new();
400    ///
401    /// let gain_idx = params.add(0.5);
402    /// let freq_idx = params.add(440.0);
403    ///
404    /// assert_eq!(gain_idx, 0);
405    /// assert_eq!(freq_idx, 1);
406    /// assert_eq!(params.count, 2);
407    /// ```
408    pub fn add(&mut self, value: f32) -> usize {
409        let idx = self.count;
410        self.params[idx] = Param::new(value);
411        self.count += 1;
412        idx
413    }
414
415    /// Gets an immutable reference to a parameter.
416    ///
417    /// # Arguments
418    ///
419    /// * `idx` - Parameter index (0-7)
420    ///
421    /// # Returns
422    ///
423    /// Reference to the parameter.
424    ///
425    /// # Panics
426    ///
427    /// Panics if `idx` is out of bounds.
428    ///
429    /// # Example
430    ///
431    /// ```
432    /// use aether_core::param::ParamBlock;
433    ///
434    /// let mut params = ParamBlock::new();
435    /// let gain_idx = params.add(0.75);
436    ///
437    /// let gain = params.get(gain_idx);
438    /// assert_eq!(gain.current, 0.75);
439    /// ```
440    #[inline(always)]
441    pub fn get(&self, idx: usize) -> &Param {
442        &self.params[idx]
443    }
444
445    /// Gets a mutable reference to a parameter.
446    ///
447    /// # Arguments
448    ///
449    /// * `idx` - Parameter index (0-7)
450    ///
451    /// # Returns
452    ///
453    /// Mutable reference to the parameter.
454    ///
455    /// # Panics
456    ///
457    /// Panics if `idx` is out of bounds.
458    ///
459    /// # Example
460    ///
461    /// ```
462    /// use aether_core::param::ParamBlock;
463    ///
464    /// let mut params = ParamBlock::new();
465    /// let cutoff_idx = params.add(1000.0);
466    ///
467    /// // Schedule a ramp
468    /// params.get_mut(cutoff_idx).set_target(2000.0, 480);
469    /// ```
470    #[inline(always)]
471    pub fn get_mut(&mut self, idx: usize) -> &mut Param {
472        &mut self.params[idx]
473    }
474
475    /// Tick all active params by one sample.
476    ///
477    /// Advances all parameters in the block by one sample. Call this once
478    /// per sample in your node's `process()` function.
479    ///
480    /// # Example
481    ///
482    /// ```
483    /// use aether_core::param::ParamBlock;
484    /// use aether_core::BUFFER_SIZE;
485    ///
486    /// let mut params = ParamBlock::new();
487    /// let gain_idx = params.add(0.0);
488    /// params.get_mut(gain_idx).set_target(1.0, BUFFER_SIZE as u32);
489    ///
490    /// // Tick through buffer
491    /// for _ in 0..BUFFER_SIZE {
492    ///     let gain_value = params.get(gain_idx).current;
493    ///     // Use gain_value for processing...
494    ///     params.tick_all();
495    /// }
496    ///
497    /// assert_eq!(params.get(gain_idx).current, 1.0);
498    /// ```
499    ///
500    /// # Performance
501    ///
502    /// This function is highly optimized:
503    /// - Inlined for zero call overhead
504    /// - Only ticks active parameters (count)
505    /// - Each tick is branch-free when not ramping
506    #[inline(always)]
507    pub fn tick_all(&mut self) {
508        for p in self.params[..self.count].iter_mut() {
509            p.tick();
510        }
511    }
512}
513
514impl Default for ParamBlock {
515    fn default() -> Self {
516        Self::new()
517    }
518}
519
520/// Parameter validation utilities.
521///
522/// These functions help ensure parameter values are safe for real-time audio processing.
523pub mod validation {
524    /// Validates that a value is finite (not NaN or Infinity).
525    ///
526    /// # Arguments
527    ///
528    /// * `value` - Value to validate
529    ///
530    /// # Returns
531    ///
532    /// `true` if the value is finite, `false` otherwise.
533    ///
534    /// # Example
535    ///
536    /// ```
537    /// use aether_core::param::validation::is_finite;
538    ///
539    /// assert!(is_finite(1.0));
540    /// assert!(is_finite(0.0));
541    /// assert!(is_finite(-100.0));
542    /// assert!(!is_finite(f32::NAN));
543    /// assert!(!is_finite(f32::INFINITY));
544    /// assert!(!is_finite(f32::NEG_INFINITY));
545    /// ```
546    #[inline]
547    pub fn is_finite(value: f32) -> bool {
548        value.is_finite()
549    }
550
551    /// Clamps a value to a range, replacing NaN/Infinity with a default.
552    ///
553    /// # Arguments
554    ///
555    /// * `value` - Value to clamp
556    /// * `min` - Minimum allowed value
557    /// * `max` - Maximum allowed value
558    /// * `default` - Default value to use if `value` is NaN/Infinity
559    ///
560    /// # Returns
561    ///
562    /// Clamped value in range `[min, max]`.
563    ///
564    /// # Example
565    ///
566    /// ```
567    /// use aether_core::param::validation::clamp_or_default;
568    ///
569    /// assert_eq!(clamp_or_default(0.5, 0.0, 1.0, 0.5), 0.5);
570    /// assert_eq!(clamp_or_default(1.5, 0.0, 1.0, 0.5), 1.0);
571    /// assert_eq!(clamp_or_default(-0.5, 0.0, 1.0, 0.5), 0.0);
572    /// assert_eq!(clamp_or_default(f32::NAN, 0.0, 1.0, 0.5), 0.5);
573    /// assert_eq!(clamp_or_default(f32::INFINITY, 0.0, 1.0, 0.5), 0.5);
574    /// ```
575    #[inline]
576    pub fn clamp_or_default(value: f32, min: f32, max: f32, default: f32) -> f32 {
577        if value.is_finite() {
578            value.clamp(min, max)
579        } else {
580            default
581        }
582    }
583
584    /// Validates a frequency value (positive, finite, reasonable range).
585    ///
586    /// # Arguments
587    ///
588    /// * `freq` - Frequency in Hz
589    /// * `sample_rate` - Sample rate in Hz
590    ///
591    /// # Returns
592    ///
593    /// Clamped frequency in range `[0.1, sample_rate/2]` (Nyquist limit).
594    ///
595    /// # Example
596    ///
597    /// ```
598    /// use aether_core::param::validation::validate_frequency;
599    ///
600    /// assert_eq!(validate_frequency(440.0, 48000.0), 440.0);
601    /// assert_eq!(validate_frequency(-100.0, 48000.0), 0.1); // Negative clamped to min
602    /// assert_eq!(validate_frequency(30000.0, 48000.0), 24000.0); // Above Nyquist
603    /// assert_eq!(validate_frequency(f32::NAN, 48000.0), 440.0); // NaN replaced with A4
604    /// ```
605    #[inline]
606    pub fn validate_frequency(freq: f32, sample_rate: f32) -> f32 {
607        const MIN_FREQ: f32 = 0.1;
608        let max_freq = sample_rate * 0.5; // Nyquist limit
609        clamp_or_default(freq, MIN_FREQ, max_freq, 440.0) // Default to A4
610    }
611
612    /// Validates a gain value (0.0 to 1.0 or higher).
613    ///
614    /// # Arguments
615    ///
616    /// * `gain` - Gain value (linear, not dB)
617    /// * `max_gain` - Maximum allowed gain
618    ///
619    /// # Returns
620    ///
621    /// Clamped gain in range `[0.0, max_gain]`.
622    ///
623    /// # Example
624    ///
625    /// ```
626    /// use aether_core::param::validation::validate_gain;
627    ///
628    /// assert_eq!(validate_gain(0.5, 2.0), 0.5);
629    /// assert_eq!(validate_gain(-0.5, 2.0), 0.0); // Negative clamped to 0
630    /// assert_eq!(validate_gain(3.0, 2.0), 2.0); // Above max
631    /// assert_eq!(validate_gain(f32::NAN, 2.0), 1.0); // NaN replaced with unity
632    /// ```
633    #[inline]
634    pub fn validate_gain(gain: f32, max_gain: f32) -> f32 {
635        clamp_or_default(gain, 0.0, max_gain, 1.0) // Default to unity gain
636    }
637
638    /// Validates a time value in milliseconds.
639    ///
640    /// # Arguments
641    ///
642    /// * `time_ms` - Time in milliseconds
643    /// * `min_ms` - Minimum allowed time
644    /// * `max_ms` - Maximum allowed time
645    ///
646    /// # Returns
647    ///
648    /// Clamped time in range `[min_ms, max_ms]`.
649    ///
650    /// # Example
651    ///
652    /// ```
653    /// use aether_core::param::validation::validate_time_ms;
654    ///
655    /// assert_eq!(validate_time_ms(50.0, 1.0, 1000.0), 50.0);
656    /// assert_eq!(validate_time_ms(0.5, 1.0, 1000.0), 1.0); // Below min
657    /// assert_eq!(validate_time_ms(2000.0, 1.0, 1000.0), 1000.0); // Above max
658    /// assert_eq!(validate_time_ms(f32::NAN, 1.0, 1000.0), 100.0); // NaN replaced with 100ms
659    /// ```
660    #[inline]
661    pub fn validate_time_ms(time_ms: f32, min_ms: f32, max_ms: f32) -> f32 {
662        clamp_or_default(time_ms, min_ms, max_ms, 100.0) // Default to 100ms
663    }
664}
665
666#[cfg(test)]
667mod tests {
668    use super::*;
669
670    #[test]
671    fn test_param_validation_nan() {
672        let mut param = Param::new(0.5);
673        let actual = param.set_target_clamped(f32::NAN, 0, 0.0, 1.0);
674        assert_eq!(actual, 0.5); // Should keep current value
675        assert_eq!(param.current, 0.5);
676    }
677
678    #[test]
679    fn test_param_validation_infinity() {
680        let mut param = Param::new(0.5);
681        let actual = param.set_target_clamped(f32::INFINITY, 0, 0.0, 1.0);
682        assert_eq!(actual, 0.5); // Should keep current value
683        assert_eq!(param.current, 0.5);
684    }
685
686    #[test]
687    fn test_param_validation_clamp_max() {
688        let mut param = Param::new(0.5);
689        let actual = param.set_target_clamped(1.5, 0, 0.0, 1.0);
690        assert_eq!(actual, 1.0); // Should clamp to max
691        assert_eq!(param.current, 1.0);
692    }
693
694    #[test]
695    fn test_param_validation_clamp_min() {
696        let mut param = Param::new(0.5);
697        let actual = param.set_target_clamped(-0.5, 0, 0.0, 1.0);
698        assert_eq!(actual, 0.0); // Should clamp to min
699        assert_eq!(param.current, 0.0);
700    }
701
702    #[test]
703    fn test_param_validation_valid_value() {
704        let mut param = Param::new(0.5);
705        let actual = param.set_target_clamped(0.75, 0, 0.0, 1.0);
706        assert_eq!(actual, 0.75); // Should accept valid value
707        assert_eq!(param.current, 0.75);
708    }
709
710    #[test]
711    fn test_validation_frequency() {
712        use validation::validate_frequency;
713
714        assert_eq!(validate_frequency(440.0, 48000.0), 440.0);
715        assert_eq!(validate_frequency(-100.0, 48000.0), 0.1);
716        assert_eq!(validate_frequency(30000.0, 48000.0), 24000.0);
717        assert_eq!(validate_frequency(f32::NAN, 48000.0), 440.0);
718    }
719
720    #[test]
721    fn test_validation_gain() {
722        use validation::validate_gain;
723
724        assert_eq!(validate_gain(0.5, 2.0), 0.5);
725        assert_eq!(validate_gain(-0.5, 2.0), 0.0);
726        assert_eq!(validate_gain(3.0, 2.0), 2.0);
727        assert_eq!(validate_gain(f32::NAN, 2.0), 1.0);
728    }
729}