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