1use crate::error::Result;
12use crate::interval::Interval;
13use crate::pitch::Pitch;
14
15use std::collections::HashMap;
16use std::sync::LazyLock;
17
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22enum Simplification {
23 Exact,
25 MaxAccidental,
27 MostCommon,
29}
30
31static STEP_INTERVALS: LazyLock<HashMap<&'static str, Interval>> = LazyLock::new(|| {
33 ["m2", "M2", "a2", "m3", "M3", "-M2"]
34 .into_iter()
35 .map(|name| {
36 let interval =
37 Interval::from_name(name).expect("scale step intervals are valid interval names");
38 (name, interval)
39 })
40 .collect()
41});
42
43fn step_interval(name: &str) -> &'static Interval {
44 STEP_INTERVALS
45 .get(name)
46 .expect("scale tables only use intervals listed in STEP_INTERVALS")
47}
48
49#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
55#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
56#[non_exhaustive]
57pub enum ScaleType {
58 Major,
60 Minor,
62 Dorian,
64 Phrygian,
66 Lydian,
68 Mixolydian,
70 Locrian,
72 Hypodorian,
74 Hypophrygian,
76 Hypolydian,
78 Hypomixolydian,
80 Hypolocrian,
82 Hypoaeolian,
84 HarmonicMinor,
86 MelodicMinor,
88 Chromatic,
90 WholeTone,
92 Octatonic,
94 RagAsawari,
96 RagMarwa,
102}
103
104impl ScaleType {
105 pub const ALL: [ScaleType; 20] = [
107 Self::Major,
108 Self::Minor,
109 Self::Dorian,
110 Self::Phrygian,
111 Self::Lydian,
112 Self::Mixolydian,
113 Self::Locrian,
114 Self::Hypodorian,
115 Self::Hypophrygian,
116 Self::Hypolydian,
117 Self::Hypomixolydian,
118 Self::Hypolocrian,
119 Self::Hypoaeolian,
120 Self::HarmonicMinor,
121 Self::MelodicMinor,
122 Self::Chromatic,
123 Self::WholeTone,
124 Self::Octatonic,
125 Self::RagAsawari,
126 Self::RagMarwa,
127 ];
128
129 pub fn music21_name(self) -> &'static str {
131 match self {
132 Self::Major => "MajorScale",
133 Self::Minor => "MinorScale",
134 Self::Dorian => "DorianScale",
135 Self::Phrygian => "PhrygianScale",
136 Self::Lydian => "LydianScale",
137 Self::Mixolydian => "MixolydianScale",
138 Self::Locrian => "LocrianScale",
139 Self::Hypodorian => "HypodorianScale",
140 Self::Hypophrygian => "HypophrygianScale",
141 Self::Hypolydian => "HypolydianScale",
142 Self::Hypomixolydian => "HypomixolydianScale",
143 Self::Hypolocrian => "HypolocrianScale",
144 Self::Hypoaeolian => "HypoaeolianScale",
145 Self::HarmonicMinor => "HarmonicMinorScale",
146 Self::MelodicMinor => "MelodicMinorScale",
147 Self::Chromatic => "ChromaticScale",
148 Self::WholeTone => "WholeToneScale",
149 Self::Octatonic => "OctatonicScale",
150 Self::RagAsawari => "RagAsawari",
151 Self::RagMarwa => "RagMarwa",
152 }
153 }
154
155 fn steps(self) -> &'static [&'static str] {
164 match self {
165 Self::Major => &["M2", "M2", "m2", "M2", "M2", "M2", "m2"],
166 Self::Minor | Self::Hypoaeolian => &["M2", "m2", "M2", "M2", "m2", "M2", "M2"],
167 Self::Dorian => &["M2", "m2", "M2", "M2", "M2", "m2", "M2"],
168 Self::Phrygian => &["m2", "M2", "M2", "M2", "m2", "M2", "M2"],
169 Self::Lydian => &["M2", "M2", "M2", "m2", "M2", "M2", "m2"],
170 Self::Mixolydian => &["M2", "M2", "m2", "M2", "M2", "m2", "M2"],
171 Self::Locrian => &["m2", "M2", "M2", "m2", "M2", "M2", "M2"],
172 Self::Hypodorian => &["M2", "m2", "M2", "M2", "M2", "m2", "M2"],
173 Self::Hypophrygian => &["m2", "M2", "M2", "M2", "m2", "M2", "M2"],
174 Self::Hypolydian => &["M2", "M2", "M2", "m2", "M2", "M2", "m2"],
175 Self::Hypomixolydian => &["M2", "M2", "m2", "M2", "M2", "m2", "M2"],
176 Self::Hypolocrian => &["m2", "M2", "M2", "m2", "M2", "M2", "M2"],
177 Self::HarmonicMinor => &["M2", "m2", "M2", "M2", "m2", "a2", "m2"],
178 Self::MelodicMinor => &["M2", "m2", "M2", "M2", "M2", "M2", "m2"],
179 Self::Chromatic => &["m2"; 12],
180 Self::WholeTone => &["M2"; 6],
181 Self::Octatonic => &["M2", "m2", "M2", "m2", "M2", "m2", "M2", "m2"],
182 Self::RagAsawari => &["M2", "m3", "M2", "m2", "M3"],
183 Self::RagMarwa => &["m2", "a2", "M2", "m3", "M2", "-M2", "m3"],
186 }
187 }
188
189 fn simplification(self) -> Simplification {
191 match self {
192 Self::WholeTone | Self::Octatonic | Self::RagMarwa => Simplification::MaxAccidental,
193 Self::Chromatic | Self::RagAsawari => Simplification::MostCommon,
194 _ => Simplification::Exact,
195 }
196 }
197
198 pub fn degree_count(self) -> usize {
200 self.steps().len()
201 }
202}
203
204#[derive(Clone, Debug, PartialEq)]
216#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
217pub struct Scale {
218 scale_type: ScaleType,
219 tonic: Pitch,
220}
221
222impl Scale {
223 pub fn new(scale_type: ScaleType, tonic: Pitch) -> Self {
225 Self { scale_type, tonic }
226 }
227
228 pub fn scale_type(&self) -> ScaleType {
230 self.scale_type
231 }
232
233 pub fn tonic(&self) -> &Pitch {
235 &self.tonic
236 }
237
238 pub fn pitches(&self) -> Result<Vec<Pitch>> {
243 let simplification = self.scale_type.simplification();
244 let mut pitches = Vec::with_capacity(self.scale_type.degree_count() + 1);
245 pitches.push(self.tonic.clone());
246
247 let mut current = self.tonic.clone();
248 for step in self.scale_type.steps() {
249 current = advance(¤t, step, simplification)?;
250 pitches.push(current.clone());
251 }
252 Ok(pitches)
253 }
254
255 pub fn pitch_at_degree(&self, degree: usize) -> Result<Pitch> {
260 if degree == 0 {
261 return Err(crate::error::Error::Ordinal(
262 "scale degree must be >= 1".to_string(),
263 ));
264 }
265
266 let simplification = self.scale_type.simplification();
267 let steps = self.scale_type.steps();
268 let mut current = self.tonic.clone();
269 for index in 0..(degree - 1) {
270 current = advance(¤t, steps[index % steps.len()], simplification)?;
271 }
272 Ok(current)
273 }
274}
275
276fn advance(pitch: &Pitch, step: &str, simplification: Simplification) -> Result<Pitch> {
278 let interval = step_interval(step);
279 match simplification {
280 Simplification::MaxAccidental => {
281 interval.transpose_pitch_with_options(pitch, false, Some(1))
282 }
283 Simplification::Exact => interval.transpose_pitch_with_options(pitch, false, None),
284 Simplification::MostCommon => {
285 let mut transposed = interval.transpose_pitch_with_options(pitch, false, None)?;
286 if transposed.accidental().alter() != 0.0 {
287 transposed.simplify_enharmonic_in_place(true)?;
288 }
289 Ok(transposed)
290 }
291 }
292}
293
294#[cfg(test)]
296fn max_alter(pitches: &[Pitch]) -> crate::defaults::IntegerType {
297 pitches
298 .iter()
299 .map(|pitch| pitch.accidental().alter().abs() as crate::defaults::IntegerType)
300 .max()
301 .unwrap_or(0)
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 fn names(scale_type: ScaleType, tonic: &str) -> Vec<String> {
309 Scale::new(scale_type, Pitch::from_name(tonic).expect("valid tonic"))
310 .pitches()
311 .expect("scale realizes")
312 .iter()
313 .map(|pitch| pitch.name())
314 .collect()
315 }
316
317 #[test]
318 fn realizes_the_church_modes_on_c() {
319 assert_eq!(
320 names(ScaleType::Major, "C4"),
321 ["C", "D", "E", "F", "G", "A", "B", "C"]
322 );
323 assert_eq!(
324 names(ScaleType::Minor, "C4"),
325 ["C", "D", "E-", "F", "G", "A-", "B-", "C"]
326 );
327 assert_eq!(
328 names(ScaleType::Dorian, "C4"),
329 ["C", "D", "E-", "F", "G", "A", "B-", "C"]
330 );
331 assert_eq!(
332 names(ScaleType::Phrygian, "C4"),
333 ["C", "D-", "E-", "F", "G", "A-", "B-", "C"]
334 );
335 assert_eq!(
336 names(ScaleType::Lydian, "C4"),
337 ["C", "D", "E", "F#", "G", "A", "B", "C"]
338 );
339 assert_eq!(
340 names(ScaleType::Mixolydian, "C4"),
341 ["C", "D", "E", "F", "G", "A", "B-", "C"]
342 );
343 assert_eq!(
344 names(ScaleType::Locrian, "C4"),
345 ["C", "D-", "E-", "F", "G-", "A-", "B-", "C"]
346 );
347 }
348
349 #[test]
350 fn realizes_the_altered_minors() {
351 assert_eq!(
352 names(ScaleType::HarmonicMinor, "C4"),
353 ["C", "D", "E-", "F", "G", "A-", "B", "C"]
354 );
355 assert_eq!(
356 names(ScaleType::MelodicMinor, "C4"),
357 ["C", "D", "E-", "F", "G", "A", "B", "C"]
358 );
359 }
360
361 #[test]
362 fn plagal_modes_share_their_authentic_pitches() {
363 for (plagal, authentic) in [
364 (ScaleType::Hypodorian, ScaleType::Dorian),
365 (ScaleType::Hypophrygian, ScaleType::Phrygian),
366 (ScaleType::Hypolydian, ScaleType::Lydian),
367 (ScaleType::Hypomixolydian, ScaleType::Mixolydian),
368 (ScaleType::Hypolocrian, ScaleType::Locrian),
369 (ScaleType::Hypoaeolian, ScaleType::Minor),
370 ] {
371 assert_eq!(
372 names(plagal, "C4"),
373 names(authentic, "C4"),
374 "{plagal:?} should share {authentic:?}'s pitch collection"
375 );
376 }
377 }
378
379 #[test]
380 fn whole_tone_spells_upward_until_accidentals_run_out() {
381 assert_eq!(
383 names(ScaleType::WholeTone, "C4"),
384 ["C", "D", "E", "F#", "G#", "A#", "B#"]
385 );
386 assert_eq!(
388 names(ScaleType::WholeTone, "B4"),
389 ["B", "C#", "D#", "E#", "G", "A", "B"]
390 );
391 }
392
393 #[test]
394 fn octatonic_alternates_tone_and_semitone() {
395 assert_eq!(
396 names(ScaleType::Octatonic, "C4"),
397 ["C", "D", "E-", "F", "G-", "A-", "A", "B", "C"]
398 );
399 assert_eq!(
400 names(ScaleType::Octatonic, "G4"),
401 ["G", "A", "B-", "C", "D-", "E-", "F-", "G-", "G"]
402 );
403 }
404
405 #[test]
406 fn chromatic_uses_the_most_common_spelling() {
407 assert_eq!(
408 names(ScaleType::Chromatic, "C4"),
409 [
410 "C", "C#", "D", "E-", "E", "F", "F#", "G", "A-", "A", "B-", "B", "C"
411 ]
412 );
413 }
414
415 #[test]
416 fn rag_asawari_is_pentatonic() {
417 assert_eq!(
418 names(ScaleType::RagAsawari, "C4"),
419 ["C", "D", "F", "G", "A-", "C"]
420 );
421 }
422
423 #[test]
424 fn rag_marwa_dips_below_its_sixth_degree() {
425 assert_eq!(
428 names(ScaleType::RagMarwa, "C4"),
429 ["C", "D-", "E", "F#", "A", "B", "A", "C"]
430 );
431 assert_eq!(
432 names(ScaleType::RagMarwa, "E-4"),
433 ["E-", "F-", "G", "A", "C", "D", "C", "E-"]
434 );
435 }
436
437 #[test]
438 fn simplifying_scales_never_exceed_their_accidental_budget() {
439 for scale_type in [ScaleType::WholeTone, ScaleType::Octatonic] {
440 for tonic in ["C4", "G4", "D4", "A4", "E4", "B4", "F#4", "E-4", "G-4"] {
441 let pitches = Scale::new(scale_type, Pitch::from_name(tonic).unwrap())
442 .pitches()
443 .unwrap();
444 assert!(
445 max_alter(&pitches) <= 1,
446 "{scale_type:?} on {tonic} exceeded one accidental"
447 );
448 }
449 }
450 }
451
452 #[test]
453 fn degree_lookup_matches_the_realized_pitches() {
454 for scale_type in ScaleType::ALL {
455 let scale = Scale::new(scale_type, Pitch::from_name("E-4").unwrap());
456 let pitches = scale.pitches().unwrap();
457 for (index, expected) in pitches.iter().enumerate() {
458 let actual = scale.pitch_at_degree(index + 1).unwrap();
459 assert_eq!(
460 actual.name_with_octave(),
461 expected.name_with_octave(),
462 "{scale_type:?} degree {}",
463 index + 1
464 );
465 }
466 }
467 }
468
469 #[test]
470 fn degree_zero_is_rejected() {
471 let scale = Scale::new(ScaleType::Major, Pitch::from_name("C4").unwrap());
472 assert!(scale.pitch_at_degree(0).is_err());
473 }
474
475 #[test]
476 fn every_scale_realizes_on_every_common_tonic() {
477 for scale_type in ScaleType::ALL {
478 for tonic in [
479 "C4", "G4", "D4", "A4", "E4", "B4", "F#4", "F4", "B-4", "E-4", "A-4",
480 ] {
481 let scale = Scale::new(scale_type, Pitch::from_name(tonic).unwrap());
482 let pitches = scale.pitches().expect("scale realizes");
483 assert_eq!(
484 pitches.len(),
485 scale_type.degree_count() + 1,
486 "{scale_type:?} on {tonic}"
487 );
488 }
489 }
490 }
491
492 #[test]
493 fn music21_names_are_distinct() {
494 let mut names: Vec<&str> = ScaleType::ALL.iter().map(|s| s.music21_name()).collect();
495 names.sort_unstable();
496 let count = names.len();
497 names.dedup();
498 assert_eq!(names.len(), count, "music21 names must be unique");
499 }
500}