sound_to_patch/music/
scale.rs1use crate::music::entry::{Group, Rule};
17use crate::music::{MusicError, dictionary};
18
19pub fn offsets(id: &str) -> Result<&'static [u8], MusicError> {
26 let entry = dictionary()?
27 .entry(id)
28 .filter(|entry| entry.group == Group::Scale)
29 .ok_or_else(|| MusicError::UnknownScale(id.to_owned()))?;
30 match &entry.rule {
31 Rule::PitchSet { offsets, .. } => Ok(offsets),
32 _ => Err(MusicError::UnknownScale(id.to_owned())),
33 }
34}
35
36#[must_use]
38pub fn contains(pitch: u8, root: u8, offsets: &[u8]) -> bool {
39 let offset = (i16::from(pitch) - i16::from(root)).rem_euclid(12);
40 offsets.iter().any(|&scale| i16::from(scale) == offset)
41}
42
43#[must_use]
47pub fn snap(pitch: u8, root: u8, offsets: &[u8]) -> u8 {
48 if offsets.is_empty() || contains(pitch, root, offsets) {
49 return pitch;
50 }
51 let below = (0..=pitch).rev().find(|&p| contains(p, root, offsets));
52 let above = (pitch..=127).find(|&p| contains(p, root, offsets));
53 match (below, above) {
54 (Some(low), Some(high)) if pitch - low <= high - pitch => low,
55 (_, Some(high)) => high,
56 (Some(low), None) => low,
57 (None, None) => pitch,
58 }
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64
65 const MAJOR: [u8; 7] = [0, 2, 4, 5, 7, 9, 11];
66 const MINOR_PENTATONIC: [u8; 5] = [0, 3, 5, 7, 10];
67 const NATURAL_MINOR: [u8; 7] = [0, 2, 3, 5, 7, 8, 10];
68
69 #[test]
70 fn snaps_the_published_cases() {
71 assert_eq!(snap(60, 4, &MAJOR), 59);
72 assert_eq!(snap(61, 4, &MAJOR), 61);
73 assert_eq!(snap(62, 0, &MINOR_PENTATONIC), 63);
74 assert_eq!(snap(66, 9, &NATURAL_MINOR), 65);
75 }
76
77 #[test]
78 fn a_tie_goes_to_the_lower_pitch() {
79 assert_eq!(snap(61, 0, &MAJOR), 60);
80 assert_eq!(snap(66, 0, &MAJOR), 65);
81 }
82
83 #[test]
84 fn every_note_in_c_major_moves_by_at_most_one() {
85 for pitch in 0..=127 {
86 let snapped = snap(pitch, 0, &MAJOR);
87 assert!(snapped.abs_diff(pitch) <= 1, "{pitch} -> {snapped}");
88 assert!(contains(snapped, 0, &MAJOR));
89 }
90 }
91
92 #[test]
93 fn the_ends_of_midi_snap_inward() {
94 assert_eq!(snap(127, 0, &[0]), 120);
95 assert_eq!(snap(0, 1, &[0]), 1);
96 }
97}