development
rs 213 lines 6.04 KB
Raw
1 //! Scales and modes, spelled diatonically from a formula of degree tokens.
2
3 use anyhow::{Result, bail};
4
5 use super::interval::Interval;
6 use super::pitch::Note;
7
8 pub struct ScaleDef {
9 pub name: &'static str,
10 pub aliases: &'static [&'static str],
11 /// Degree tokens relative to the major scale.
12 pub formula: &'static str,
13 }
14
15 const fn def(
16 name: &'static str,
17 aliases: &'static [&'static str],
18 formula: &'static str,
19 ) -> ScaleDef {
20 ScaleDef {
21 name,
22 aliases,
23 formula,
24 }
25 }
26
27 pub const SCALES: &[ScaleDef] = &[
28 def("major", &["ionian", "maj"], "1 2 3 4 5 6 7"),
29 def(
30 "natural minor",
31 &["minor", "aeolian", "min"],
32 "1 2 b3 4 5 b6 b7",
33 ),
34 def("dorian", &[], "1 2 b3 4 5 6 b7"),
35 def("phrygian", &[], "1 b2 b3 4 5 b6 b7"),
36 def("lydian", &[], "1 2 3 #4 5 6 7"),
37 def("mixolydian", &["dominant"], "1 2 3 4 5 6 b7"),
38 def("locrian", &[], "1 b2 b3 4 b5 b6 b7"),
39 def("harmonic minor", &[], "1 2 b3 4 5 b6 7"),
40 def("melodic minor", &["jazz minor"], "1 2 b3 4 5 6 7"),
41 def("harmonic major", &[], "1 2 3 4 5 b6 7"),
42 def("dorian b2", &["phrygian #6"], "1 b2 b3 4 5 6 b7"),
43 def("lydian augmented", &[], "1 2 3 #4 #5 6 7"),
44 def(
45 "lydian dominant",
46 &["overtone", "acoustic"],
47 "1 2 3 #4 5 6 b7",
48 ),
49 def(
50 "mixolydian b6",
51 &["hindu", "melodic major"],
52 "1 2 3 4 5 b6 b7",
53 ),
54 def(
55 "locrian #2",
56 &["half-diminished", "aeolian b5"],
57 "1 2 b3 4 b5 b6 b7",
58 ),
59 def(
60 "altered",
61 &["super locrian", "diminished whole tone"],
62 "1 b2 b3 b4 b5 b6 b7",
63 ),
64 def(
65 "phrygian dominant",
66 &["spanish", "freygish"],
67 "1 b2 3 4 5 b6 b7",
68 ),
69 def("major pentatonic", &["pentatonic major"], "1 2 3 5 6"),
70 def(
71 "minor pentatonic",
72 &["pentatonic minor", "pentatonic"],
73 "1 b3 4 5 b7",
74 ),
75 def("blues", &["minor blues"], "1 b3 4 b5 5 b7"),
76 def("major blues", &[], "1 2 b3 3 5 6"),
77 def("whole tone", &[], "1 2 3 #4 #5 b7"),
78 def(
79 "diminished whole-half",
80 &["octatonic whole-half", "diminished"],
81 "1 2 b3 4 b5 b6 6 7",
82 ),
83 def(
84 "diminished half-whole",
85 &["octatonic half-whole", "dominant diminished"],
86 "1 b2 b3 3 #4 5 6 b7",
87 ),
88 def("bebop dominant", &[], "1 2 3 4 5 6 b7 7"),
89 ];
90
91 fn norm(s: &str) -> String {
92 s.trim()
93 .to_lowercase()
94 .replace(['_', '-'], " ")
95 .split_whitespace()
96 .collect::<Vec<_>>()
97 .join(" ")
98 }
99
100 pub fn find(name: &str) -> Option<&'static ScaleDef> {
101 let n = norm(name);
102 SCALES
103 .iter()
104 .find(|d| norm(d.name) == n || d.aliases.iter().any(|a| norm(a) == n))
105 }
106
107 pub fn names() -> Vec<&'static str> {
108 SCALES.iter().map(|d| d.name).collect()
109 }
110
111 impl ScaleDef {
112 pub fn intervals(&self) -> Vec<Interval> {
113 self.formula
114 .split_whitespace()
115 .filter_map(Interval::from_token)
116 .collect()
117 }
118
119 pub fn spell(&self, root: Note) -> Vec<Note> {
120 self.intervals()
121 .into_iter()
122 .map(|i| root.up(i.steps, i.semis))
123 .collect()
124 }
125 }
126
127 pub fn parse_scale(name: &str) -> Result<&'static ScaleDef> {
128 match find(name) {
129 Some(d) => Ok(d),
130 None => bail!("unknown scale `{name}`; known: {}", names().join(", ")),
131 }
132 }
133
134 /// Signed accidental count of a diatonic scale's key signature (+ sharps, - flats).
135 pub fn signature(notes: &[Note]) -> i32 {
136 notes.iter().map(|n| n.acc as i32).sum()
137 }
138
139 /// Names the accidentals of a key signature in order of appearance, e.g. `F# C#`.
140 pub fn signature_text(count: i32) -> String {
141 const SHARPS: [&str; 7] = ["F#", "C#", "G#", "D#", "A#", "E#", "B#"];
142 const FLATS: [&str; 7] = ["Bb", "Eb", "Ab", "Db", "Gb", "Cb", "Fb"];
143 match count {
144 0 => "no sharps or flats".into(),
145 n if n > 0 && n <= 7 => format!(
146 "{n} sharp{} ({})",
147 plural(n),
148 SHARPS[..n as usize].join(" ")
149 ),
150 n if (-7..0).contains(&n) => {
151 let n = -n;
152 format!("{n} flat{} ({})", plural(n), FLATS[..n as usize].join(" "))
153 }
154 n => format!("{n} accidentals (theoretical key with double accidentals)"),
155 }
156 }
157
158 fn plural(n: i32) -> &'static str {
159 if n == 1 { "" } else { "s" }
160 }
161
162 #[cfg(test)]
163 mod tests {
164 use super::*;
165
166 fn spelled(root: &str, scale: &str) -> String {
167 let notes = parse_scale(scale)
168 .unwrap()
169 .spell(Note::parse(root).unwrap());
170 notes.iter().map(|n| n.name()).collect::<Vec<_>>().join(" ")
171 }
172
173 #[test]
174 fn c_sharp_major_has_e_sharp_and_b_sharp() {
175 assert_eq!(spelled("C#", "major"), "C# D# E# F# G# A# B#");
176 let notes = parse_scale("major")
177 .unwrap()
178 .spell(Note::parse("C#").unwrap());
179 assert_eq!(signature(&notes), 7);
180 }
181
182 #[test]
183 fn flat_keys_and_modes() {
184 assert_eq!(spelled("Gb", "major"), "Gb Ab Bb Cb Db Eb F");
185 assert_eq!(spelled("D", "dorian"), "D E F G A B C");
186 assert_eq!(spelled("F#", "locrian"), "F# G A B C D E");
187 assert_eq!(spelled("A", "harmonic minor"), "A B C D E F G#");
188 assert_eq!(spelled("C", "lydian"), "C D E F# G A B");
189 assert_eq!(spelled("C", "altered"), "C Db Eb Fb Gb Ab Bb");
190 }
191
192 #[test]
193 fn pentatonic_and_blues() {
194 assert_eq!(spelled("A", "minor pentatonic"), "A C D E G");
195 assert_eq!(spelled("C", "major pentatonic"), "C D E G A");
196 assert_eq!(spelled("E", "blues"), "E G A Bb B D");
197 assert_eq!(spelled("C", "whole tone"), "C D E F# G# Bb");
198 }
199
200 #[test]
201 fn aliases_and_unknown() {
202 assert!(find("Aeolian").is_some());
203 assert!(find("natural-minor").is_some());
204 assert!(parse_scale("klingon").is_err());
205 }
206
207 #[test]
208 fn signature_text_names_accidentals() {
209 assert_eq!(signature_text(2), "2 sharps (F# C#)");
210 assert_eq!(signature_text(-1), "1 flat (Bb)");
211 assert_eq!(signature_text(0), "no sharps or flats");
212 }
213 }