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}