1use crate::error::Result;
12use crate::interval::Interval;
13use crate::pitch::Pitch;
14
15use std::collections::HashMap;
16use std::sync::LazyLock;
17
18pub use super::realized::Scale;
19
20#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29pub enum Simplification {
30 Exact,
32 MaxAccidental,
34 MostCommon,
36}
37
38impl Simplification {
39 #[must_use]
42 pub fn music21_name(self) -> Option<&'static str> {
43 match self {
44 Self::Exact => None,
45 Self::MaxAccidental => Some("maxAccidental"),
46 Self::MostCommon => Some("mostCommon"),
47 }
48 }
49}
50
51static STEP_INTERVALS: LazyLock<HashMap<&'static str, Interval>> = LazyLock::new(|| {
57 ["m2", "M2", "a2", "m3", "M3", "-M2"]
58 .into_iter()
59 .map(|name| {
60 let interval =
61 Interval::from_name(name).expect("scale step intervals are valid interval names");
62 (name, interval)
63 })
64 .collect()
65});
66
67pub(super) fn step_interval(name: &str) -> Result<Interval> {
68 match STEP_INTERVALS.get(name) {
69 Some(interval) => Ok(interval.clone()),
70 None => Interval::from_name(name),
71 }
72}
73
74pub(super) fn close_on_tonic(steps: &[Interval]) -> Result<Vec<Interval>> {
81 let Some((last, leading)) = steps.split_last() else {
82 return Ok(Vec::new());
83 };
84 let octave = Pitch::from_name("C5")?;
85 let mut before_last = Pitch::from_name("C4")?;
86 for step in leading {
87 before_last = step.transpose_pitch_with_options(&before_last, false, Some(1))?;
88 }
89 let landed = last.transpose_pitch_with_options(&before_last, false, Some(4))?;
90 if landed.name_with_octave() == octave.name_with_octave() {
91 return Ok(steps.to_vec());
92 }
93 let mut closed = leading.to_vec();
94 closed.push(Interval::between_pitches(&before_last, &octave)?);
95 Ok(closed)
96}
97
98pub(super) fn cycle_within_octave(steps: &[Interval]) -> Result<Option<Vec<Interval>>> {
106 if steps.is_empty() || steps.iter().any(|step| step.semitones().fract() != 0.0) {
107 return Ok(None);
108 }
109 let span: crate::defaults::FloatType = steps.iter().map(Interval::semitones).sum();
110 if ![1.0, 2.0, 3.0, 4.0, 6.0, 12.0].contains(&span) {
111 return Ok(None);
112 }
113 let repeats = (12.0 / span) as usize;
114 let filled: Vec<Interval> = steps
115 .iter()
116 .cycle()
117 .take(steps.len() * repeats)
118 .cloned()
119 .collect();
120 close_on_tonic(&filled).map(Some)
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
125#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
126pub enum SolfegVariant {
127 Music21,
129 Humdrum,
131}
132
133pub const SOLFEG_SYLLABLES: [[&str; 5]; 7] = [
136 ["def", "de", "do", "di", "dis"],
137 ["raf", "ra", "re", "ri", "ris"],
138 ["mef", "me", "mi", "mis", "mish"],
139 ["fef", "fe", "fa", "fi", "fis"],
140 ["sef", "se", "sol", "si", "sis"],
141 ["lef", "le", "la", "li", "lis"],
142 ["tef", "te", "ti", "tis", "tish"],
143];
144
145pub const HUMDRUM_SOLFEG_SYLLABLES: [[&str; 5]; 7] = [
147 ["def", "de", "do", "di", "dis"],
148 ["raf", "ra", "re", "ri", "ris"],
149 ["mef", "me", "mi", "my", "mish"],
150 ["fef", "fe", "fa", "fi", "fis"],
151 ["sef", "se", "so", "si", "sis"],
152 ["lef", "le", "la", "li", "lis"],
153 ["tef", "te", "ti", "ty", "tish"],
154];
155
156#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
162#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
163#[must_use]
164#[non_exhaustive]
165pub enum ScaleType {
166 Major,
168 Minor,
170 Dorian,
172 Phrygian,
174 Lydian,
176 Mixolydian,
178 Locrian,
180 Hypodorian,
182 Hypophrygian,
184 Hypolydian,
186 Hypomixolydian,
188 Hypolocrian,
190 Hypoaeolian,
192 HarmonicMinor,
194 MelodicMinor,
196 Chromatic,
198 WholeTone,
200 Octatonic,
202 RagAsawari,
204 RagMarwa,
210}
211
212impl ScaleType {
213 pub const ALL: [ScaleType; 20] = [
215 Self::Major,
216 Self::Minor,
217 Self::Dorian,
218 Self::Phrygian,
219 Self::Lydian,
220 Self::Mixolydian,
221 Self::Locrian,
222 Self::Hypodorian,
223 Self::Hypophrygian,
224 Self::Hypolydian,
225 Self::Hypomixolydian,
226 Self::Hypolocrian,
227 Self::Hypoaeolian,
228 Self::HarmonicMinor,
229 Self::MelodicMinor,
230 Self::Chromatic,
231 Self::WholeTone,
232 Self::Octatonic,
233 Self::RagAsawari,
234 Self::RagMarwa,
235 ];
236
237 pub fn music21_name(self) -> &'static str {
239 match self {
240 Self::Major => "MajorScale",
241 Self::Minor => "MinorScale",
242 Self::Dorian => "DorianScale",
243 Self::Phrygian => "PhrygianScale",
244 Self::Lydian => "LydianScale",
245 Self::Mixolydian => "MixolydianScale",
246 Self::Locrian => "LocrianScale",
247 Self::Hypodorian => "HypodorianScale",
248 Self::Hypophrygian => "HypophrygianScale",
249 Self::Hypolydian => "HypolydianScale",
250 Self::Hypomixolydian => "HypomixolydianScale",
251 Self::Hypolocrian => "HypolocrianScale",
252 Self::Hypoaeolian => "HypoaeolianScale",
253 Self::HarmonicMinor => "HarmonicMinorScale",
254 Self::MelodicMinor => "MelodicMinorScale",
255 Self::Chromatic => "ChromaticScale",
256 Self::WholeTone => "WholeToneScale",
257 Self::Octatonic => "OctatonicScale",
258 Self::RagAsawari => "RagAsawari",
259 Self::RagMarwa => "RagMarwa",
260 }
261 }
262
263 pub fn music21_descriptive_name(self) -> &'static str {
270 match self {
271 Self::Major => "major",
272 Self::Minor => "minor",
273 Self::Dorian => "dorian",
274 Self::Phrygian => "phrygian",
275 Self::Lydian => "lydian",
276 Self::Mixolydian => "mixolydian",
277 Self::Locrian => "locrian",
278 Self::Hypodorian => "hypodorian",
279 Self::Hypophrygian => "hypophrygian",
280 Self::Hypolydian => "hypolydian",
281 Self::Hypomixolydian => "hypomixolydian",
282 Self::Hypolocrian => "hypolocrian",
283 Self::Hypoaeolian => "hypoaeolian",
284 Self::HarmonicMinor => "harmonic minor",
285 Self::MelodicMinor => "melodic minor",
286 Self::Chromatic => "Chromatic",
287 Self::WholeTone => "Whole tone",
288 Self::Octatonic => "Octatonic",
289 Self::RagAsawari => "Rag Asawari",
290 Self::RagMarwa => "Rag Marwa",
291 }
292 }
293
294 pub fn from_music21_name(name: &str) -> Option<Self> {
297 Self::ALL
298 .into_iter()
299 .find(|scale_type| scale_type.music21_name() == name)
300 }
301
302 pub fn tonic_degree(self) -> usize {
310 if self.is_plagal() { 4 } else { 1 }
311 }
312
313 pub fn dominant_degree(self) -> usize {
319 match self {
320 Self::Hypophrygian | Self::Hypomixolydian => 7,
321 _ if self.is_plagal() => 6,
322 _ => 5,
323 }
324 }
325
326 pub fn realization_steps(self) -> Vec<&'static str> {
335 let steps = self.steps();
336 let count = steps.len();
337 let start = (count + 1 - self.tonic_degree()) % count;
338 steps[start..]
339 .iter()
340 .chain(&steps[..start])
341 .copied()
342 .collect()
343 }
344
345 pub fn beyond_terminus(self) -> Option<&'static str> {
353 match self {
354 Self::RagMarwa => Some("m2"),
355 _ => None,
356 }
357 }
358
359 pub fn descending_form(self) -> Option<Self> {
368 match self {
369 Self::MelodicMinor | Self::RagAsawari => Some(Self::Minor),
370 _ => None,
371 }
372 }
373
374 pub fn ascending_degrees(self) -> Option<&'static [u8]> {
382 match self {
383 Self::RagAsawari => Some(&[1, 2, 4, 5, 6]),
384 _ => None,
385 }
386 }
387
388 pub fn descending_steps(self) -> Option<&'static [&'static str]> {
400 match self {
401 Self::RagMarwa => Some(&["m2", "A2", "M2", "m3", "M2", "d3", "-m2"]),
402 _ => None,
403 }
404 }
405
406 pub fn is_plagal(self) -> bool {
408 matches!(
409 self,
410 Self::Hypodorian
411 | Self::Hypophrygian
412 | Self::Hypolydian
413 | Self::Hypomixolydian
414 | Self::Hypolocrian
415 | Self::Hypoaeolian
416 )
417 }
418
419 pub(super) fn steps(self) -> &'static [&'static str] {
428 match self {
429 Self::Major => &["M2", "M2", "m2", "M2", "M2", "M2", "m2"],
430 Self::Minor | Self::Hypoaeolian => &["M2", "m2", "M2", "M2", "m2", "M2", "M2"],
431 Self::Dorian => &["M2", "m2", "M2", "M2", "M2", "m2", "M2"],
432 Self::Phrygian => &["m2", "M2", "M2", "M2", "m2", "M2", "M2"],
433 Self::Lydian => &["M2", "M2", "M2", "m2", "M2", "M2", "m2"],
434 Self::Mixolydian => &["M2", "M2", "m2", "M2", "M2", "m2", "M2"],
435 Self::Locrian => &["m2", "M2", "M2", "m2", "M2", "M2", "M2"],
436 Self::Hypodorian => &["M2", "m2", "M2", "M2", "M2", "m2", "M2"],
437 Self::Hypophrygian => &["m2", "M2", "M2", "M2", "m2", "M2", "M2"],
438 Self::Hypolydian => &["M2", "M2", "M2", "m2", "M2", "M2", "m2"],
439 Self::Hypomixolydian => &["M2", "M2", "m2", "M2", "M2", "m2", "M2"],
440 Self::Hypolocrian => &["m2", "M2", "M2", "m2", "M2", "M2", "M2"],
441 Self::HarmonicMinor => &["M2", "m2", "M2", "M2", "m2", "a2", "m2"],
442 Self::MelodicMinor => &["M2", "m2", "M2", "M2", "M2", "M2", "m2"],
443 Self::Chromatic => &[
447 "m2", "m2", "m2", "m2", "m2", "m2", "m2", "m2", "m2", "m2", "m2", "a1",
448 ],
449 Self::WholeTone => &["M2", "M2", "M2", "M2", "M2", "d3"],
450 Self::Octatonic => &["M2", "m2", "M2", "m2", "M2", "m2", "M2", "m2"],
451 Self::RagAsawari => &["M2", "m3", "M2", "m2", "M3"],
452 Self::RagMarwa => &["m2", "a2", "M2", "m3", "M2", "-M2", "m3"],
455 }
456 }
457
458 pub(super) fn simplification(self) -> Simplification {
460 match self {
461 Self::WholeTone | Self::Octatonic | Self::RagMarwa => Simplification::MaxAccidental,
462 Self::Chromatic | Self::RagAsawari => Simplification::MostCommon,
463 _ => Simplification::Exact,
464 }
465 }
466
467 pub fn degree_count(self) -> usize {
469 self.steps().len()
470 }
471}
472
473pub(super) const SCALE_STARTS: [&str; 15] = [
476 "C", "C#", "Db", "D", "D#", "Eb", "E", "F", "F#", "G", "G#", "A", "Bb", "B", "Cb",
477];
478
479impl ScaleType {
480 pub fn derive_ranked(
489 self,
490 pitches: &[Pitch],
491 limit: Option<usize>,
492 ) -> Result<Vec<(usize, Scale)>> {
493 self.derive_ranked_by(pitches, limit, DegreeComparison::PitchClass)
494 }
495
496 pub fn derive_ranked_by(
502 self,
503 pitches: &[Pitch],
504 limit: Option<usize>,
505 comparison: DegreeComparison,
506 ) -> Result<Vec<(usize, Scale)>> {
507 rank_on_every_start(pitches, limit, comparison, |tonic| Scale::new(self, tonic))
508 }
509
510 pub fn derive(self, pitches: &[Pitch]) -> Result<Scale> {
512 self.derive_ranked(pitches, Some(1))?
513 .pop()
514 .map(|(_, scale)| scale)
515 .ok_or_else(|| crate::error::Error::Scale("no candidate tonics".to_string()))
516 }
517
518 pub fn derive_all(self, pitches: &[Pitch]) -> Result<Vec<Scale>> {
521 Ok(self
522 .derive_ranked(pitches, None)?
523 .into_iter()
524 .filter(|(matched, _)| *matched == pitches.len())
525 .map(|(_, scale)| scale)
526 .collect())
527 }
528}
529
530pub(super) fn rank_on_every_start(
534 pitches: &[Pitch],
535 limit: Option<usize>,
536 comparison: DegreeComparison,
537 place: impl Fn(Pitch) -> Scale,
538) -> Result<Vec<(usize, Scale)>> {
539 let targets = pitches
540 .iter()
541 .map(|pitch| comparison.key(pitch))
542 .collect::<Vec<_>>();
543 let Some((lowest, highest)) = target_range(pitches) else {
544 return Ok(Vec::new());
545 };
546 let mut ranked = Vec::with_capacity(SCALE_STARTS.len());
547 for start in SCALE_STARTS {
548 let scale = place(Pitch::from_name(start)?);
549 let degrees = scale
550 .pitches_between(lowest, highest)?
551 .iter()
552 .map(|pitch| comparison.key(pitch))
553 .collect::<Vec<_>>();
554 let matched = targets
555 .iter()
556 .filter(|target| degrees.contains(target))
557 .count();
558 ranked.push((matched, scale));
559 }
560 ranked.sort_by(|left, right| {
561 left.0
562 .cmp(&right.0)
563 .then_with(|| left.1.tonic().ps().total_cmp(&right.1.tonic().ps()))
564 });
565 ranked.reverse();
566 if let Some(limit) = limit {
567 ranked.truncate(limit);
568 }
569 Ok(ranked)
570}
571
572pub(super) fn target_range(pitches: &[Pitch]) -> Option<(&Pitch, &Pitch)> {
575 let lowest = pitches.iter().min_by(|a, b| a.ps().total_cmp(&b.ps()))?;
576 let highest = pitches.iter().max_by(|a, b| a.ps().total_cmp(&b.ps()))?;
577 Some((lowest, highest))
578}
579
580pub(super) const MAX_RANGE_OCTAVES: usize = 12;
583
584#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
587#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
588pub enum DegreeComparison {
589 PitchClass,
591 Name,
593 Step,
595}
596
597impl DegreeComparison {
598 pub(super) fn key(self, pitch: &Pitch) -> String {
600 match self {
601 Self::PitchClass => pitch.pitch_class().number().to_string(),
602 Self::Name => pitch.name(),
603 Self::Step => pitch.name().chars().take(1).collect(),
604 }
605 }
606}
607
608pub(super) fn advance(
610 pitch: &Pitch,
611 interval: &Interval,
612 simplification: Simplification,
613) -> Result<Pitch> {
614 match simplification {
615 Simplification::MaxAccidental => {
616 interval.transpose_pitch_with_options(pitch, false, Some(1))
617 }
618 Simplification::Exact => interval.transpose_pitch_with_options(pitch, false, Some(4)),
621 Simplification::MostCommon => {
622 let mut transposed = interval.transpose_pitch_with_options(pitch, false, Some(4))?;
623 if transposed.accidental().alter() != 0.0 {
624 transposed.simplify_enharmonic_in_place(true)?;
625 }
626 Ok(transposed)
627 }
628 }
629}
630
631#[cfg(test)]
633fn max_alter(pitches: &[Pitch]) -> crate::defaults::IntegerType {
634 pitches
635 .iter()
636 .map(|pitch| pitch.accidental().alter().abs() as crate::defaults::IntegerType)
637 .max()
638 .unwrap_or(0)
639}
640
641#[cfg(test)]
642mod tests {
643 use crate::defaults::{FloatType, IntegerType};
644 #[test]
645 fn a_scala_scale_is_a_cycle_walked_from_its_tonic() {
646 use super::Scale;
647 use crate::Pitch;
648 use crate::tuningsystem::scala::ScalaScale;
649
650 let slendro = ScalaScale::parse(
655 "! slendro.scl
656!
657Observed Javanese Slendro scale, Helmholtz/Ellis p. 518, nr.94
658 5
659!
660 228.00000
661 484.00000
662 728.00000
663 960.00000
664 2/1
665",
666 )
667 .unwrap();
668 let scale = Scale::from_scala(Pitch::from_name("C4").unwrap(), &slendro).unwrap();
669 let spelled: Vec<String> = scale
670 .pitches_between(
671 &Pitch::from_name("C3").unwrap(),
672 &Pitch::from_name("C5").unwrap(),
673 )
674 .unwrap()
675 .iter()
676 .map(|pitch| match pitch.microtone() {
677 Some(microtone) if microtone.cents() != 0.0 => {
678 format!("{}{microtone}", pitch.name_with_octave())
679 }
680 _ => pitch.name_with_octave(),
681 })
682 .collect();
683 assert_eq!(
684 spelled,
685 [
686 "C3",
687 "D~3(-22c)",
688 "F3(-16c)",
689 "G~3(-22c)",
690 "Bb3(-40c)",
691 "C4",
692 "D~4(-22c)",
693 "F4(-16c)",
694 "G~4(-22c)",
695 "A~4(+10c)",
696 "C5"
697 ]
698 );
699 assert!(!scale.octave_duplicating());
700 }
701
702 #[test]
703 fn degrees_are_found_by_pitch_class_name_or_step() {
704 use super::{DegreeComparison, Scale, ScaleType};
705 use crate::Pitch;
706
707 let major = Scale::new(ScaleType::Major, Pitch::from_name("C").unwrap());
708 let e_sharp = Pitch::from_name("E#").unwrap();
709 assert_eq!(
710 major
711 .degree_of_by(&e_sharp, DegreeComparison::PitchClass)
712 .unwrap(),
713 Some(4)
714 );
715 assert_eq!(
716 major
717 .degree_of_by(&e_sharp, DegreeComparison::Name)
718 .unwrap(),
719 None
720 );
721 assert_eq!(
722 major
723 .degree_of_by(&e_sharp, DegreeComparison::Step)
724 .unwrap(),
725 Some(3)
726 );
727 let e_flat = Pitch::from_name("Eb").unwrap();
728 let (degree, accidental) = major.degree_and_accidental_of(&e_flat).unwrap();
729 assert_eq!(degree, 3);
730 assert_eq!(accidental.as_ref().map(|a| a.name()), Some("flat"));
731 let whole_tone = Scale::new(ScaleType::WholeTone, Pitch::from_name("C").unwrap());
732 assert!(
733 whole_tone
734 .degree_and_accidental_of(&Pitch::from_name("B").unwrap())
735 .is_err()
736 );
737
738 let marwa = Scale::new(ScaleType::RagMarwa, Pitch::from_name("C4").unwrap());
740 assert_eq!(
741 marwa
742 .degrees_of_by(&Pitch::from_name("A").unwrap(), DegreeComparison::Name)
743 .unwrap(),
744 [5, 7]
745 );
746 }
747
748 #[test]
749 fn a_scale_moves_to_its_relatives_parallels_and_a_new_tonic() {
750 use super::{Scale, ScaleType};
751 use crate::Pitch;
752
753 let tonic = |scale: &Scale| scale.tonic().name();
754 let c_minor = Scale::new(ScaleType::Minor, Pitch::from_name("C").unwrap());
755 assert_eq!(tonic(&c_minor.parallel_major()), "C");
756 assert_eq!(c_minor.parallel_major().scale_type(), ScaleType::Major);
757 assert_eq!(tonic(&c_minor.relative_major().unwrap()), "Eb");
758 let c_major = Scale::new(ScaleType::Major, Pitch::from_name("C").unwrap());
759 assert_eq!(tonic(&c_major.relative_minor().unwrap()), "A");
760 assert_eq!(c_major.parallel_minor().scale_type(), ScaleType::Minor);
761 let mut moved = c_major.clone();
762 moved.set_tonic(Pitch::from_name("D").unwrap());
763 assert_eq!(tonic(&moved), "D");
764 assert_eq!(moved.pitches().unwrap()[1].name(), "E");
765 assert!(c_major.octave_duplicating());
766 assert!(c_major.is_realizable());
767 assert_eq!(
768 ScaleType::from_music21_name("MajorScale"),
769 Some(ScaleType::Major)
770 );
771 assert_eq!(ScaleType::from_music21_name("NoSuchScale"), None);
772 }
773
774 #[test]
775 fn a_range_is_walked_from_where_music21_starts_it() {
776 use super::Scale;
778 use crate::Pitch;
779 let pitch = |name: &str| Pitch::from_name(name).unwrap();
780 let named = |pitches: Vec<Pitch>| -> Vec<String> {
781 pitches.iter().map(Pitch::name_with_octave).collect()
782 };
783 let between = |scale_type: ScaleType, tonic: &str, low: &str, high: &str| {
784 named(
785 Scale::new(scale_type, pitch(tonic))
786 .pitches_between(&pitch(low), &pitch(high))
787 .unwrap(),
788 )
789 };
790 let coming_down = |scale_type: ScaleType, tonic: &str| {
791 named(
792 Scale::new(scale_type, pitch(tonic))
793 .pitches_descending()
794 .unwrap(),
795 )
796 };
797
798 assert_eq!(
801 between(ScaleType::WholeTone, "Bb4", "C#4", "G#4"),
802 ["D4", "E4", "F#4", "G#4"]
803 );
804 assert_eq!(
807 between(ScaleType::Octatonic, "G#2", "C#4", "G#4"),
808 ["C#4", "D4", "E4", "F4", "G4", "G#4"]
809 );
810 assert_eq!(
813 between(ScaleType::RagMarwa, "Bb3", "C#4", "G#4"),
814 ["D4", "E4", "G4"]
815 );
816 assert_eq!(
818 coming_down(ScaleType::RagAsawari, "Bb3"),
819 ["Bb4", "Ab4", "F#4", "F4", "Eb4", "C#4", "C4", "Bb3"]
820 );
821 assert_eq!(
822 coming_down(ScaleType::RagMarwa, "C4"),
823 ["Db5", "C5", "Db5", "B4", "A4", "F#4", "E4", "Db4", "C4"]
824 );
825 }
826
827 #[test]
828 fn a_scale_is_derived_from_where_the_pitches_are() {
829 use super::DegreeComparison;
830 use crate::Pitch;
831 let pitches: Vec<Pitch> = ["F#", "A", "C#", "E#"]
835 .iter()
836 .map(|name| Pitch::from_name(*name).unwrap())
837 .collect();
838 let ranked: Vec<(usize, String)> = ScaleType::WholeTone
839 .derive_ranked_by(&pitches, Some(2), DegreeComparison::Name)
840 .unwrap()
841 .into_iter()
842 .map(|(matched, scale)| (matched, scale.tonic().name()))
843 .collect();
844 assert_eq!(ranked, [(3, "B".to_string()), (3, "A".to_string())]);
845 }
846
847 #[test]
848 fn a_scale_walks_down_as_well_as_up() {
849 use super::{Scale, ScaleType};
850 use crate::Pitch;
851
852 let names = |pitches: Vec<Pitch>| -> Vec<String> {
853 pitches.iter().map(Pitch::name_with_octave).collect()
854 };
855 let major = Scale::new(ScaleType::Major, Pitch::from_name("C4").unwrap());
856 assert_eq!(
857 names(major.pitches_descending().unwrap()),
858 ["C5", "B4", "A4", "G4", "F4", "E4", "D4", "C4"]
859 );
860 let low = Pitch::from_name("C3").unwrap();
861 let high = Pitch::from_name("C5").unwrap();
862 assert_eq!(
863 names(
864 major
865 .pitches_between_descending(&Pitch::from_name("G4").unwrap(), &high)
866 .unwrap()
867 ),
868 ["C5", "B4", "A4", "G4"]
869 );
870 assert_eq!(
871 names(
872 major
873 .pitches_from_scale_degrees_between(&[1, 5], &low, &high)
874 .unwrap()
875 ),
876 ["C3", "G3", "C4", "G4", "C5"]
877 );
878
879 let c_sharp = Pitch::from_name("C#4").unwrap();
882 assert_eq!(
883 major
884 .next_pitch_below(&c_sharp, 1)
885 .unwrap()
886 .name_with_octave(),
887 "C4"
888 );
889 assert_eq!(
890 major
891 .next_pitch_beside(&c_sharp, -1, true)
892 .unwrap()
893 .name_with_octave(),
894 "B3"
895 );
896 assert_eq!(
897 major
898 .next_pitch_beside(&c_sharp, -1, false)
899 .unwrap()
900 .name_with_octave(),
901 "C4"
902 );
903 assert_eq!(
904 major
905 .next_pitch_beside(&c_sharp, 1, false)
906 .unwrap()
907 .name_with_octave(),
908 "E4"
909 );
910
911 let marwa = Scale::new(ScaleType::RagMarwa, Pitch::from_name("C4").unwrap());
915 let d_flat = Pitch::from_name("D-4").unwrap();
916 assert_eq!(marwa.places_of(&d_flat).unwrap(), 1);
917 assert_eq!(
918 marwa
919 .next_pitch_above_from(&d_flat, 1, 0)
920 .unwrap()
921 .name_with_octave(),
922 "E4"
923 );
924 assert_eq!(
925 marwa
926 .next_pitch_below_from(&Pitch::from_name("C5").unwrap(), 1, 0)
927 .unwrap()
928 .name_with_octave(),
929 "A4"
930 );
931 }
932
933 #[test]
934 fn a_custom_scale_ranks_the_tonics_that_fit_a_set_of_pitches() {
935 use super::{DegreeComparison, Scale};
936 use crate::Pitch;
937
938 let pitches: Vec<Pitch> = ["C4", "D4", "E4", "F4", "G4", "A4", "B4", "C5"]
939 .iter()
940 .map(|name| Pitch::from_name(*name).unwrap())
941 .collect();
942 let custom = Scale::from_pitches(&pitches).unwrap();
943 assert!(custom.is_custom());
944 let targets: Vec<Pitch> = ["G4", "B4", "D5"]
945 .iter()
946 .map(|name| Pitch::from_name(*name).unwrap())
947 .collect();
948 let ranked = custom
949 .derive_ranked_by(&targets, Some(2), DegreeComparison::PitchClass)
950 .unwrap();
951 assert_eq!(ranked.len(), 2);
952 assert_eq!(ranked[0].0, 3);
953 assert!(ranked[0].1.is_custom());
954 }
955
956 #[test]
960 fn a_stream_is_tuned_onto_a_scale() {
961 use super::Scale;
962 use crate::stream::StreamElement;
963 use crate::{Chord, Note, Pitch, Stream};
964
965 let tuned: Vec<Pitch> = [
966 ("C4", 0.0),
967 ("D4", 20.0),
968 ("E4", 0.0),
969 ("F4", 0.0),
970 ("G4", 0.0),
971 ("A4", -15.0),
972 ("B4", 0.0),
973 ("C5", 0.0),
974 ]
975 .iter()
976 .map(|(name, cents)| {
977 let mut pitch = Pitch::from_name(*name).unwrap();
978 pitch.set_microtone_cents(*cents).unwrap();
979 pitch
980 })
981 .collect();
982 let scale = Scale::from_pitches(&tuned).unwrap();
983 let mut stream = Stream::new();
984 for name in ["D5", "F#4", "A3", "B#4", "Gb5"] {
985 stream.push(Note::from_pitch(Pitch::from_name(name).unwrap()));
986 }
987 stream.push(Chord::new("C5 E5 G5").unwrap());
988 scale.tune(&mut stream).unwrap();
989 let sounding: Vec<(String, FloatType)> = stream
990 .events()
991 .iter()
992 .flat_map(|event| match event.element() {
993 StreamElement::Note(note) => {
994 vec![(note.pitch().name_with_octave(), note.pitch().ps())]
995 }
996 StreamElement::Chord(chord) => chord
997 .pitches()
998 .iter()
999 .map(|pitch| (pitch.name_with_octave(), pitch.ps()))
1000 .collect(),
1001 _ => Vec::new(),
1002 })
1003 .collect();
1004 let expected: Vec<(String, FloatType)> = [
1005 ("D5", 74.2),
1006 ("F#4", 66.0),
1007 ("A3", 56.85),
1008 ("B#4", 72.0),
1009 ("Gb5", 78.0),
1010 ("C5", 72.0),
1011 ("E5", 76.0),
1012 ("G5", 79.0),
1013 ]
1014 .iter()
1015 .map(|(name, ps)| (name.to_string(), *ps))
1016 .collect();
1017 assert_eq!(sounding.len(), expected.len());
1018 for ((name, ps), (wanted_name, wanted_ps)) in sounding.iter().zip(&expected) {
1019 assert_eq!(name, wanted_name);
1020 assert!(
1021 (ps - wanted_ps).abs() < 1e-9,
1022 "{name} sounds at {ps}, not {wanted_ps}"
1023 );
1024 }
1025 }
1026
1027 #[test]
1030 fn a_scale_carries_numerals_and_writes_itself_as_scala() {
1031 use super::{Scale, ScaleType};
1032 use crate::Pitch;
1033 use crate::tuningsystem::scala::ScalaDegree;
1034
1035 let scale = Scale::new(ScaleType::Major, Pitch::from_name("Ab4").unwrap());
1036 let tonic = scale.roman_numeral(1).unwrap();
1037 assert_eq!(tonic.to_chord().unwrap().root().unwrap().to_string(), "Ab4");
1038 let dominant = scale.roman_numeral(5).unwrap();
1039 assert_eq!(
1040 dominant.to_chord().unwrap().root().unwrap().to_string(),
1041 "Eb5"
1042 );
1043 assert_eq!(dominant.figure_and_key(), "V in Ab major");
1044 assert!(scale.roman_numeral(8).is_err());
1045
1046 let scala = scale.scala_data().unwrap();
1047 assert_eq!(scala.len(), 7);
1048 assert!(matches!(scala.degrees()[0], ScalaDegree::Ratio(_)));
1049 let cents: Vec<i64> = scala
1050 .degrees()
1051 .iter()
1052 .map(|degree| degree.cents().round() as i64)
1053 .collect();
1054 assert_eq!(cents, [0, 200, 400, 500, 700, 900, 1100]);
1055 assert_eq!(scala.period().cents().round() as i64, 1200);
1056 assert_eq!(scala.description(), "Ab major");
1057 }
1058
1059 #[test]
1060 fn a_scale_can_be_given_by_its_notes() {
1061 let given: Vec<Pitch> = ["C4", "Eb4", "Gb4", "A4"]
1064 .iter()
1065 .map(|name| Pitch::from_name(*name).expect("valid pitch"))
1066 .collect();
1067 let scale = Scale::from_pitches(&given).expect("a scale");
1068 assert!(scale.is_custom());
1069 assert_eq!(scale.degree_count(), 4);
1070 let realized: Vec<String> = scale
1071 .pitches()
1072 .expect("realizes")
1073 .iter()
1074 .map(Pitch::name_with_octave)
1075 .collect();
1076 assert_eq!(realized, ["C4", "Eb4", "Gb4", "A4", "C5"]);
1077 assert_eq!(scale.pitch_at_degree(4).unwrap().name_with_octave(), "A4");
1078 assert_eq!(scale.pitch_at_degree(5).unwrap().name_with_octave(), "C4");
1082 assert_eq!(
1083 scale.degree_of(&Pitch::from_name("G-").unwrap()).unwrap(),
1084 Some(3)
1085 );
1086
1087 let range: Vec<String> = scale
1089 .pitches_between(
1090 &Pitch::from_name("Eb5").unwrap(),
1091 &Pitch::from_name("C6").unwrap(),
1092 )
1093 .expect("a range")
1094 .iter()
1095 .map(Pitch::name_with_octave)
1096 .collect();
1097 assert_eq!(range, ["Eb5", "Gb5", "A5", "C6"]);
1098
1099 assert!(Scale::from_pitches(&[]).is_err());
1100 }
1101 #[test]
1102 fn a_range_starts_at_the_first_scale_pitch_inside_it() {
1103 let scale = Scale::new(ScaleType::Major, Pitch::from_name("C").unwrap());
1106 let range = scale
1107 .pitches_between(
1108 &Pitch::from_name("E-5").unwrap(),
1109 &Pitch::from_name("G-7").unwrap(),
1110 )
1111 .unwrap();
1112 let names: Vec<String> = range.iter().map(Pitch::name_with_octave).take(4).collect();
1113 assert_eq!(names, ["E5", "F5", "G5", "A5"]);
1114 assert_eq!(range.last().unwrap().name_with_octave(), "F7");
1115
1116 let octave = scale
1118 .pitches_between(
1119 &Pitch::from_name("C3").unwrap(),
1120 &Pitch::from_name("C4").unwrap(),
1121 )
1122 .unwrap();
1123 let names: Vec<String> = octave.iter().map(Pitch::name_with_octave).collect();
1124 assert_eq!(names, ["C3", "D3", "E3", "F3", "G3", "A3", "B3", "C4"]);
1125
1126 let descending: Vec<String> = scale
1129 .pitches_between(
1130 &Pitch::from_name("C5").unwrap(),
1131 &Pitch::from_name("C4").unwrap(),
1132 )
1133 .unwrap()
1134 .iter()
1135 .map(Pitch::name_with_octave)
1136 .collect();
1137 assert_eq!(descending, ["C5", "B4", "A4", "G4", "F4", "E4", "D4", "C4"]);
1138 }
1139
1140 #[test]
1141 fn scale_helpers_match_music21() {
1142 let c_major = Scale::new(ScaleType::Major, Pitch::from_name("C4").unwrap());
1143 let pitch = |name: &str| Pitch::from_name(name).unwrap();
1144 let names = |pitches: &[Pitch]| -> Vec<String> {
1145 pitches.iter().map(Pitch::name_with_octave).collect()
1146 };
1147
1148 assert_eq!(c_major.degree_count(), 7);
1149 assert_eq!(
1150 Scale::new(ScaleType::Chromatic, pitch("C4")).degree_count(),
1151 12
1152 );
1153 assert_eq!(
1154 c_major
1155 .transpose(&Interval::from_name("P5").unwrap())
1156 .unwrap()
1157 .tonic()
1158 .name_with_octave(),
1159 "G4"
1160 );
1161 assert_eq!(
1162 names(&c_major.chord().unwrap().pitches()),
1163 ["C4", "D4", "E4", "F4", "G4", "A4", "B4", "C5"]
1164 );
1165 assert_eq!(
1166 names(
1167 &c_major
1168 .pitches_from_scale_degrees(&[1, 3, 5, 8, 10])
1169 .unwrap()
1170 ),
1171 ["C4", "E4", "G4", "C5"]
1172 );
1173 assert_eq!(
1174 c_major.interval_between_degrees(1, 5).unwrap().short_name(),
1175 "P5"
1176 );
1177 assert_eq!(
1178 c_major.interval_between_degrees(3, 7).unwrap().short_name(),
1179 "P5"
1180 );
1181 assert_eq!(
1182 c_major
1183 .interval_between_degrees(5, 2)
1184 .unwrap()
1185 .directed_name(),
1186 "P-4"
1187 );
1188 assert_eq!(
1189 c_major.interval_between_degrees(2, 9).unwrap().short_name(),
1190 "P1"
1191 );
1192
1193 assert!(c_major.is_next(&pitch("D4"), &pitch("C4"), 1).unwrap());
1194 assert!(c_major.is_next(&pitch("D5"), &pitch("C4"), 1).unwrap());
1195 assert!(!c_major.is_next(&pitch("E4"), &pitch("C4"), 1).unwrap());
1196 assert!(c_major.is_next(&pitch("E4"), &pitch("C4"), 2).unwrap());
1197
1198 let (matched, unmatched) = c_major
1199 .match_pitches(&["C4", "E4", "Gb4", "Bb5", "A"].map(pitch))
1200 .unwrap();
1201 assert_eq!(names(&matched), ["C4", "E4", "A4"]);
1202 assert_eq!(names(&unmatched), ["Gb4", "Bb5"]);
1203
1204 assert_eq!(
1205 names(
1206 &c_major
1207 .find_missing(&["C4", "E4", "G4"].map(pitch))
1208 .unwrap()
1209 ),
1210 ["D4", "F4", "A4", "B4"]
1211 );
1212 let a_minor = Scale::new(ScaleType::Minor, pitch("A"));
1213 assert_eq!(
1214 names(
1215 &a_minor
1216 .find_missing(&["A", "B", "C", "E"].map(pitch))
1217 .unwrap()
1218 ),
1219 ["D5", "F5", "G5"]
1220 );
1221 assert!(
1222 c_major
1223 .find_missing(&["C4", "D4", "E4", "F4", "G4", "A4", "B4"].map(pitch))
1224 .unwrap()
1225 .is_empty()
1226 );
1227 }
1228
1229 #[test]
1230 fn solfeg_matches_music21() {
1231 let c_major = Scale::new(ScaleType::Major, Pitch::from_name("C4").unwrap());
1232 let cases = [
1233 ("C4", "do", "do", "do"),
1234 ("C#4", "di", "di", "do"),
1235 ("D-4", "ra", "ra", "re"),
1236 ("E-4", "me", "me", "mi"),
1237 ("F#4", "fi", "fi", "fa"),
1238 ("G-4", "se", "se", "sol"),
1239 ("G4", "sol", "so", "sol"),
1240 ("A-4", "le", "le", "la"),
1241 ("B-4", "te", "te", "ti"),
1242 ("C-4", "de", "de", "do"),
1243 ("B#4", "tis", "ty", "ti"),
1244 ("F##4", "fis", "fis", "fa"),
1245 ("E#4", "mis", "my", "mi"),
1246 ("D##4", "ris", "ris", "re"),
1247 ];
1248 for (name, music21, humdrum, plain) in cases {
1249 let pitch = Pitch::from_name(name).unwrap();
1250 assert_eq!(
1251 c_major
1252 .solfeg(&pitch, SolfegVariant::Music21, true)
1253 .unwrap(),
1254 music21,
1255 "{name}"
1256 );
1257 assert_eq!(
1258 c_major
1259 .solfeg(&pitch, SolfegVariant::Humdrum, true)
1260 .unwrap(),
1261 humdrum,
1262 "{name}"
1263 );
1264 assert_eq!(
1265 c_major
1266 .solfeg(&pitch, SolfegVariant::Music21, false)
1267 .unwrap(),
1268 plain,
1269 "{name}"
1270 );
1271 }
1272 let chromatic = Scale::new(ScaleType::Chromatic, Pitch::from_name("C4").unwrap());
1273 assert_eq!(
1274 chromatic
1275 .solfeg(
1276 &Pitch::from_name("D4").unwrap(),
1277 SolfegVariant::Music21,
1278 true
1279 )
1280 .unwrap(),
1281 "mi"
1282 );
1283 assert!(
1284 chromatic
1285 .solfeg(
1286 &Pitch::from_name("B4").unwrap(),
1287 SolfegVariant::Music21,
1288 true
1289 )
1290 .is_err()
1291 );
1292 assert!(
1293 c_major
1294 .solfeg(
1295 &Pitch::from_name("C###4").unwrap(),
1296 SolfegVariant::Music21,
1297 true
1298 )
1299 .is_err()
1300 );
1301 }
1302 use super::*;
1303
1304 #[test]
1305 fn degrees_and_neighbouring_pitches_match_music21() {
1306 let scale = Scale::new(ScaleType::Major, Pitch::from_name("G4").unwrap());
1307 let degree = |name: &str| scale.degree_of(&Pitch::from_name(name).unwrap()).unwrap();
1308 assert_eq!(degree("G4"), Some(1));
1309 assert_eq!(degree("A4"), Some(2));
1310 assert_eq!(degree("F#5"), Some(7));
1311 assert_eq!(degree("B"), Some(3));
1312 assert_eq!(degree("B-4"), None);
1313 assert_eq!(degree("E3"), Some(6));
1314 assert_eq!(
1315 scale
1316 .degree_of_pitch_class(&Pitch::from_name("G-4").unwrap())
1317 .unwrap(),
1318 Some(7)
1319 );
1320
1321 let cases = [
1322 ("G4", true, 1, "A4"),
1323 ("G4", false, 1, "F#4"),
1324 ("F#4", true, 1, "G4"),
1325 ("B4", true, 3, "E5"),
1326 ("G4", false, 2, "E4"),
1327 ("E-4", true, 1, "E4"),
1328 ("E-4", false, 1, "D4"),
1329 ("B-4", false, 1, "A4"),
1330 ("G4", true, 8, "A5"),
1331 ("D5", true, 7, "D6"),
1332 ("G", true, 1, "A"),
1333 ("F#5", true, 1, "G5"),
1334 ("C3", false, 4, "F#2"),
1335 ];
1336 for (origin, ascending, steps, expected) in cases {
1337 let origin_pitch = Pitch::from_name(origin).unwrap();
1338 let next = if ascending {
1339 scale.next_pitch_above(&origin_pitch, steps)
1340 } else {
1341 scale.next_pitch_below(&origin_pitch, steps)
1342 };
1343 assert_eq!(
1344 next.unwrap().name_with_octave(),
1345 expected,
1346 "{origin} {ascending} {steps}"
1347 );
1348 }
1349 assert!(
1350 scale
1351 .next_pitch_above(&Pitch::from_name("G4").unwrap(), 0)
1352 .is_err()
1353 );
1354 }
1355
1356 #[test]
1357 fn derivation_matches_music21() {
1358 let pitches = |names: &[&str]| {
1359 names
1360 .iter()
1361 .map(|name| Pitch::from_name(*name).unwrap())
1362 .collect::<Vec<_>>()
1363 };
1364 type Row = (
1365 &'static [&'static str],
1366 ScaleType,
1367 &'static str,
1368 [(&'static str, usize); 4],
1369 &'static [&'static str],
1370 );
1371 let cases: [Row; 12] = [
1372 (
1373 &["C", "E", "G"],
1374 ScaleType::Major,
1375 "G",
1376 [("G", 3), ("F", 3), ("C", 3), ("Bb", 2)],
1377 &["G", "F", "C"],
1378 ),
1379 (
1380 &["C", "E", "G"],
1381 ScaleType::Minor,
1382 "A",
1383 [("A", 3), ("E", 3), ("D", 3), ("B", 2)],
1384 &["A", "E", "D"],
1385 ),
1386 (
1387 &["C", "E", "G"],
1388 ScaleType::Dorian,
1389 "A",
1390 [("A", 3), ("G", 3), ("D", 3), ("Bb", 2)],
1391 &["A", "G", "D"],
1392 ),
1393 (
1394 &["F#", "A", "C#", "E"],
1395 ScaleType::Major,
1396 "A",
1397 [("A", 4), ("E", 4), ("D", 4), ("B", 3)],
1398 &["A", "E", "D"],
1399 ),
1400 (
1401 &["F#", "A", "C#", "E"],
1402 ScaleType::Minor,
1403 "B",
1404 [("B", 4), ("F#", 4), ("Db", 4), ("C#", 4)],
1405 &["B", "F#", "Db", "C#", "Cb"],
1406 ),
1407 (
1408 &["G#", "B", "D", "F"],
1409 ScaleType::HarmonicMinor,
1410 "A",
1411 [("A", 4), ("F#", 4), ("Eb", 4), ("D#", 4)],
1412 &["A", "F#", "Eb", "D#", "C"],
1413 ),
1414 (
1415 &["Bb", "D", "F", "A-"],
1416 ScaleType::Major,
1417 "Eb",
1418 [("Eb", 4), ("D#", 4), ("Bb", 3), ("G#", 3)],
1419 &["Eb", "D#"],
1420 ),
1421 (
1422 &["Bb", "D", "F", "A-"],
1423 ScaleType::Dorian,
1424 "F",
1425 [("F", 4), ("Bb", 3), ("G#", 3), ("G", 3)],
1426 &["F"],
1427 ),
1428 (
1429 &["C", "D", "E", "F#", "G", "A", "B"],
1430 ScaleType::Major,
1431 "G",
1432 [("G", 7), ("D", 6), ("C", 6), ("A", 5)],
1433 &["G"],
1434 ),
1435 (
1436 &["C", "D", "E", "F#", "G", "A", "B"],
1437 ScaleType::Minor,
1438 "E",
1439 [("E", 7), ("B", 6), ("A", 6), ("Cb", 6)],
1440 &["E"],
1441 ),
1442 (
1443 &["C", "C#", "D"],
1444 ScaleType::Major,
1445 "Bb",
1446 [("Bb", 2), ("A", 2), ("G#", 2), ("G", 2)],
1447 &[],
1448 ),
1449 (
1450 &["Eb", "G", "Bb"],
1451 ScaleType::Major,
1452 "Bb",
1453 [("Bb", 3), ("G#", 3), ("Eb", 3), ("D#", 3)],
1454 &["Bb", "G#", "Eb", "D#"],
1455 ),
1456 ];
1457 for (names, scale_type, best, ranked, all) in cases {
1458 let input = pitches(names);
1459 assert_eq!(
1460 scale_type.derive(&input).unwrap().tonic().name(),
1461 best,
1462 "{scale_type:?} {names:?}"
1463 );
1464 let top = scale_type
1465 .derive_ranked(&input, Some(4))
1466 .unwrap()
1467 .into_iter()
1468 .map(|(matched, scale)| (scale.tonic().name(), matched))
1469 .collect::<Vec<_>>();
1470 let expected = ranked
1471 .iter()
1472 .map(|(tonic, matched)| (tonic.to_string(), *matched))
1473 .collect::<Vec<_>>();
1474 assert_eq!(top, expected, "{scale_type:?} {names:?}");
1475 let complete = scale_type
1476 .derive_all(&input)
1477 .unwrap()
1478 .iter()
1479 .map(|scale| scale.tonic().name())
1480 .collect::<Vec<_>>();
1481 assert_eq!(complete, all, "{scale_type:?} {names:?}");
1482 }
1483 }
1484
1485 #[test]
1486 fn degree_with_accidental_matches_music21() {
1487 let scale = Scale::new(ScaleType::Major, Pitch::from_name("C4").unwrap());
1488 let cases = [
1489 ("C4", 1, None),
1490 ("D4", 2, None),
1491 ("E-4", 3, Some("flat")),
1492 ("F#4", 4, Some("sharp")),
1493 ("G", 5, None),
1494 ("A#", 6, Some("sharp")),
1495 ("Bb", 7, Some("flat")),
1496 ("D--4", 2, Some("double-flat")),
1497 ("C#4", 1, Some("sharp")),
1498 ("E#4", 3, Some("sharp")),
1499 ];
1500 for (name, degree, accidental) in cases {
1501 let (found, found_accidental) = scale
1502 .degree_and_accidental_of(&Pitch::from_name(name).unwrap())
1503 .unwrap();
1504 assert_eq!(found, degree, "{name}");
1505 assert_eq!(
1506 found_accidental
1507 .as_ref()
1508 .map(|accidental| accidental.name()),
1509 accidental,
1510 "{name}"
1511 );
1512 }
1513 }
1514
1515 fn names(scale_type: ScaleType, tonic: &str) -> Vec<String> {
1516 Scale::new(scale_type, Pitch::from_name(tonic).expect("valid tonic"))
1517 .pitches()
1518 .expect("scale realizes")
1519 .iter()
1520 .map(|pitch| pitch.name())
1521 .collect()
1522 }
1523
1524 #[test]
1525 fn realizes_the_church_modes_on_c() {
1526 assert_eq!(
1527 names(ScaleType::Major, "C4"),
1528 ["C", "D", "E", "F", "G", "A", "B", "C"]
1529 );
1530 assert_eq!(
1531 names(ScaleType::Minor, "C4"),
1532 ["C", "D", "Eb", "F", "G", "Ab", "Bb", "C"]
1533 );
1534 assert_eq!(
1535 names(ScaleType::Dorian, "C4"),
1536 ["C", "D", "Eb", "F", "G", "A", "Bb", "C"]
1537 );
1538 assert_eq!(
1539 names(ScaleType::Phrygian, "C4"),
1540 ["C", "Db", "Eb", "F", "G", "Ab", "Bb", "C"]
1541 );
1542 assert_eq!(
1543 names(ScaleType::Lydian, "C4"),
1544 ["C", "D", "E", "F#", "G", "A", "B", "C"]
1545 );
1546 assert_eq!(
1547 names(ScaleType::Mixolydian, "C4"),
1548 ["C", "D", "E", "F", "G", "A", "Bb", "C"]
1549 );
1550 assert_eq!(
1551 names(ScaleType::Locrian, "C4"),
1552 ["C", "Db", "Eb", "F", "Gb", "Ab", "Bb", "C"]
1553 );
1554 }
1555
1556 #[test]
1557 fn realizes_the_altered_minors() {
1558 assert_eq!(
1559 names(ScaleType::HarmonicMinor, "C4"),
1560 ["C", "D", "Eb", "F", "G", "Ab", "B", "C"]
1561 );
1562 assert_eq!(
1563 names(ScaleType::MelodicMinor, "C4"),
1564 ["C", "D", "Eb", "F", "G", "A", "B", "C"]
1565 );
1566 }
1567
1568 #[test]
1569 fn plagal_modes_share_their_authentic_pitches() {
1570 for (plagal, authentic) in [
1571 (ScaleType::Hypodorian, ScaleType::Dorian),
1572 (ScaleType::Hypophrygian, ScaleType::Phrygian),
1573 (ScaleType::Hypolydian, ScaleType::Lydian),
1574 (ScaleType::Hypomixolydian, ScaleType::Mixolydian),
1575 (ScaleType::Hypolocrian, ScaleType::Locrian),
1576 (ScaleType::Hypoaeolian, ScaleType::Minor),
1577 ] {
1578 let mut plagal_names = names(plagal, "C4");
1579 plagal_names.sort();
1580 plagal_names.dedup();
1581 let mut authentic_names = names(authentic, "C4");
1582 authentic_names.sort();
1583 authentic_names.dedup();
1584 assert_eq!(
1585 plagal_names, authentic_names,
1586 "{plagal:?} should share {authentic:?}'s pitch collection"
1587 );
1588 }
1589 }
1590
1591 #[test]
1592 fn a_plagal_mode_is_realized_a_fourth_below_its_final() {
1593 let scale = Scale::new(
1596 ScaleType::Hypodorian,
1597 Pitch::from_name("d").expect("valid tonic"),
1598 );
1599 let realized: Vec<String> = scale
1600 .pitches()
1601 .expect("scale realizes")
1602 .iter()
1603 .map(Pitch::name_with_octave)
1604 .collect();
1605 assert_eq!(
1606 realized,
1607 ["A3", "B3", "C4", "D4", "E4", "F4", "G4", "A4"],
1608 "a plagal mode starts a fourth below its final"
1609 );
1610 assert_eq!(scale.final_pitch().unwrap().name_with_octave(), "D4");
1611 assert_eq!(scale.dominant().unwrap().name_with_octave(), "F4");
1612
1613 let dorian = Scale::new(
1616 ScaleType::Dorian,
1617 Pitch::from_name("d").expect("valid tonic"),
1618 );
1619 assert_eq!(dorian.final_pitch().unwrap().name_with_octave(), "D4");
1620 assert_eq!(dorian.dominant().unwrap().name_with_octave(), "A4");
1621 }
1622
1623 #[test]
1624 fn a_leading_tone_is_a_semitone_below_the_final() {
1625 let minor = Scale::new(
1627 ScaleType::Minor,
1628 Pitch::from_name("c").expect("valid tonic"),
1629 );
1630 assert_eq!(minor.pitch_at_degree(7).unwrap().name(), "Bb");
1631 assert_eq!(minor.leading_tone().unwrap().name(), "B");
1632 let major = Scale::new(
1634 ScaleType::Major,
1635 Pitch::from_name("C").expect("valid tonic"),
1636 );
1637 assert_eq!(major.leading_tone().unwrap().name_with_octave(), "B4");
1638 }
1639
1640 #[test]
1641 fn whole_tone_spells_upward_until_accidentals_run_out() {
1642 assert_eq!(
1644 names(ScaleType::WholeTone, "C4"),
1645 ["C", "D", "E", "F#", "G#", "A#", "C"]
1646 );
1647 assert_eq!(
1649 names(ScaleType::WholeTone, "B4"),
1650 ["B", "C#", "D#", "E#", "G", "A", "B"]
1651 );
1652 }
1653
1654 #[test]
1655 fn octatonic_alternates_tone_and_semitone() {
1656 assert_eq!(
1657 names(ScaleType::Octatonic, "C4"),
1658 ["C", "D", "Eb", "F", "Gb", "Ab", "A", "B", "C"]
1659 );
1660 assert_eq!(
1661 names(ScaleType::Octatonic, "G4"),
1662 ["G", "A", "Bb", "C", "Db", "Eb", "Fb", "Gb", "G"]
1663 );
1664 assert_eq!(
1667 names(ScaleType::Octatonic, "E4"),
1668 ["E", "F#", "G", "A", "Bb", "C", "Db", "Eb", "E"]
1669 );
1670 }
1671
1672 #[test]
1673 fn chromatic_uses_the_most_common_spelling() {
1674 assert_eq!(
1675 names(ScaleType::Chromatic, "C4"),
1676 [
1677 "C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B", "C"
1678 ]
1679 );
1680 }
1681
1682 #[test]
1683 fn rag_asawari_is_pentatonic() {
1684 assert_eq!(
1685 names(ScaleType::RagAsawari, "C4"),
1686 ["C", "D", "F", "G", "Ab", "C"]
1687 );
1688 }
1689
1690 #[test]
1691 fn rag_marwa_dips_below_its_sixth_degree() {
1692 assert_eq!(
1697 names(ScaleType::RagMarwa, "C4"),
1698 ["C", "Db", "E", "F#", "A", "B", "A", "C", "Db"]
1699 );
1700 assert_eq!(
1701 names(ScaleType::RagMarwa, "E-4"),
1702 ["Eb", "Fb", "G", "A", "C", "D", "C", "Eb", "Fb"]
1703 );
1704 }
1705
1706 #[test]
1707 fn simplifying_scales_never_exceed_their_accidental_budget() {
1708 for scale_type in [ScaleType::WholeTone, ScaleType::Octatonic] {
1709 for tonic in ["C4", "G4", "D4", "A4", "E4", "B4", "F#4", "E-4", "G-4"] {
1710 let pitches = Scale::new(scale_type, Pitch::from_name(tonic).unwrap())
1711 .pitches()
1712 .unwrap();
1713 assert!(
1714 max_alter(&pitches) <= 1,
1715 "{scale_type:?} on {tonic} exceeded one accidental"
1716 );
1717 }
1718 }
1719 }
1720
1721 #[test]
1722 fn a_note_a_scale_stands_twice_has_two_notes_below_it() {
1723 let marwa = Scale::new(ScaleType::RagMarwa, Pitch::from_name("C4").unwrap()).descending();
1729 let from = Pitch::from_name("D-2").unwrap();
1730 assert_eq!(marwa.places_of(&from).unwrap(), 2);
1731 let below: Vec<String> = (0..2)
1732 .map(|place| {
1733 marwa
1734 .next_pitch_below_from(&from, 1, place)
1735 .unwrap()
1736 .name_with_octave()
1737 })
1738 .collect();
1739 assert_eq!(below, ["C2", "B1"]);
1740 assert_eq!(
1741 marwa.next_pitch_below(&from, 1).unwrap().name_with_octave(),
1742 "C2"
1743 );
1744
1745 let major = Scale::new(ScaleType::Major, Pitch::from_name("C4").unwrap());
1748 let d = Pitch::from_name("D4").unwrap();
1749 assert_eq!(major.places_of(&d).unwrap(), 1);
1750 assert_eq!(
1751 major
1752 .next_pitch_below_from(&d, 1, 7)
1753 .unwrap()
1754 .name_with_octave(),
1755 "C4"
1756 );
1757 }
1758
1759 #[test]
1760 fn degree_lookup_matches_the_realized_pitches() {
1761 for scale_type in ScaleType::ALL {
1762 let scale = Scale::new(scale_type, Pitch::from_name("E-4").unwrap());
1763 let pitches = scale.pitches().unwrap();
1764 let degrees = scale
1768 .named_degrees()
1769 .unwrap_or_else(|| (1..=scale.degree_count() as IntegerType).collect());
1770 for (expected, degree) in pitches.iter().zip(degrees) {
1771 let actual = scale.pitch_at_degree(degree).unwrap();
1772 assert_eq!(
1773 actual.name_with_octave(),
1774 expected.name_with_octave(),
1775 "{scale_type:?} degree {degree}"
1776 );
1777 let found = scale.degree_of(expected).unwrap().unwrap();
1781 assert_eq!(
1782 scale
1783 .pitch_at_degree(found as IntegerType)
1784 .unwrap()
1785 .name_with_octave(),
1786 expected.name_with_octave(),
1787 "{scale_type:?} degree {degree} read back as {found}"
1788 );
1789 }
1790 if let Some(named) = scale.named_degrees() {
1793 for degree in 1..=*named.last().unwrap() {
1794 assert_eq!(
1795 scale.pitch_on_degree(degree).unwrap().is_some(),
1796 named.contains(°ree),
1797 "{scale_type:?} degree {degree}"
1798 );
1799 }
1800 }
1801 }
1802 }
1803
1804 #[test]
1805 fn a_degree_outside_the_scale_counts_round_it() {
1806 let scale = Scale::new(ScaleType::Phrygian, Pitch::from_name("G4").unwrap());
1808 for (degree, expected) in [
1809 (-1, "Eb5"),
1810 (0, "F5"),
1811 (1, "G4"),
1812 (7, "F5"),
1813 (8, "G4"),
1814 (11, "C5"),
1815 (15, "G4"),
1816 ] {
1817 assert_eq!(
1818 scale.pitch_at_degree(degree).unwrap().name_with_octave(),
1819 expected,
1820 "degree {degree}"
1821 );
1822 }
1823 }
1824
1825 #[test]
1826 fn every_scale_realizes_on_every_common_tonic() {
1827 for scale_type in ScaleType::ALL {
1828 for tonic in [
1829 "C4", "G4", "D4", "A4", "E4", "B4", "F#4", "F4", "B-4", "E-4", "A-4",
1830 ] {
1831 let scale = Scale::new(scale_type, Pitch::from_name(tonic).unwrap());
1832 let pitches = scale.pitches().expect("scale realizes");
1833 let beyond = usize::from(scale_type.beyond_terminus().is_some());
1834 assert_eq!(
1835 pitches.len(),
1836 scale_type.degree_count() + 1 + beyond,
1837 "{scale_type:?} on {tonic}"
1838 );
1839 }
1840 }
1841 }
1842
1843 #[test]
1844 fn music21_names_are_distinct() {
1845 let mut names: Vec<&str> = ScaleType::ALL.iter().map(|s| s.music21_name()).collect();
1846 names.sort_unstable();
1847 let count = names.len();
1848 names.dedup();
1849 assert_eq!(names.len(), count, "music21 names must be unique");
1850 }
1851}