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