use std::ops::RangeInclusive;
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct Scale {
intervals: Vec<u8>,
name: String,
}
impl Scale {
#[must_use]
pub fn new(intervals: Vec<u8>, name: impl Into<String>) -> Self {
let mut ivs: Vec<u8> = intervals.into_iter().filter(|&i| i < 12).collect();
ivs.sort_unstable();
ivs.dedup();
if ivs.is_empty() {
ivs.push(0);
}
Self {
intervals: ivs,
name: name.into(),
}
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn intervals(&self) -> &[u8] {
&self.intervals
}
#[must_use]
pub fn quantize(&self, raw_note: u8, root: u8) -> u8 {
if self.is_in_scale(raw_note, root) {
return raw_note;
}
for distance in 1..=127i16 {
let below = raw_note as i16 - distance;
let above = raw_note as i16 + distance;
if below >= 0 && self.is_in_scale(below as u8, root) {
return below as u8;
}
if above <= 127 && self.is_in_scale(above as u8, root) {
return above as u8;
}
}
raw_note
}
fn is_in_scale(&self, note: u8, root: u8) -> bool {
let degree = (note as i16 - root as i16).rem_euclid(12) as u8;
self.intervals.contains(°ree)
}
#[must_use]
pub fn chromatic() -> Self {
Self::new(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], "Chromatic")
}
#[must_use]
pub fn major() -> Self {
Self::new(vec![0, 2, 4, 5, 7, 9, 11], "Major")
}
#[must_use]
pub fn natural_minor() -> Self {
Self::new(vec![0, 2, 3, 5, 7, 8, 10], "Natural Minor")
}
#[must_use]
pub fn harmonic_minor() -> Self {
Self::new(vec![0, 2, 3, 5, 7, 8, 11], "Harmonic Minor")
}
#[must_use]
pub fn pentatonic_major() -> Self {
Self::new(vec![0, 2, 4, 7, 9], "Pentatonic Major")
}
#[must_use]
pub fn pentatonic_minor() -> Self {
Self::new(vec![0, 3, 5, 7, 10], "Pentatonic Minor")
}
#[must_use]
pub fn blues() -> Self {
Self::new(vec![0, 3, 5, 6, 7, 10], "Blues")
}
#[must_use]
pub fn dorian() -> Self {
Self::new(vec![0, 2, 3, 5, 7, 9, 10], "Dorian")
}
#[must_use]
pub fn phrygian() -> Self {
Self::new(vec![0, 1, 3, 5, 7, 8, 10], "Phrygian")
}
#[must_use]
pub fn lydian() -> Self {
Self::new(vec![0, 2, 4, 6, 7, 9, 11], "Lydian")
}
#[must_use]
pub fn mixolydian() -> Self {
Self::new(vec![0, 2, 4, 5, 7, 9, 10], "Mixolydian")
}
#[must_use]
pub fn whole_tone() -> Self {
Self::new(vec![0, 2, 4, 6, 8, 10], "Whole Tone")
}
#[must_use]
pub fn diminished() -> Self {
Self::new(vec![0, 2, 3, 5, 6, 8, 9, 11], "Diminished")
}
#[must_use]
pub fn augmented() -> Self {
Self::new(vec![0, 3, 4, 7, 8, 11], "Augmented")
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Quantizer {
scale: Scale,
root: u8,
range: RangeInclusive<u8>,
}
impl Quantizer {
#[must_use]
pub fn new(scale: Scale, root: u8) -> Self {
Self {
scale,
root: root.min(11),
range: 36..=84,
}
}
pub fn set_scale(&mut self, scale: Scale) {
self.scale = scale;
}
pub fn set_root(&mut self, root: u8) {
self.root = root.min(11);
}
pub fn set_range(&mut self, range: RangeInclusive<u8>) {
self.range = range;
}
#[must_use]
pub fn note_from_dac(&self, dac: u8) -> u8 {
let lo = *self.range.start() as u16;
let hi = *self.range.end() as u16;
let span = hi.saturating_sub(lo);
let raw = lo + (u16::from(dac) * span + 127) / 255;
let raw = raw.min(127) as u8;
self.scale
.quantize(raw, self.root)
.clamp(*self.range.start(), *self.range.end())
.max(1)
}
#[must_use]
pub fn velocity_from_dac(&self, dac: u8) -> u8 {
(dac & 0x7F).max(1)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chromatic_passthrough() {
let q = Quantizer {
scale: Scale::chromatic(),
root: 0,
range: 0..=127,
};
assert_eq!(q.note_from_dac(0), 1, "MIDI note 0 must never be output");
assert!(q.note_from_dac(255) <= 127);
let mid = q.note_from_dac(128);
assert!(
(58..=68).contains(&mid),
"mid-range DAC should map near 64, got {mid}"
);
}
#[test]
fn test_major_snaps_correctly() {
let scale = Scale::major();
assert_eq!(scale.quantize(61, 0), 60);
}
#[test]
fn test_range_respected() {
let q = Quantizer::new(Scale::major(), 0);
assert!(q.note_from_dac(0) >= 36, "must be >= range start");
assert!(q.note_from_dac(255) <= 84, "must be <= range end");
}
#[test]
fn test_velocity_never_zero() {
let q = Quantizer::new(Scale::chromatic(), 0);
for dac in 0..=255u8 {
let vel = q.velocity_from_dac(dac);
assert!(vel >= 1, "velocity must be >= 1 for dac={dac}, got {vel}");
assert!(vel <= 127, "velocity must be <= 127 for dac={dac}");
}
}
#[test]
fn test_pentatonic_quantize() {
let scale = Scale::pentatonic_major();
assert_eq!(scale.quantize(60, 0), 60);
assert_eq!(scale.quantize(61, 0), 60);
assert_eq!(scale.quantize(65, 0), 64);
assert_eq!(scale.quantize(66, 0), 67);
assert_eq!(scale.quantize(70, 0), 69);
}
#[test]
fn test_scale_new_sorts_and_deduplicates() {
let scale = Scale::new(vec![7, 0, 4, 4, 0, 2], "Test");
assert_eq!(scale.intervals(), &[0, 2, 4, 7]);
}
#[test]
fn test_scale_new_empty_gets_root() {
let scale = Scale::new(vec![], "Empty");
assert_eq!(scale.intervals(), &[0]);
}
#[test]
fn test_scale_new_filters_out_of_range() {
let scale = Scale::new(vec![0, 5, 12, 15, 200], "Filtered");
assert_eq!(scale.intervals(), &[0, 5]);
}
#[test]
fn test_scale_name() {
assert_eq!(Scale::major().name(), "Major");
assert_eq!(Scale::blues().name(), "Blues");
assert_eq!(Scale::chromatic().name(), "Chromatic");
}
#[test]
fn test_quantize_at_boundaries() {
let scale = Scale::major();
assert_eq!(scale.quantize(0, 0), 0);
assert_eq!(scale.quantize(127, 0), 127);
}
#[test]
fn test_quantizer_set_root_clamps() {
let mut q = Quantizer::new(Scale::chromatic(), 0);
q.set_root(15);
assert_eq!(q.root, 11);
}
#[test]
fn test_note_from_dac_never_zero() {
let q = Quantizer {
scale: Scale::chromatic(),
root: 0,
range: 0..=127,
};
assert!(q.note_from_dac(0) >= 1);
}
#[test]
fn test_velocity_from_dac_lower_7_bits() {
let q = Quantizer::new(Scale::chromatic(), 0);
assert_eq!(q.velocity_from_dac(128), 1);
assert_eq!(q.velocity_from_dac(255), 127);
assert_eq!(q.velocity_from_dac(64), 64);
}
#[test]
fn test_quantize_with_nonzero_root() {
let scale = Scale::major();
assert_eq!(scale.quantize(61, 2), 61);
assert_eq!(scale.quantize(60, 2), 59);
}
#[test]
fn test_quantizer_set_root_wraps_or_clamps() {
let mut q = Quantizer::new(Scale::chromatic(), 0);
q.set_root(12);
assert_eq!(q.root, 11, "root 12 should be clamped to 11");
q.set_root(255);
assert_eq!(q.root, 11, "root 255 should be clamped to 11");
}
#[test]
fn test_scale_root_only() {
let scale = Scale::new(vec![0], "Root Only");
assert_eq!(scale.quantize(5, 0), 0);
assert_eq!(scale.quantize(7, 0), 12);
assert_eq!(scale.quantize(6, 0), 0);
assert_eq!(scale.quantize(60, 0), 60);
}
#[test]
fn test_note_from_dac_zero_range() {
let q = Quantizer {
scale: Scale::chromatic(),
root: 0,
range: 60..=60,
};
assert_eq!(q.note_from_dac(0), 60);
assert_eq!(q.note_from_dac(128), 60);
assert_eq!(q.note_from_dac(255), 60);
}
#[test]
fn test_quantize_exact_match() {
let scale = Scale::major();
assert_eq!(scale.quantize(60, 0), 60);
assert_eq!(scale.quantize(67, 0), 67);
assert_eq!(scale.quantize(71, 0), 71);
}
#[test]
fn test_velocity_from_dac_zero() {
let q = Quantizer::new(Scale::chromatic(), 0);
assert_eq!(q.velocity_from_dac(0), 1);
}
#[test]
fn test_velocity_from_dac_max() {
let q = Quantizer::new(Scale::chromatic(), 0);
assert_eq!(q.velocity_from_dac(255), 127);
}
#[test]
fn test_note_from_dac_full_range() {
let q = Quantizer {
scale: Scale::chromatic(),
root: 0,
range: 0..=127,
};
let low = q.note_from_dac(0);
let high = q.note_from_dac(255);
assert!(low >= 1, "note must be >= 1, got {low}");
assert!(high <= 127, "note must be <= 127, got {high}");
assert!(
high - low > 100,
"full range should span at least 100 notes, got {}",
high - low
);
}
#[test]
fn test_note_from_dac_narrow_range() {
let q = Quantizer {
scale: Scale::chromatic(),
root: 0,
range: 60..=72,
};
for dac in 0..=255u8 {
let note = q.note_from_dac(dac);
assert!(
(60..=72).contains(¬e),
"note {note} out of range 60..=72 for dac={dac}"
);
}
}
#[test]
fn test_quantizer_set_scale() {
let mut q = Quantizer::new(Scale::chromatic(), 0);
q.set_scale(Scale::major());
let note = q.note_from_dac(128);
let degree = note % 12;
assert!(
[0, 2, 4, 5, 7, 9, 11].contains(°ree),
"note {note} (degree {degree}) should be in C major after set_scale"
);
}
#[test]
fn test_quantizer_set_range() {
let mut q = Quantizer::new(Scale::chromatic(), 0);
q.set_range(48..=72);
for dac in 0..=255u8 {
let note = q.note_from_dac(dac);
assert!(
(48..=72).contains(¬e),
"note {note} should be within 48..=72 for dac={dac}"
);
}
}
#[test]
fn test_quantize_near_zero_finds_above() {
let scale = Scale::pentatonic_major();
assert_eq!(scale.quantize(1, 0), 0);
assert_eq!(scale.quantize(0, 2), 2);
assert_eq!(scale.quantize(1, 2), 2);
}
#[test]
fn test_quantize_note_zero_not_in_scale() {
let scale = Scale::new(vec![0], "Root Only");
assert_eq!(scale.quantize(0, 5), 5);
}
#[test]
fn test_quantize_near_127_finds_below() {
let scale = Scale::new(vec![0], "Root Only");
assert_eq!(scale.quantize(127, 0), 120);
}
#[test]
fn test_quantize_note_126_root_only() {
let scale = Scale::new(vec![0], "Root Only");
assert_eq!(scale.quantize(126, 0), 120);
}
#[test]
fn test_natural_minor_intervals() {
let scale = Scale::natural_minor();
assert_eq!(scale.intervals(), &[0, 2, 3, 5, 7, 8, 10]);
assert_eq!(scale.name(), "Natural Minor");
}
#[test]
fn test_harmonic_minor_intervals() {
let scale = Scale::harmonic_minor();
assert_eq!(scale.intervals(), &[0, 2, 3, 5, 7, 8, 11]);
assert_eq!(scale.name(), "Harmonic Minor");
}
#[test]
fn test_pentatonic_minor_intervals() {
let scale = Scale::pentatonic_minor();
assert_eq!(scale.intervals(), &[0, 3, 5, 7, 10]);
assert_eq!(scale.name(), "Pentatonic Minor");
}
#[test]
fn test_dorian_intervals() {
let scale = Scale::dorian();
assert_eq!(scale.intervals(), &[0, 2, 3, 5, 7, 9, 10]);
assert_eq!(scale.name(), "Dorian");
}
#[test]
fn test_phrygian_intervals() {
let scale = Scale::phrygian();
assert_eq!(scale.intervals(), &[0, 1, 3, 5, 7, 8, 10]);
assert_eq!(scale.name(), "Phrygian");
}
#[test]
fn test_lydian_intervals() {
let scale = Scale::lydian();
assert_eq!(scale.intervals(), &[0, 2, 4, 6, 7, 9, 11]);
assert_eq!(scale.name(), "Lydian");
}
#[test]
fn test_mixolydian_intervals() {
let scale = Scale::mixolydian();
assert_eq!(scale.intervals(), &[0, 2, 4, 5, 7, 9, 10]);
assert_eq!(scale.name(), "Mixolydian");
}
#[test]
fn test_whole_tone_intervals() {
let scale = Scale::whole_tone();
assert_eq!(scale.intervals(), &[0, 2, 4, 6, 8, 10]);
assert_eq!(scale.name(), "Whole Tone");
}
#[test]
fn test_diminished_intervals() {
let scale = Scale::diminished();
assert_eq!(scale.intervals(), &[0, 2, 3, 5, 6, 8, 9, 11]);
assert_eq!(scale.name(), "Diminished");
}
#[test]
fn test_augmented_intervals() {
let scale = Scale::augmented();
assert_eq!(scale.intervals(), &[0, 3, 4, 7, 8, 11]);
assert_eq!(scale.name(), "Augmented");
}
#[test]
fn test_quantize_harmonic_minor() {
let scale = Scale::harmonic_minor();
assert_eq!(scale.quantize(70, 9), 69);
}
#[test]
fn test_quantize_blues() {
let scale = Scale::blues();
assert_eq!(scale.quantize(62, 0), 63);
}
#[test]
fn test_quantize_whole_tone() {
let scale = Scale::whole_tone();
assert_eq!(scale.quantize(61, 0), 60);
}
#[test]
fn test_note_from_dac_with_major_scale() {
let q = Quantizer::new(Scale::major(), 0);
for dac in 0..=255u8 {
let note = q.note_from_dac(dac);
assert!(
(36..=84).contains(¬e),
"note {note} out of range 36..=84 for dac={dac}"
);
let degree = note % 12;
assert!(
[0, 2, 4, 5, 7, 9, 11].contains(°ree),
"note {note} (degree {degree}) should be in C major for dac={dac}"
);
}
}
#[test]
fn test_note_from_dac_with_pentatonic_and_root() {
let q = Quantizer {
scale: Scale::pentatonic_major(),
root: 7, range: 48..=84,
};
for dac in 0..=255u8 {
let note = q.note_from_dac(dac);
assert!(
(48..=84).contains(¬e),
"note {note} out of range for dac={dac}"
);
}
}
#[test]
fn test_note_from_dac_low_range_start() {
let q = Quantizer {
scale: Scale::chromatic(),
root: 0,
range: 1..=12,
};
let note = q.note_from_dac(0);
assert!(note >= 1, "note must be >= 1, got {note}");
assert!(note <= 12, "note must be <= 12, got {note}");
}
#[test]
fn test_note_from_dac_high_range() {
let q = Quantizer {
scale: Scale::chromatic(),
root: 0,
range: 100..=127,
};
for dac in 0..=255u8 {
let note = q.note_from_dac(dac);
assert!(
(100..=127).contains(¬e),
"note {note} out of range 100..=127 for dac={dac}"
);
}
}
#[test]
fn test_scale_eq() {
let a = Scale::major();
let b = Scale::new(vec![0, 2, 4, 5, 7, 9, 11], "Major");
assert_eq!(a, b);
}
#[test]
fn test_scale_ne() {
assert_ne!(Scale::major(), Scale::natural_minor());
}
#[test]
fn test_scale_clone() {
let original = Scale::major();
let cloned = original.clone();
assert_eq!(original, cloned);
}
#[test]
fn test_quantizer_clone_eq() {
let q = Quantizer::new(Scale::major(), 5);
let cloned = q.clone();
assert_eq!(q, cloned);
}
#[test]
fn test_quantizer_debug() {
let q = Quantizer::new(Scale::chromatic(), 0);
let debug = format!("{q:?}");
assert!(
debug.contains("Quantizer"),
"debug output should contain 'Quantizer': {debug}"
);
assert!(
debug.contains("Chromatic"),
"debug output should contain scale name: {debug}"
);
}
#[test]
fn test_scale_debug() {
let scale = Scale::major();
let debug = format!("{scale:?}");
assert!(
debug.contains("Major"),
"debug output should contain 'Major': {debug}"
);
}
#[test]
fn test_scale_new_all_out_of_range() {
let scale = Scale::new(vec![12, 13, 255], "AllBad");
assert_eq!(scale.intervals(), &[0]);
assert_eq!(scale.name(), "AllBad");
}
}