1use heapless::Vec;
12
13pub struct Quantizer {
15 cached_conversion: Conversion,
17
18 allowed: u16,
22}
23
24#[derive(Clone, Copy)]
35pub struct Conversion {
36 pub note_num: u8,
38 pub stairstep: f32,
40 pub fraction: f32,
42}
43
44#[allow(clippy::new_without_default)]
45impl Conversion {
46 pub fn new() -> Self {
48 Self {
49 note_num: 0,
50 stairstep: f32::MIN, fraction: 0.0_f32,
52 }
53 }
54}
55
56#[allow(clippy::new_without_default)]
57impl Quantizer {
58 pub fn new() -> Self {
60 Self {
61 cached_conversion: Conversion::new(),
62 allowed: 0b0000_1111_1111_1111, }
64 }
65
66 pub fn convert(&mut self, v_in: f32) -> Conversion {
92 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 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 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 if delta < HALF_STEP_IN_MICROVOLTS {
144 return (candidate_note_microvolts / HALF_STEP_IN_MICROVOLTS) as u8;
145 }
146
147 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 pub fn allow(&mut self, notes: &[Note]) {
167 notes.iter().for_each(|n| {
168 self.allowed |= 1 << n.0;
169 })
170 }
171
172 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(¬es[notes.len() - 1..])
182 }
183 }
184
185 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#[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 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
237pub const SEMITONE_WIDTH: f32 = 1.0_f32 / NUM_NOTES_PER_OCTAVE;
239pub const HALF_SEMITONE_WIDTH: f32 = SEMITONE_WIDTH / 2.0_f32;
240
241const 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 ]);
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::E,
317 Note::F,
318 Note::FSHARP,
319 Note::G,
320 Note::GSHARP,
321 Note::A,
322 Note::ASHARP,
323 Note::B,
324 ]);
325 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::E,
338 Note::F,
339 Note::FSHARP,
340 Note::G,
341 Note::GSHARP,
342 Note::A,
343 Note::ASHARP,
344 Note::B,
345 ]);
346 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 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 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 assert_eq!(q.convert(1. / 12. + HALF_SEMITONE_WIDTH * 0.99).note_num, 1);
378
379 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 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}