Skip to main content

nice_plug_core/
formatters.rs

1//! Convenience functions for formatting and parsing parameter values in various common formats.
2//!
3//! Functions prefixed with `v2s_` are meant to be used with the `.value_to_string()` parameter
4//! functions, while the `s2v_` functions are meant to be used wit the `.string_to_value()`.
5//! functions. Most of these formatters come as a pair. Check each formatter's documentation for any
6//! additional usage information.
7
8use std::cmp::Ordering;
9use std::sync::Arc;
10
11use crate::util;
12
13// TODO: The v2s and s2v naming convention isn't ideal, but at least it's unambiguous. Is there a
14//       better way to name these functions? Should we just split this up into two modules?
15
16/// Round an `f32` value to always have a specific number of decimal digits. Avoids returning
17/// negative zero values to make sure string->value->string roundtrips work correctly. Otherwise
18/// `-0.001` rounded to two digits would result in `-0.00`.
19pub fn v2s_f32_rounded(digits: usize) -> Arc<dyn Fn(f32) -> String + Send + Sync> {
20    let rounding_multiplier = 10u32.pow(digits as u32) as f32;
21    Arc::new(move |value| {
22        // See above
23        if (value * rounding_multiplier).round() / rounding_multiplier == 0.0 {
24            format!("{:.digits$}", 0.0)
25        } else {
26            format!("{value:.digits$}")
27        }
28    })
29}
30
31/// Format a `[0, 1]` number as a percentage. Does not include the percent sign, you should specify
32/// this as the parameter's unit.
33pub fn v2s_f32_percentage(digits: usize) -> Arc<dyn Fn(f32) -> String + Send + Sync> {
34    Arc::new(move |value| format!("{:.digits$}", value * 100.0))
35}
36
37/// Parse a `[0, 100]` percentage to a `[0, 1]` number. Handles the percentage unit for you. Used in
38/// conjunction with [`v2s_f32_percentage()`].
39pub fn s2v_f32_percentage() -> Arc<dyn Fn(&str) -> Option<f32> + Send + Sync> {
40    Arc::new(|string| {
41        string
42            .trim_end_matches([' ', '%'])
43            .parse()
44            .ok()
45            .map(|x: f32| x / 100.0)
46    })
47}
48
49/// Format a positive number as a compression ratio. A value of 4 will be formatted as `4.0:1` while
50/// 0.25 is formatted as `1:4.0`.
51pub fn v2s_compression_ratio(digits: usize) -> Arc<dyn Fn(f32) -> String + Send + Sync> {
52    Arc::new(move |value| {
53        if value >= 1.0 {
54            format!("{value:.digits$}:1")
55        } else {
56            format!("1:{:.digits$}", value.recip())
57        }
58    })
59}
60
61/// Parse a `x:y` compression ratio back to a floating point number. Used in conjunction with
62/// [`v2s_compression_ratio()`]. Plain numbers are parsed directly for UX's sake.
63pub fn s2v_compression_ratio() -> Arc<dyn Fn(&str) -> Option<f32> + Send + Sync> {
64    Arc::new(|string| {
65        let string = string.trim();
66        string
67            .trim()
68            .split_once(':')
69            .and_then(|(numerator, denominator)| {
70                let numerator: f32 = numerator.trim().parse().ok()?;
71                let denominator: f32 = denominator.trim().parse().ok()?;
72
73                Some(numerator / denominator)
74            })
75            // Just parse the value directly if it doesn't contain a colon
76            .or_else(|| string.parse().ok())
77    })
78}
79
80/// Turn an `f32` value from voltage gain to decibels using the semantics described in
81/// [`util::gain_to_db()`]. You should use either `" dB"` or `" dBFS"` for the parameter's unit.
82/// `0.0` will be formatted as `-inf`. Avoids returning negative zero values to make sure
83/// string->value->string roundtrips work correctly. Otherwise `-0.001` rounded to two digits
84/// would result in `-0.00`.
85pub fn v2s_f32_gain_to_db(digits: usize) -> Arc<dyn Fn(f32) -> String + Send + Sync> {
86    let rounding_multiplier = 10u32.pow(digits as u32) as f32;
87    Arc::new(move |value| {
88        if value < util::MINUS_INFINITY_GAIN {
89            String::from("-inf")
90        } else {
91            let value_db = util::gain_to_db(value);
92
93            // See above
94            if (value_db * rounding_multiplier).round() / rounding_multiplier == 0.0 {
95                format!("{:.digits$}", 0.0)
96            } else {
97                format!("{value_db:.digits$}")
98            }
99        }
100    })
101}
102
103/// Parse a decibel value to a linear voltage gain ratio. Handles the `dB` or `dBFS` units for you.
104/// Used in conjunction with [`v2s_f32_gain_to_db()`]. `-inf dB` will be parsed to 0.0.
105pub fn s2v_f32_gain_to_db() -> Arc<dyn Fn(&str) -> Option<f32> + Send + Sync> {
106    Arc::new(|string| {
107        let string = string.trim_end_matches([' ', 'd', 'D', 'b', 'B', 'f', 'F', 's', 'S']);
108        // NOTE: The above line strips the `f`, so checked for `-inf` here will always return false
109        if string.eq_ignore_ascii_case("-in") {
110            Some(0.0)
111        } else {
112            string.parse().ok().map(util::db_to_gain)
113        }
114    })
115}
116
117/// Turn an `f32` `[-1, 1]` value to a panning value where negative values are represented by
118/// `[100L, 1L]`, 0 gets turned into `C`, and positive values become `[1R, 100R]` values.
119pub fn v2s_f32_panning() -> Arc<dyn Fn(f32) -> String + Send + Sync> {
120    Arc::new(move |value| match value.partial_cmp(&0.0) {
121        Some(Ordering::Less) => format!("{:.0}L", value * -100.0),
122        Some(Ordering::Equal) => String::from("C"),
123        Some(Ordering::Greater) => format!("{:.0}R", value * 100.0),
124        None => String::from("NaN"),
125    })
126}
127
128/// Parse a pan value in the format of [`v2s_f32_panning()`] to a linear value in the range `[-1,
129/// 1]`.
130pub fn s2v_f32_panning() -> Arc<dyn Fn(&str) -> Option<f32> + Send + Sync> {
131    Arc::new(|string| {
132        let string = string.trim();
133        let cleaned_string = string
134            .trim_end_matches([' ', 'l', 'L', 'c', 'C', 'r', 'R'])
135            .parse()
136            .ok();
137        match string.chars().last()?.to_uppercase().next()? {
138            'L' => cleaned_string.map(|x: f32| x / -100.0),
139            'C' => Some(0.0),
140            'R' => cleaned_string.map(|x: f32| x / 100.0),
141            _ => None,
142        }
143    })
144}
145
146/// Format a `f32` Hertz value as a rounded `Hz` below 1000 Hz, and as a rounded `kHz` value above
147/// 1000 Hz. This already includes the unit.
148pub fn v2s_f32_hz_then_khz(digits: usize) -> Arc<dyn Fn(f32) -> String + Send + Sync> {
149    Arc::new(move |value| {
150        if value.round() < 1000.0 {
151            format!("{value:.digits$} Hz")
152        } else {
153            format!("{:.digits$} kHz", value / 1000.0, digits = digits.max(1))
154        }
155    })
156}
157
158/// [`v2s_f32_hz_then_khz()`], but also includes the note name. Can be used with
159/// [`s2v_f32_hz_then_khz()`].
160#[deprecated(
161    since = "0.4.1",
162    note = "Use v2s_f32_hz_then_khz_with_key_name instead"
163)]
164pub fn v2s_f32_hz_then_khz_with_note_name(
165    digits: usize,
166    include_cents: bool,
167) -> Arc<dyn Fn(f32) -> String + Send + Sync> {
168    v2s_f32_hz_then_khz_with_key_name(digits, include_cents)
169}
170
171/// [`v2s_f32_hz_then_khz()`], but also includes the MIDI key name. Can be used with
172/// [`s2v_f32_hz_then_khz()`].
173pub fn v2s_f32_hz_then_khz_with_key_name(
174    digits: usize,
175    include_cents: bool,
176) -> Arc<dyn Fn(f32) -> String + Send + Sync> {
177    Arc::new(move |value| {
178        // With 0.0 this would result in a subtraction below i32's minimum value, and it would look
179        // ridiculous anyways so we'll just not even bother for tiny values
180        if value.abs() < 1.0 {
181            return format!("{value:.digits$} Hz");
182        }
183
184        // This is the inverse of the formula in `f32_midi_key_to_freq`
185        let fractional_key = util::freq_to_midi_key(value);
186        let key = fractional_key.round();
187        let cents = ((fractional_key - key) * 100.0).round() as i32;
188
189        let key_name = util::KEYS[(key as i32).rem_euclid(12) as usize];
190        // NOTE: This is different compared from `(key as i32 / 12) - 1` because truncating always
191        //       rounds towards zero
192        let octave = (key / 12.0).floor() as i32 - 1;
193        let key_str = if cents == 0 || !include_cents {
194            format!("{key_name}{octave}")
195        } else {
196            format!("{key_name}{octave}, {cents:+} ct.")
197        };
198
199        if value < 1000.0 {
200            format!("{value:.digits$} Hz, {key_str}")
201        } else {
202            format!(
203                "{:.digits$} kHz, {}",
204                value / 1000.0,
205                key_str,
206                digits = digits.max(1)
207            )
208        }
209    })
210}
211
212/// Convert an input in the same format at that of [`v2s_f32_hz_then_khz()`] to a Hertz value. This
213/// additionally also accepts MIDI key names in the same format as [`s2v_i32_key_formatter()`], and
214/// optionally also with cents in the form of `D#5, -23 ct.`.
215pub fn s2v_f32_hz_then_khz() -> Arc<dyn Fn(&str) -> Option<f32> + Send + Sync> {
216    // FIXME: This is a very crude way to reuse the key value formatter. There's no real runtime
217    //        penalty for doing it this way, but it does look less pretty.
218    let key_formatter = s2v_i32_key_formatter();
219
220    Arc::new(move |string| {
221        let string = string.trim();
222
223        // The input can contain a frequency in Hz or kHz, a key name, a key name and cents, or
224        // one of those two combined with a frequency. In the last case we'll ignore the frequency.
225        // If the string cannot be parsed as a key name, we'll try parsing it as a frequency
226        // instead. This is needed for the formatting roundtrip to work correctly. The input will
227        // consists of 1 to three segments, so we'll try to unpack them like this so we can pattern
228        // match on them
229        let mut segments = string.split(',');
230        let segments = (segments.next(), segments.next(), segments.next());
231
232        if let (_, Some(midi_key_number_str), Some(cents_str))
233        | (Some(midi_key_number_str), Some(cents_str), None) = segments
234        {
235            let cents_str = cents_str
236                .trim_start_matches([' ', '+'])
237                .trim_end_matches([' ', 'C', 'c', 'E', 'e', 'N', 'n', 'T', 't', 'S', 's', '.']);
238
239            if let (Some(midi_key_number), Ok(cents)) =
240                (key_formatter(midi_key_number_str), cents_str.parse::<i32>())
241            {
242                let plain_key_freq = util::f32_midi_key_to_freq(midi_key_number as f32);
243                let cents_multiplier = 2.0f32.powf(cents as f32 / 100.0 / 12.0);
244                return Some(plain_key_freq * cents_multiplier);
245            }
246        }
247
248        if let (_, Some(midi_key_number_str), _) | (Some(midi_key_number_str), None, None) =
249            segments
250            && let Some(midi_key_number) = key_formatter(midi_key_number_str)
251        {
252            return Some(util::f32_midi_key_to_freq(midi_key_number as f32));
253        }
254
255        // Otherwise we'll accept values in either Hz (with or without unit) or kHz
256        let frequency_segment = segments.0?;
257        let cleaned_string = frequency_segment
258            .trim_end_matches([' ', 'k', 'K', 'h', 'H', 'z', 'Z'])
259            .parse()
260            .ok();
261        match frequency_segment.get(frequency_segment.len().saturating_sub(3)..) {
262            Some(unit) if unit.eq_ignore_ascii_case("khz") => cleaned_string.map(|x| x * 1000.0),
263            // Even if there's no unit at all, just assume the input is in Hertz
264            _ => cleaned_string,
265        }
266    })
267}
268
269/// Format an order/power of two. Useful in conjunction with [`s2v_i32_power_of_two()`] to limit
270/// integer parameter ranges to be only powers of two.
271pub fn v2s_i32_power_of_two() -> Arc<dyn Fn(i32) -> String + Send + Sync> {
272    Arc::new(|value| format!("{}", 1 << value))
273}
274
275/// Parse a parameter input string to a power of two. Useful in conjunction with
276/// [`v2s_i32_power_of_two()`] to limit integer parameter ranges to be only powers of two.
277pub fn s2v_i32_power_of_two() -> Arc<dyn Fn(&str) -> Option<i32> + Send + Sync> {
278    Arc::new(|string| string.parse().ok().map(|n: i32| (n as f32).log2() as i32))
279}
280
281/// Turns an integer MIDI note number (usually in the range [0, 127]) into a note name, where 60 is
282/// C4 and 69 is A4 (nice).
283#[deprecated(since = "0.4.1", note = "Use v2s_i32_key_formatter instead")]
284pub fn v2s_i32_note_formatter() -> Arc<dyn Fn(i32) -> String + Send + Sync> {
285    v2s_i32_key_formatter()
286}
287
288/// Parse a note name to a MIDI number using the inverse mapping from [`v2s_i32_note_formatter()`].
289#[deprecated(since = "0.4.1", note = "Use s2v_i32_key_formatter instead")]
290pub fn s2v_i32_note_formatter() -> Arc<dyn Fn(&str) -> Option<i32> + Send + Sync> {
291    s2v_i32_key_formatter()
292}
293
294/// Turns an integer MIDI key number (usually in the range [0, 127]) into a key name, where 60 is
295/// C4 and 69 is A4 (nice).
296pub fn v2s_i32_key_formatter() -> Arc<dyn Fn(i32) -> String + Send + Sync> {
297    Arc::new(move |value| {
298        let key_name = util::KEYS[value.rem_euclid(12) as usize];
299        let octave = (value / 12) - 1;
300        format!("{key_name}{octave}")
301    })
302}
303
304/// Parse a key name to a MIDI key number using the inverse mapping from [`v2s_i32_key_formatter()`].
305pub fn s2v_i32_key_formatter() -> Arc<dyn Fn(&str) -> Option<i32> + Send + Sync> {
306    Arc::new(|string| {
307        let string = string.trim();
308        if string.len() < 2 {
309            return None;
310        }
311
312        // A valid trimmed string will either be be at least two characters (we already checked the
313        // length) or at least three characters if the second character is a hash, and there may be
314        // spaces in between the key name and the octave number
315        let (key_name, octave) = string
316            .split_once(|c: char| c.is_whitespace())
317            .unwrap_or_else(|| {
318                // Sharps need to be handled separately
319                if string.len() > 2 && &string[1..2] == "#" {
320                    (&string[..2], &string[2..])
321                } else {
322                    (&string[..1], &string[1..])
323                }
324            });
325
326        let key_number = util::KEYS
327            .iter()
328            .position(|&candidate| key_name.eq_ignore_ascii_case(candidate))?
329            as i32;
330        let octave: i32 = octave.trim().parse().ok()?;
331
332        // 0 = C-1, 12 = C0, 24 = C1
333        Some(key_number + (12 * (octave + 1)))
334    })
335}
336
337/// Display 'Bypassed' or 'Not Bypassed' depending on whether the parameter is true or false.
338/// 'Enabled' would have also been a possibility here, but that could be a bit confusing.
339pub fn v2s_bool_bypass() -> Arc<dyn Fn(bool) -> String + Send + Sync> {
340    Arc::new(move |value| {
341        if value {
342            String::from("Bypassed")
343        } else {
344            String::from("Not Bypassed")
345        }
346    })
347}
348
349/// Parse a string in the same format as [`v2s_bool_bypass()`].
350pub fn s2v_bool_bypass() -> Arc<dyn Fn(&str) -> Option<bool> + Send + Sync> {
351    Arc::new(|string| {
352        let string = string.trim();
353        if string.eq_ignore_ascii_case("bypassed") {
354            Some(true)
355        } else if string.eq_ignore_ascii_case("not bypassed") {
356            Some(false)
357        } else {
358            None
359        }
360    })
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366
367    /// The rounding function should never return strings containing negative zero values.
368    #[test]
369    fn v2s_f32_rounded_negative_zero() {
370        let v2s = v2s_f32_rounded(2);
371
372        assert_eq!("0.00", v2s(-0.001));
373
374        // Sanity check
375        assert_eq!("-0.01", v2s(-0.009));
376        assert_eq!("0.01", v2s(0.009));
377    }
378
379    // More of these validators could use tests, but this one in particular is tricky and I noticed
380    // an issue where it didn't roundtrip correctly
381    #[test]
382    fn f32_hz_then_khz_with_key_name_roundtrip() {
383        let v2s = v2s_f32_hz_then_khz_with_key_name(1, true);
384        let s2v = s2v_f32_hz_then_khz();
385
386        for freq in [0.0, 5.0, 7.18, 8.18, 69.420, 18181.8, 133333.7] {
387            let string = v2s(freq);
388            // We can't compare `freq` and `roundtrip_freq` because the string is rounded on both
389            // cents and frequency and is thus lossy
390            let roundtrip_freq = s2v(&string).unwrap();
391            let roundtrip_string = v2s(roundtrip_freq);
392            assert_eq!(
393                string, roundtrip_string,
394                "Unexpected: {string} -> {roundtrip_freq} -> {roundtrip_string}"
395            );
396        }
397    }
398}