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