Skip to main content

nice_plug_core/params/
range.rs

1//! Different ranges for numeric parameters.
2
3use crate::{nice_debug_assert, util};
4
5/// A distribution for a floating point parameter's range. All range endpoints are inclusive.
6#[derive(Debug, Clone, Copy)]
7pub enum FloatRange {
8    /// The values are uniformly distributed between `min` and `max`.
9    Linear { min: f32, max: f32 },
10    /// The range is skewed by a factor. Values above 1.0 will make the end of the range wider,
11    /// while values between 0 and 1 will skew the range towards the start. Use
12    /// [`FloatRange::skew_factor()`] for a more intuitively way to calculate the skew factor where
13    /// positive values skew the range towards the end while negative values skew the range toward
14    /// the start.
15    Skewed { min: f32, max: f32, factor: f32 },
16    /// The same as [`FloatRange::Skewed`], but with the skewing happening from a central point.
17    /// This central point is rescaled to be at 50% of the parameter's range for convenience of use.
18    /// Git blame this comment to find a version that doesn't do this.
19    SymmetricalSkewed {
20        min: f32,
21        max: f32,
22        factor: f32,
23        center: f32,
24    },
25    /// A reversed range that goes from high to low instead of from low to high.
26    Reversed(&'static FloatRange),
27}
28
29/// A distribution for an integer parameter's range. All range endpoints are inclusive. Only linear
30/// ranges are supported for integers since hosts expect discrete parameters to have a fixed step
31/// size.
32#[derive(Debug, Clone, Copy)]
33pub enum IntRange {
34    /// The values are uniformly distributed between `min` and `max`.
35    Linear { min: i32, max: i32 },
36    /// A reversed range that goes from high to low instead of from low to high.
37    Reversed(&'static IntRange),
38}
39
40impl FloatRange {
41    /// Calculate a skew factor for [`FloatRange::Skewed`] and [`FloatRange::SymmetricalSkewed`].
42    /// Positive values make the end of the range wider while negative make the start of the range
43    /// wider.
44    pub fn skew_factor(factor: f32) -> f32 {
45        2.0f32.powf(factor)
46    }
47
48    /// Calculate a skew factor for [`FloatRange::Skewed`] that makes a linear gain parameter range
49    /// appear as if it was linear when formatted as decibels.
50    pub fn gain_skew_factor(min_db: f32, max_db: f32) -> f32 {
51        nice_debug_assert!(min_db < max_db);
52
53        let min_gain = util::db_to_gain(min_db);
54        let max_gain = util::db_to_gain(max_db);
55        let middle_db = (max_db + min_db) / 2.0;
56        let middle_gain = util::db_to_gain(middle_db);
57
58        // Check the Skewed equation in the normalized function below, we need to solve the factor
59        // such that the a normalized value of 0.5 resolves to the middle of the range
60        0.5f32.log((middle_gain - min_gain) / (max_gain - min_gain))
61    }
62
63    /// Create a parameter range for a gain value that is linear when formatted as decibels.
64    pub fn gain_range(min_db: f32, max_db: f32) -> Self {
65        Self::Skewed {
66            min: util::db_to_gain(min_db),
67            max: util::db_to_gain(max_db),
68            factor: Self::gain_skew_factor(min_db, max_db),
69        }
70    }
71
72    /// Normalize a plain, unnormalized value. Will be clamped to the bounds of the range if the
73    /// normalized value exceeds `[0, 1]`.
74    pub fn normalize(&self, plain: f32) -> f32 {
75        match self {
76            FloatRange::Linear { min, max } => (plain.clamp(*min, *max) - min) / (max - min),
77            FloatRange::Skewed { min, max, factor } => {
78                ((plain.clamp(*min, *max) - min) / (max - min)).powf(*factor)
79            }
80            FloatRange::SymmetricalSkewed {
81                min,
82                max,
83                factor,
84                center,
85            } => {
86                // There's probably a much faster equivalent way to write this. Also, I have no clue
87                // how I managed to implement this correctly on the first try.
88                let unscaled_proportion = (plain.clamp(*min, *max) - min) / (max - min);
89                let center_proportion = (center - min) / (max - min);
90                if unscaled_proportion > center_proportion {
91                    // The part above the center gets normalized to a [0, 1] range, skewed, and then
92                    // unnormalized and scaled back to the original [center_proportion, 1] range
93                    let scaled_proportion = (unscaled_proportion - center_proportion)
94                        * (1.0 - center_proportion).recip();
95                    (scaled_proportion.powf(*factor) * 0.5) + 0.5
96                } else {
97                    // The part below the center gets scaled, inverted (so the range is [0, 1] where
98                    // 0 corresponds to the center proportion and 1 corresponds to the original
99                    // normalized 0 value), skewed, inverted back again, and then scaled back to the
100                    // original range
101                    let inverted_scaled_proportion =
102                        (center_proportion - unscaled_proportion) * (center_proportion).recip();
103                    (1.0 - inverted_scaled_proportion.powf(*factor)) * 0.5
104                }
105            }
106            FloatRange::Reversed(range) => 1.0 - range.normalize(plain),
107        }
108    }
109
110    /// Unnormalize a normalized value. Will be clamped to `[0, 1]` if the plain, unnormalized value
111    /// would exceed that range.
112    pub fn unnormalize(&self, normalized: f32) -> f32 {
113        let normalized = normalized.clamp(0.0, 1.0);
114        match self {
115            FloatRange::Linear { min, max } => (normalized * (max - min)) + min,
116            FloatRange::Skewed { min, max, factor } => {
117                (normalized.powf(factor.recip()) * (max - min)) + min
118            }
119            FloatRange::SymmetricalSkewed {
120                min,
121                max,
122                factor,
123                center,
124            } => {
125                // Reconstructing the subranges works the same as with the normal skewed ranges
126                let center_proportion = (center - min) / (max - min);
127                let skewed_proportion = if normalized > 0.5 {
128                    let scaled_proportion = (normalized - 0.5) * 2.0;
129                    (scaled_proportion.powf(factor.recip()) * (1.0 - center_proportion))
130                        + center_proportion
131                } else {
132                    let inverted_scaled_proportion = (0.5 - normalized) * 2.0;
133                    (1.0 - inverted_scaled_proportion.powf(factor.recip())) * center_proportion
134                };
135
136                (skewed_proportion * (max - min)) + min
137            }
138            FloatRange::Reversed(range) => range.unnormalize(1.0 - normalized),
139        }
140    }
141
142    /// The range's previous discrete step from a certain value with a certain step size. If the
143    /// step size is not set, then the normalized range is split into 50 segments instead. If
144    /// `finer` is true, then this is upped to 200 segments.
145    pub fn previous_step(&self, from: f32, step_size: Option<f32>, finer: bool) -> f32 {
146        // This one's slightly more involved than the integer version. We'll split the normalized
147        // range up into 50 segments, but if `self.step_size` would cause the range to be devided
148        // into less than 50 segments then we'll use that.
149        match self {
150            FloatRange::Linear { min, max }
151            | FloatRange::Skewed { min, max, .. }
152            | FloatRange::SymmetricalSkewed { min, max, .. } => {
153                let normalized_naive_step_size = if finer { 0.005 } else { 0.02 };
154                let naive_step =
155                    self.unnormalize(self.normalize(from) - normalized_naive_step_size);
156
157                match step_size {
158                    // Use the naive step size if it is larger than the configured step size
159                    Some(step_size) if (naive_step - from).abs() > step_size => {
160                        self.snap_to_step(naive_step, step_size)
161                    }
162                    Some(step_size) => from - step_size,
163                    None => naive_step,
164                }
165                .clamp(*min, *max)
166            }
167            FloatRange::Reversed(range) => range.next_step(from, step_size, finer),
168        }
169    }
170
171    /// The range's next discrete step from a certain value with a certain step size. If the step
172    /// size is not set, then the normalized range is split into 100 segments instead.
173    pub fn next_step(&self, from: f32, step_size: Option<f32>, finer: bool) -> f32 {
174        // See above
175        match self {
176            FloatRange::Linear { min, max }
177            | FloatRange::Skewed { min, max, .. }
178            | FloatRange::SymmetricalSkewed { min, max, .. } => {
179                let normalized_naive_step_size = if finer { 0.005 } else { 0.02 };
180                let naive_step =
181                    self.unnormalize(self.normalize(from) + normalized_naive_step_size);
182
183                match step_size {
184                    Some(step_size) if (naive_step - from).abs() > step_size => {
185                        self.snap_to_step(naive_step, step_size)
186                    }
187                    Some(step_size) => from + step_size,
188                    None => naive_step,
189                }
190                .clamp(*min, *max)
191            }
192            FloatRange::Reversed(range) => range.previous_step(from, step_size, finer),
193        }
194    }
195
196    /// Snap a value to a step size, clamping to the minimum and maximum value of the range.
197    pub fn snap_to_step(&self, value: f32, step_size: f32) -> f32 {
198        match self {
199            FloatRange::Linear { min, max }
200            | FloatRange::Skewed { min, max, .. }
201            | FloatRange::SymmetricalSkewed { min, max, .. } => {
202                ((value / step_size).round() * step_size).clamp(*min, *max)
203            }
204            FloatRange::Reversed(range) => range.snap_to_step(value, step_size),
205        }
206    }
207
208    /// Emits debug assertions to make sure that range minima are always less than the maxima and
209    /// that they are not equal.
210    pub(super) fn assert_validity(&self) {
211        match self {
212            FloatRange::Linear { min, max }
213            | FloatRange::Skewed { min, max, .. }
214            | FloatRange::SymmetricalSkewed { min, max, .. } => {
215                nice_debug_assert!(
216                    min < max,
217                    "The range minimum ({}) needs to be less than the range maximum ({}) and they \
218                     cannot be equal",
219                    min,
220                    max
221                );
222            }
223            FloatRange::Reversed(range) => range.assert_validity(),
224        }
225    }
226}
227
228impl IntRange {
229    /// Normalize a plain, unnormalized value. Will be clamped to the bounds of the range if the
230    /// normalized value exceeds `[0, 1]`.
231    pub fn normalize(&self, plain: i32) -> f32 {
232        match self {
233            IntRange::Linear { min, max } => (plain - min) as f32 / (max - min) as f32,
234            IntRange::Reversed(range) => 1.0 - range.normalize(plain),
235        }
236        .clamp(0.0, 1.0)
237    }
238
239    /// Unnormalize a normalized value. Will be clamped to `[0, 1]` if the plain, unnormalized value
240    /// would exceed that range.
241    pub fn unnormalize(&self, normalized: f32) -> i32 {
242        let normalized = normalized.clamp(0.0, 1.0);
243        match self {
244            IntRange::Linear { min, max } => (normalized * (max - min) as f32).round() as i32 + min,
245            IntRange::Reversed(range) => range.unnormalize(1.0 - normalized),
246        }
247    }
248
249    /// The range's previous discrete step from a certain value.
250    pub fn previous_step(&self, from: i32) -> i32 {
251        match self {
252            IntRange::Linear { min, max } => (from - 1).clamp(*min, *max),
253            IntRange::Reversed(range) => range.next_step(from),
254        }
255    }
256
257    /// The range's next discrete step from a certain value.
258    pub fn next_step(&self, from: i32) -> i32 {
259        match self {
260            IntRange::Linear { min, max } => (from + 1).clamp(*min, *max),
261            IntRange::Reversed(range) => range.previous_step(from),
262        }
263    }
264
265    /// The number of steps in this range. Used for the host's generic UI.
266    pub fn step_count(&self) -> usize {
267        match self {
268            IntRange::Linear { min, max } => (max - min) as usize,
269            IntRange::Reversed(range) => range.step_count(),
270        }
271    }
272
273    /// If this range is wrapped in an adapter, like `Reversed`, then return the wrapped range.
274    pub fn inner_range(&self) -> Self {
275        match self {
276            IntRange::Linear { .. } => *self,
277            IntRange::Reversed(range) => range.inner_range(),
278        }
279    }
280
281    /// Emits debug assertions to make sure that range minima are always less than the maxima and
282    /// that they are not equal.
283    pub(super) fn assert_validity(&self) {
284        match self {
285            IntRange::Linear { min, max } => {
286                nice_debug_assert!(
287                    min < max,
288                    "The range minimum ({}) needs to be less than the range maximum ({}) and they \
289                     cannot be equal",
290                    min,
291                    max
292                );
293            }
294            IntRange::Reversed(range) => range.assert_validity(),
295        }
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    const fn make_linear_float_range() -> FloatRange {
304        FloatRange::Linear {
305            min: 10.0,
306            max: 20.0,
307        }
308    }
309
310    const fn make_linear_int_range() -> IntRange {
311        IntRange::Linear { min: -10, max: 10 }
312    }
313
314    const fn make_skewed_float_range(factor: f32) -> FloatRange {
315        FloatRange::Skewed {
316            min: 10.0,
317            max: 20.0,
318            factor,
319        }
320    }
321
322    const fn make_symmetrical_skewed_float_range(factor: f32) -> FloatRange {
323        FloatRange::SymmetricalSkewed {
324            min: 10.0,
325            max: 20.0,
326            factor,
327            center: 12.5,
328        }
329    }
330
331    #[test]
332    fn step_size() {
333        // These are weird step sizes, but if it works here then it will work for anything
334        let range = make_linear_float_range();
335        // XXX: We round to decimal places when outputting, but not when snapping to steps
336        assert_eq!(range.snap_to_step(13.0, 4.73), 14.190001);
337    }
338
339    #[test]
340    fn step_size_clamping() {
341        let range = make_linear_float_range();
342        assert_eq!(range.snap_to_step(10.0, 4.73), 10.0);
343        assert_eq!(range.snap_to_step(20.0, 6.73), 20.0);
344    }
345
346    mod linear {
347        use super::*;
348
349        #[test]
350        fn range_normalize_float() {
351            let range = make_linear_float_range();
352            assert_eq!(range.normalize(17.5), 0.75);
353        }
354
355        #[test]
356        fn range_normalize_int() {
357            let range = make_linear_int_range();
358            assert_eq!(range.normalize(-5), 0.25);
359        }
360
361        #[test]
362        fn range_unnormalize_float() {
363            let range = make_linear_float_range();
364            assert_eq!(range.unnormalize(0.25), 12.5);
365        }
366
367        #[test]
368        fn range_unnormalize_int() {
369            let range = make_linear_int_range();
370            assert_eq!(range.unnormalize(0.75), 5);
371        }
372
373        #[test]
374        fn range_unnormalize_int_rounding() {
375            let range = make_linear_int_range();
376            assert_eq!(range.unnormalize(0.73), 5);
377        }
378    }
379
380    mod skewed {
381        use super::*;
382
383        #[test]
384        fn range_normalize_float() {
385            let range = make_skewed_float_range(FloatRange::skew_factor(-2.0));
386            assert_eq!(range.normalize(17.5), 0.9306049);
387        }
388
389        #[test]
390        fn range_unnormalize_float() {
391            let range = make_skewed_float_range(FloatRange::skew_factor(-2.0));
392            assert_eq!(range.unnormalize(0.9306049), 17.5);
393        }
394
395        #[test]
396        fn range_normalize_linear_equiv_float() {
397            let linear_range = make_linear_float_range();
398            let skewed_range = make_skewed_float_range(1.0);
399            assert_eq!(linear_range.normalize(17.5), skewed_range.normalize(17.5));
400        }
401
402        #[test]
403        fn range_unnormalize_linear_equiv_float() {
404            let linear_range = make_linear_float_range();
405            let skewed_range = make_skewed_float_range(1.0);
406            assert_eq!(
407                linear_range.unnormalize(0.25),
408                skewed_range.unnormalize(0.25)
409            );
410        }
411    }
412
413    mod symmetrical_skewed {
414        use super::*;
415
416        #[test]
417        fn range_normalize_float() {
418            let range = make_symmetrical_skewed_float_range(FloatRange::skew_factor(-2.0));
419            assert_eq!(range.normalize(17.5), 0.951801);
420        }
421
422        #[test]
423        fn range_unnormalize_float() {
424            let range = make_symmetrical_skewed_float_range(FloatRange::skew_factor(-2.0));
425            assert_eq!(range.unnormalize(0.951801), 17.5);
426        }
427    }
428
429    mod reversed_linear {
430        use super::*;
431
432        #[test]
433        fn range_normalize_int() {
434            const WRAPPED_RANGE: IntRange = make_linear_int_range();
435            let range = IntRange::Reversed(&WRAPPED_RANGE);
436            assert_eq!(range.normalize(-5), 1.0 - 0.25);
437        }
438
439        #[test]
440        fn range_unnormalize_int() {
441            const WRAPPED_RANGE: IntRange = make_linear_int_range();
442            let range = IntRange::Reversed(&WRAPPED_RANGE);
443            assert_eq!(range.unnormalize(1.0 - 0.75), 5);
444        }
445
446        #[test]
447        fn range_unnormalize_int_rounding() {
448            const WRAPPED_RANGE: IntRange = make_linear_int_range();
449            let range = IntRange::Reversed(&WRAPPED_RANGE);
450            assert_eq!(range.unnormalize(1.0 - 0.73), 5);
451        }
452    }
453
454    mod reversed_skewed {
455        use super::*;
456
457        #[test]
458        fn range_normalize_float() {
459            const WRAPPED_RANGE: FloatRange = make_skewed_float_range(0.25);
460            let range = FloatRange::Reversed(&WRAPPED_RANGE);
461            assert_eq!(range.normalize(17.5), 1.0 - 0.9306049);
462        }
463
464        #[test]
465        fn range_unnormalize_float() {
466            const WRAPPED_RANGE: FloatRange = make_skewed_float_range(0.25);
467            let range = FloatRange::Reversed(&WRAPPED_RANGE);
468            assert_eq!(range.unnormalize(1.0 - 0.9306049), 17.5);
469        }
470
471        #[test]
472        fn range_normalize_linear_equiv_float() {
473            const WRAPPED_LINEAR_RANGE: FloatRange = make_linear_float_range();
474            const WRAPPED_SKEWED_RANGE: FloatRange = make_skewed_float_range(1.0);
475            let linear_range = FloatRange::Reversed(&WRAPPED_LINEAR_RANGE);
476            let skewed_range = FloatRange::Reversed(&WRAPPED_SKEWED_RANGE);
477            assert_eq!(linear_range.normalize(17.5), skewed_range.normalize(17.5));
478        }
479
480        #[test]
481        fn range_unnormalize_linear_equiv_float() {
482            const WRAPPED_LINEAR_RANGE: FloatRange = make_linear_float_range();
483            const WRAPPED_SKEWED_RANGE: FloatRange = make_skewed_float_range(1.0);
484            let linear_range = FloatRange::Reversed(&WRAPPED_LINEAR_RANGE);
485            let skewed_range = FloatRange::Reversed(&WRAPPED_SKEWED_RANGE);
486            assert_eq!(
487                linear_range.unnormalize(0.25),
488                skewed_range.unnormalize(0.25)
489            );
490        }
491    }
492}