synth_utils/
quantizer.rs

1//! # Quantizer
2//!
3//! Quantizers are used to force continuous inputs into discrete output steps. Musically they are used to generate
4//! in-tune outputs from various inputs.
5//!
6//! This quantizer operates similarly to common hardware quantizers, using 1volt/octave scaling. This means that each
7//! octave spans 1 volt, and so each semitone spans 1/12 of a volt, or about 83.3mV
8//!
9//! Specific notes may be allowed or forbidden, allowing the user to program user defined scales.
10
11use heapless::Vec;
12
13/// A quantizer which converts smooth inputs into stairsteps is represented here.
14pub struct Quantizer {
15    // save the last conversion for hysteresis purposes
16    cached_conversion: Conversion,
17
18    // allowed notes are represented as an integer bitfield
19    // the 12 lowest bits represent C, C#, D, ... B
20    // a set-bit means the note is allowed, cleared-bit means the note is forbidden
21    allowed: u16,
22}
23
24/// A quantizer conversion is represented here.
25///
26/// Conversions consist of a stairstep portion and fractional portion.
27/// The stairstep is the input value converted to a stairstep with as many steps as there are semitones, and the
28/// fractional part is the difference between the actual input value and the quantized stairstep.
29///
30/// The stairstep will always be positive, the fraction may be positive or negative.
31/// The stairstep plus the fraction will get us back to the original input value.
32///
33/// The integer note number is also included.
34#[derive(Clone, Copy)]
35pub struct Conversion {
36    /// The integer note number of the conversion
37    pub note_num: u8,
38    /// The conversion as a stairstep pattern, in the same range as the input except quantized to discrete steps
39    pub stairstep: f32,
40    /// The fractional remainder of the stairstep, `stairstep + fraction` results in the original input value
41    pub fraction: f32,
42}
43
44#[allow(clippy::new_without_default)]
45impl Conversion {
46    /// `Conversion::new()` is a new conversion
47    pub fn new() -> Self {
48        Self {
49            note_num: 0,
50            stairstep: f32::MIN, // initialized so that hysteresis doesn't influence the first conversion
51            fraction: 0.0_f32,
52        }
53    }
54}
55
56#[allow(clippy::new_without_default)]
57impl Quantizer {
58    /// `Quantizer::new()` is a new quantizer with all notes allowed.
59    pub fn new() -> Self {
60        Self {
61            cached_conversion: Conversion::new(),
62            allowed: 0b0000_1111_1111_1111, // all 12 notes allowed
63        }
64    }
65
66    /// `q.convert(val)` is the quantized version of the input value.
67    ///
68    /// The input is split into a stairstep component and fractional component.
69    ///
70    /// # Arguments
71    ///
72    /// * `v_in` - the value to quantize, in volts, clamped to `[0.0, V_MAX]`
73    ///
74    /// # Returns
75    ///
76    /// * `Conversion` - the input split into a stairstep and fractional portion
77    ///
78    /// # Examples
79    ///
80    /// ```
81    /// # use synth_utils::quantizer;
82    /// let mut q = quantizer::Quantizer::new();
83    /// // input is a bit above C#, but C# is the closest note number
84    /// assert_eq!(q.convert(1.5 / 12.).note_num, 1);
85    ///
86    /// // same input, but since C# is forbidden now D is the closest note
87    /// q.forbid(&[quantizer::Note::CSHARP]);
88    /// assert_eq!(q.convert(1.5 / 12.).note_num, 2);
89    /// ```
90    ///
91    pub fn convert(&mut self, v_in: f32) -> Conversion {
92        // return early if vin is within the window of the last coversion plus a little hysteresis
93        if self.is_allowed(self.cached_conversion.note_num.into()) {
94            let low_bound = self.cached_conversion.stairstep - HYSTERESIS;
95            let high_bound = self.cached_conversion.stairstep + SEMITONE_WIDTH + HYSTERESIS;
96
97            if low_bound < v_in && v_in < high_bound {
98                self.cached_conversion.fraction = v_in - self.cached_conversion.stairstep;
99                return self.cached_conversion;
100            }
101        }
102
103        let v_in = v_in.max(0.0_f32).min(V_MAX);
104
105        self.cached_conversion.note_num = self.find_nearest_note(v_in);
106        self.cached_conversion.stairstep = self.cached_conversion.note_num as f32 / 12.0_f32;
107        self.cached_conversion.fraction = v_in - self.cached_conversion.stairstep;
108
109        self.cached_conversion
110    }
111
112    /// `q.find_nearest_note(v)` is 1volt/octave voltage `v` converted to the nearest semitone number
113    fn find_nearest_note(&self, v_in: f32) -> u8 {
114        let vin_microvolts = (v_in * ONE_OCTAVE_IN_MICROVOLTS as f32) as u32;
115        let octave_num_of_vin = vin_microvolts / ONE_OCTAVE_IN_MICROVOLTS;
116
117        // we want to look in either two or three octaves to find the nearest note
118        // it might be in the same octave as the input, but the nearest note might also be in the octave above or below
119        // we can't go below octave zero or above MAX_OCTAVE, so there might be only two to check if we're near an edge
120        let mut octaves_to_search = Vec::<u32, 3>::new();
121        octaves_to_search.push(octave_num_of_vin).ok();
122        if 1 <= octave_num_of_vin {
123            octaves_to_search.push(octave_num_of_vin - 1).ok();
124        }
125        if octave_num_of_vin < MAX_OCTAVE {
126            octaves_to_search.push(octave_num_of_vin + 1).ok();
127        }
128
129        let mut nearest_note_so_far_microvolts = 0;
130        let mut smallest_delta_so_far = u32::MAX;
131
132        for octave in octaves_to_search {
133            for n in 0..12 {
134                let this_note_is_enabled = (self.allowed >> n) & 1 == 1;
135
136                if this_note_is_enabled {
137                    let candidate_note_microvolts =
138                        n * HALF_STEP_IN_MICROVOLTS + octave * ONE_OCTAVE_IN_MICROVOLTS;
139
140                    let delta = delta(vin_microvolts, candidate_note_microvolts);
141
142                    // early return if we get very close to an enabled note, this must be the one
143                    if delta < HALF_STEP_IN_MICROVOLTS {
144                        return (candidate_note_microvolts / HALF_STEP_IN_MICROVOLTS) as u8;
145                    }
146
147                    // early return if delta starts getting bigger, this means that we passed the right note
148                    if smallest_delta_so_far < delta {
149                        return (nearest_note_so_far_microvolts / HALF_STEP_IN_MICROVOLTS) as u8;
150                    }
151
152                    if delta < smallest_delta_so_far {
153                        smallest_delta_so_far = delta;
154                        nearest_note_so_far_microvolts = candidate_note_microvolts;
155                    }
156                }
157            }
158        }
159
160        (nearest_note_so_far_microvolts / HALF_STEP_IN_MICROVOLTS) as u8
161    }
162
163    /// `q.allow(ns)` allows notes `ns`, meaning they will be included in conversions
164    ///
165    /// Any notes in `ns` that are already allowed are left unchanged
166    pub fn allow(&mut self, notes: &[Note]) {
167        notes.iter().for_each(|n| {
168            self.allowed |= 1 << n.0;
169        })
170    }
171
172    /// `q.forbid(ns)` forbids notes `ns`, they will not be included in conversions even if they are the nearest note
173    ///
174    /// Any notes in `ns` that are already forbidden are left unchanged
175    ///
176    /// At least one note must always be left allowed. If `ns` would forbid every note, the last note in `ns` will not
177    /// be forbidden and instead will be left allowed.
178    pub fn forbid(&mut self, notes: &[Note]) {
179        notes.iter().for_each(|n| self.allowed &= !(1 << n.0));
180        if self.allowed == 0 {
181            self.allow(&notes[notes.len() - 1..])
182        }
183    }
184
185    /// `q.is_allowed(n)` is true iff note `n` is allowed
186    pub fn is_allowed(&self, note: Note) -> bool {
187        self.allowed >> note.0 & 1 == 1
188    }
189}
190
191fn delta(v1: u32, v2: u32) -> u32 {
192    if v1 < v2 {
193        v2 - v1
194    } else {
195        v1 - v2
196    }
197}
198
199/// Note names are represented here, the quantizer can allow and forbid various notes from being converted
200#[derive(Clone, Copy, PartialEq, Eq)]
201pub struct Note(u8);
202
203impl Note {
204    pub const C: Self = Self::new(0);
205    pub const CSHARP: Self = Self::new(1);
206    pub const D: Self = Self::new(2);
207    pub const DSHARP: Self = Self::new(3);
208    pub const E: Self = Self::new(4);
209    pub const F: Self = Self::new(5);
210    pub const FSHARP: Self = Self::new(6);
211    pub const G: Self = Self::new(7);
212    pub const GSHARP: Self = Self::new(8);
213    pub const A: Self = Self::new(9);
214    pub const ASHARP: Self = Self::new(10);
215    pub const B: Self = Self::new(11);
216
217    /// `Note::new(n)` is a new note from `n` clamped to `[0..11]`
218    pub const fn new(n: u8) -> Self {
219        Self(if n <= 11 { n } else { 11 })
220    }
221}
222
223impl From<u8> for Note {
224    fn from(n: u8) -> Self {
225        Self::new(n)
226    }
227}
228
229impl From<Note> for u8 {
230    fn from(n: Note) -> Self {
231        n.0
232    }
233}
234
235pub const NUM_NOTES_PER_OCTAVE: f32 = 12.0_f32;
236
237/// The width of each bucket for the semitones.
238pub const SEMITONE_WIDTH: f32 = 1.0_f32 / NUM_NOTES_PER_OCTAVE;
239pub const HALF_SEMITONE_WIDTH: f32 = SEMITONE_WIDTH / 2.0_f32;
240
241/// Hysteresis provides some noise immunity and prevents oscillations near transition regions.
242const HYSTERESIS: f32 = SEMITONE_WIDTH * 0.1_f32;
243
244const ONE_OCTAVE_IN_MICROVOLTS: u32 = 1_000_000;
245
246const HALF_STEP_IN_MICROVOLTS: u32 = ONE_OCTAVE_IN_MICROVOLTS / 12;
247
248const MAX_OCTAVE: u32 = 10;
249
250const V_MAX: f32 = MAX_OCTAVE as f32;
251
252#[cfg(test)]
253#[allow(non_snake_case)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn vin_0_is_note_num_zero_with_all_allowed() {
259        let mut q = Quantizer::new();
260        assert_eq!(q.convert(0.0).note_num, 0);
261    }
262
263    #[test]
264    fn vin_point_08333_is_note_num_1_with_all_allowed() {
265        let mut q = Quantizer::new();
266        assert_eq!(q.convert(1. / 12.).note_num, 1);
267    }
268
269    #[test]
270    fn vin_1_is_note_num_12_with_all_allowed() {
271        let mut q = Quantizer::new();
272        assert_eq!(q.convert(1.).note_num, 12);
273    }
274
275    #[test]
276    fn vin_2_point_08333_is_note_num_25_with_all_allowed() {
277        let mut q = Quantizer::new();
278        assert_eq!(q.convert(2. + 1. / 12.).note_num, 25);
279    }
280
281    #[test]
282    fn when_C_is_forbidden_vin_0_is_1() {
283        let mut q = Quantizer::new();
284        q.forbid(&[Note::C]);
285        assert_eq!(q.convert(0.0).note_num, 1);
286    }
287
288    #[test]
289    fn when_only_B_is_allowed_vin_0_is_11() {
290        let mut q = Quantizer::new();
291        q.forbid(&[
292            Note::C,
293            Note::CSHARP,
294            Note::D,
295            Note::DSHARP,
296            Note::E,
297            Note::F,
298            Note::FSHARP,
299            Note::G,
300            Note::GSHARP,
301            Note::A,
302            Note::ASHARP,
303            // Note::B,
304        ]);
305        assert_eq!(q.convert(0.0).note_num, 11);
306    }
307
308    #[test]
309    fn when_only_Dsharp_is_allowed_vin_8_12ths_is_3() {
310        let mut q = Quantizer::new();
311        q.forbid(&[
312            Note::C,
313            Note::CSHARP,
314            Note::D,
315            // Note::Dsharp,
316            Note::E,
317            Note::F,
318            Note::FSHARP,
319            Note::G,
320            Note::GSHARP,
321            Note::A,
322            Note::ASHARP,
323            Note::B,
324        ]);
325        // it picks the D# in octave zero
326        assert_eq!(q.convert(8. / 12.).note_num, 3);
327    }
328
329    #[test]
330    fn when_only_Dsharp_is_allowed_vin_10_12ths_is_15() {
331        let mut q = Quantizer::new();
332        q.forbid(&[
333            Note::C,
334            Note::CSHARP,
335            Note::D,
336            // Note::Dsharp,
337            Note::E,
338            Note::F,
339            Note::FSHARP,
340            Note::G,
341            Note::GSHARP,
342            Note::A,
343            Note::ASHARP,
344            Note::B,
345        ]);
346        // it picks the D# in octave 1
347        assert_eq!(q.convert(10. / 12.).note_num, 15);
348    }
349
350    #[test]
351    fn can_not_forbid_every_note() {
352        let mut q = Quantizer::new();
353        // try to forbid every note
354        q.forbid(&[
355            Note::C,
356            Note::CSHARP,
357            Note::D,
358            Note::DSHARP,
359            Note::E,
360            Note::F,
361            Note::FSHARP,
362            Note::G,
363            Note::GSHARP,
364            Note::A,
365            Note::ASHARP,
366            Note::B,
367        ]);
368        // B is still left, because it is the last one we tried to forbid
369        assert_eq!(q.convert(0.5).note_num, 11);
370    }
371
372    #[test]
373    fn hysteresis_widens_window() {
374        let mut q = Quantizer::new();
375
376        // register a conversion with note number 1
377        assert_eq!(q.convert(1. / 12. + HALF_SEMITONE_WIDTH * 0.99).note_num, 1);
378
379        // it is now a little harder to get back out of 1, due to hysteresis
380        assert_eq!(q.convert(1. / 12. - HYSTERESIS * 0.99).note_num, 1);
381        assert_eq!(
382            q.convert(1. / 12. + SEMITONE_WIDTH + HYSTERESIS * 0.99)
383                .note_num,
384            1
385        );
386
387        // starting from scratch the same input values map to the below and above semitones
388        let mut q = Quantizer::new();
389        assert_eq!(q.convert(1. / 12. - HYSTERESIS * 0.99).note_num, 0);
390
391        let mut q = Quantizer::new();
392        assert_eq!(
393            q.convert(1. / 12. + SEMITONE_WIDTH + HYSTERESIS * 0.99)
394                .note_num,
395            2
396        );
397    }
398
399    #[test]
400    fn stairstep_plus_fraction_is_vin() {
401        let mut q = Quantizer::new();
402        let v_in = 1.234;
403        let conversion = q.convert(v_in);
404        assert_eq!(conversion.stairstep + conversion.fraction, v_in);
405    }
406}