Skip to main content

audio_automation/
parameter.rs

1//! Parameter range and scaling for automation and plugin parameters.
2//!
3//! Provides normalized (0.0-1.0) ↔ real value conversion with different scaling algorithms.
4//!
5//! # Example
6//!
7//! ```
8//! use audio_automation::{ParameterRange, ParameterScale};
9//!
10//! // Filter cutoff: 20Hz to 20kHz, logarithmic scaling
11//! let cutoff = ParameterRange::new(20.0, 20000.0, 1000.0, ParameterScale::Logarithmic);
12//!
13//! // Automation stores normalized 0.0-1.0
14//! let normalized = 0.5;
15//! let freq_hz = cutoff.denormalize(normalized);  // ~632 Hz (geometric mean)
16//!
17//! // Convert back to normalized
18//! let back = cutoff.normalize(freq_hz);  // ~0.5
19//! ```
20
21/// How a parameter value is scaled between normalized (0-1) and real values.
22#[derive(Debug, Clone, Copy, PartialEq, Default)]
23pub enum ParameterScale {
24    /// Linear mapping (default)
25    ///
26    /// `real = min + normalized * (max - min)`
27    #[default]
28    Linear,
29
30    /// Logarithmic scaling (for frequencies, gains)
31    ///
32    /// `real = min * (max/min)^normalized`
33    ///
34    /// Requires `min > 0` and `max > min`.
35    Logarithmic,
36
37    /// Exponential curve with configurable shape
38    ///
39    /// `curve > 1.0`: More resolution at low end
40    /// `curve < 1.0`: More resolution at high end
41    /// `curve = 1.0`: Linear (equivalent to Linear)
42    Exponential {
43        /// Curve shape factor (typically 2.0-4.0)
44        curve: f32,
45    },
46
47    /// On/off toggle (normalized < 0.5 = off, >= 0.5 = on)
48    ///
49    /// Denormalizes to `min` (off) or `max` (on).
50    Toggle,
51
52    /// Discrete integer steps
53    ///
54    /// Values are quantized to integers between `min` and `max`.
55    Integer,
56}
57
58/// Parameter range with scaling for automation and plugin parameters.
59///
60/// Stores the valid range and default value, and provides conversion between
61/// normalized (0.0-1.0) and real parameter values.
62#[derive(Debug, Clone)]
63pub struct ParameterRange {
64    pub min: f32,
65    pub max: f32,
66    pub default: f32,
67    pub scale: ParameterScale,
68}
69
70impl ParameterRange {
71    pub fn new(min: f32, max: f32, default: f32, scale: ParameterScale) -> Self {
72        debug_assert!(max > min, "max must be greater than min");
73
74        Self {
75            min,
76            max,
77            default: clamp(default, min, max),
78            scale,
79        }
80    }
81
82    pub fn linear(min: f32, max: f32, default: f32) -> Self {
83        Self::new(min, max, default, ParameterScale::Linear)
84    }
85
86    /// Create a logarithmic parameter range (for frequencies, gains).
87    ///
88    /// # Panics
89    ///
90    /// Panics in debug mode if `min <= 0`.
91    pub fn logarithmic(min: f32, max: f32, default: f32) -> Self {
92        debug_assert!(min > 0.0, "logarithmic scale requires min > 0");
93        Self::new(min, max, default, ParameterScale::Logarithmic)
94    }
95
96    /// `curve` shape factor: values > 1.0 give more resolution at low end.
97    pub fn exponential(min: f32, max: f32, default: f32, curve: f32) -> Self {
98        Self::new(min, max, default, ParameterScale::Exponential { curve })
99    }
100
101    pub fn toggle(off_value: f32, on_value: f32, default_on: bool) -> Self {
102        Self::new(
103            off_value,
104            on_value,
105            if default_on { on_value } else { off_value },
106            ParameterScale::Toggle,
107        )
108    }
109
110    pub fn integer(min: i32, max: i32, default: i32) -> Self {
111        Self::new(
112            min as f32,
113            max as f32,
114            default as f32,
115            ParameterScale::Integer,
116        )
117    }
118
119    #[inline]
120    pub fn normalize(&self, value: f32) -> f32 {
121        let value = clamp(value, self.min, self.max);
122        let range = self.max - self.min;
123
124        if range <= 0.0 {
125            return 0.0;
126        }
127
128        match self.scale {
129            ParameterScale::Linear => (value - self.min) / range,
130
131            ParameterScale::Logarithmic => {
132                if self.min <= 0.0 {
133                    (value - self.min) / range
134                } else {
135                    let log_min = libm::logf(self.min);
136                    let log_max = libm::logf(self.max);
137                    (libm::logf(value) - log_min) / (log_max - log_min)
138                }
139            }
140
141            ParameterScale::Exponential { curve } => {
142                let linear = (value - self.min) / range;
143                if curve <= 0.0 || curve == 1.0 {
144                    linear
145                } else {
146                    libm::powf(linear, 1.0 / curve)
147                }
148            }
149
150            ParameterScale::Toggle => {
151                if value >= (self.min + self.max) / 2.0 {
152                    1.0
153                } else {
154                    0.0
155                }
156            }
157
158            ParameterScale::Integer => {
159                let int_value = libm::roundf(value);
160                (int_value - self.min) / range
161            }
162        }
163    }
164
165    #[inline]
166    pub fn denormalize(&self, normalized: f32) -> f32 {
167        let normalized = clamp(normalized, 0.0, 1.0);
168        let range = self.max - self.min;
169
170        match self.scale {
171            ParameterScale::Linear => self.min + normalized * range,
172
173            ParameterScale::Logarithmic => {
174                if self.min <= 0.0 {
175                    self.min + normalized * range
176                } else {
177                    let log_min = libm::logf(self.min);
178                    let log_max = libm::logf(self.max);
179                    libm::expf(log_min + normalized * (log_max - log_min))
180                }
181            }
182
183            ParameterScale::Exponential { curve } => {
184                let shaped = if curve <= 0.0 || curve == 1.0 {
185                    normalized
186                } else {
187                    libm::powf(normalized, curve)
188                };
189                self.min + shaped * range
190            }
191
192            ParameterScale::Toggle => {
193                if normalized >= 0.5 {
194                    self.max
195                } else {
196                    self.min
197                }
198            }
199
200            ParameterScale::Integer => {
201                let continuous = self.min + normalized * range;
202                libm::roundf(continuous)
203            }
204        }
205    }
206
207    #[inline]
208    pub fn clamp(&self, value: f32) -> f32 {
209        clamp(value, self.min, self.max)
210    }
211
212    #[inline]
213    pub fn default_normalized(&self) -> f32 {
214        self.normalize(self.default)
215    }
216
217    #[inline]
218    pub fn contains(&self, value: f32) -> bool {
219        value >= self.min && value <= self.max
220    }
221
222    #[inline]
223    pub fn span(&self) -> f32 {
224        self.max - self.min
225    }
226
227    #[inline]
228    pub fn db_to_linear(db: f32) -> f32 {
229        libm::powf(10.0, db / 20.0)
230    }
231
232    /// Returns -inf for amplitude <= 0.
233    #[inline]
234    pub fn linear_to_db(linear: f32) -> f32 {
235        if linear <= 0.0 {
236            f32::NEG_INFINITY
237        } else {
238            20.0 * libm::log10f(linear)
239        }
240    }
241}
242
243impl Default for ParameterRange {
244    fn default() -> Self {
245        Self::linear(0.0, 1.0, 0.5)
246    }
247}
248
249/// no_std-compatible clamp (f32::clamp requires std on older editions).
250#[inline]
251fn clamp(value: f32, min: f32, max: f32) -> f32 {
252    if value < min {
253        min
254    } else if value > max {
255        max
256    } else {
257        value
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    fn approx_eq(a: f32, b: f32) -> bool {
266        let abs_diff = (a - b).abs();
267        let max_val = a.abs().max(b.abs());
268
269        if max_val < 1.0 {
270            abs_diff < 0.0001
271        } else {
272            abs_diff / max_val < 0.00001
273        }
274    }
275
276    #[test]
277    fn test_linear_normalize_denormalize() {
278        let range = ParameterRange::linear(0.0, 100.0, 50.0);
279
280        assert!(approx_eq(range.normalize(0.0), 0.0));
281        assert!(approx_eq(range.normalize(50.0), 0.5));
282        assert!(approx_eq(range.normalize(100.0), 1.0));
283
284        assert!(approx_eq(range.denormalize(0.0), 0.0));
285        assert!(approx_eq(range.denormalize(0.5), 50.0));
286        assert!(approx_eq(range.denormalize(1.0), 100.0));
287    }
288
289    #[test]
290    fn test_linear_roundtrip() {
291        let range = ParameterRange::linear(-10.0, 10.0, 0.0);
292
293        for value in [-10.0, -5.0, 0.0, 5.0, 10.0] {
294            let normalized = range.normalize(value);
295            let back = range.denormalize(normalized);
296            assert!(approx_eq(value, back), "Roundtrip failed for {}", value);
297        }
298    }
299
300    #[test]
301    fn test_logarithmic_normalize_denormalize() {
302        let range = ParameterRange::logarithmic(20.0, 20000.0, 1000.0);
303
304        let mid = range.denormalize(0.5);
305        let expected_mid = libm::sqrtf(20.0 * 20000.0);
306        assert!(
307            approx_eq(mid, expected_mid),
308            "Expected ~{}, got {}",
309            expected_mid,
310            mid
311        );
312
313        assert!(approx_eq(range.denormalize(0.0), 20.0));
314        assert!(approx_eq(range.denormalize(1.0), 20000.0));
315    }
316
317    #[test]
318    fn test_logarithmic_roundtrip() {
319        let range = ParameterRange::logarithmic(20.0, 20000.0, 1000.0);
320
321        for value in [20.0, 100.0, 1000.0, 10000.0, 20000.0] {
322            let normalized = range.normalize(value);
323            let back = range.denormalize(normalized);
324            assert!(
325                (value - back).abs() / value < 0.001,
326                "Roundtrip failed for {}: got {}",
327                value,
328                back
329            );
330        }
331    }
332
333    #[test]
334    fn test_exponential_curve() {
335        let range = ParameterRange::exponential(0.0, 1.0, 0.5, 2.0);
336
337        assert!(approx_eq(range.denormalize(0.5), 0.25));
338        assert!(approx_eq(range.denormalize(0.0), 0.0));
339        assert!(approx_eq(range.denormalize(1.0), 1.0));
340    }
341
342    #[test]
343    fn test_toggle() {
344        let range = ParameterRange::toggle(0.0, 1.0, false);
345
346        assert!(approx_eq(range.denormalize(0.0), 0.0));
347        assert!(approx_eq(range.denormalize(0.49), 0.0));
348        assert!(approx_eq(range.denormalize(0.5), 1.0));
349        assert!(approx_eq(range.denormalize(1.0), 1.0));
350
351        assert!(approx_eq(range.normalize(0.0), 0.0));
352        assert!(approx_eq(range.normalize(1.0), 1.0));
353    }
354
355    #[test]
356    fn test_integer() {
357        let range = ParameterRange::integer(0, 10, 5);
358
359        assert!(approx_eq(range.denormalize(0.0), 0.0));
360        assert!(approx_eq(range.denormalize(0.5), 5.0));
361        assert!(approx_eq(range.denormalize(1.0), 10.0));
362
363        assert!(approx_eq(range.denormalize(0.15), 2.0));
364        assert!(approx_eq(range.denormalize(0.35), 4.0));
365    }
366
367    #[test]
368    fn test_clamp() {
369        let range = ParameterRange::linear(0.0, 100.0, 50.0);
370
371        assert!(approx_eq(range.clamp(-10.0), 0.0));
372        assert!(approx_eq(range.clamp(50.0), 50.0));
373        assert!(approx_eq(range.clamp(110.0), 100.0));
374    }
375
376    #[test]
377    fn test_default_normalized() {
378        let range = ParameterRange::linear(0.0, 100.0, 25.0);
379        assert!(approx_eq(range.default_normalized(), 0.25));
380    }
381
382    #[test]
383    fn test_db_conversion() {
384        assert!(approx_eq(ParameterRange::db_to_linear(0.0), 1.0));
385
386        let minus_6db = ParameterRange::db_to_linear(-6.0);
387        assert!(
388            (minus_6db - 0.5).abs() < 0.02,
389            "Expected ~0.5, got {}",
390            minus_6db
391        );
392
393        let plus_6db = ParameterRange::db_to_linear(6.0);
394        assert!(
395            (plus_6db - 2.0).abs() < 0.05,
396            "Expected ~2.0, got {}",
397            plus_6db
398        );
399
400        let db = -12.0;
401        let linear = ParameterRange::db_to_linear(db);
402        let back = ParameterRange::linear_to_db(linear);
403        assert!(
404            approx_eq(db, back),
405            "dB roundtrip failed: {} -> {}",
406            db,
407            back
408        );
409    }
410
411    #[test]
412    fn test_contains() {
413        let range = ParameterRange::linear(0.0, 100.0, 50.0);
414
415        assert!(range.contains(0.0));
416        assert!(range.contains(50.0));
417        assert!(range.contains(100.0));
418        assert!(!range.contains(-1.0));
419        assert!(!range.contains(101.0));
420    }
421}