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)]
24pub(super) enum Simplification {
25 Exact,
27 MaxAccidental,
29 MostCommon,
31}
32
33static STEP_INTERVALS: LazyLock<HashMap<&'static str, Interval>> = LazyLock::new(|| {
39 ["m2", "M2", "a2", "m3", "M3", "-M2"]
40 .into_iter()
41 .map(|name| {
42 let interval =
43 Interval::from_name(name).expect("scale step intervals are valid interval names");
44 (name, interval)
45 })
46 .collect()
47});
48
49pub(super) fn step_interval(name: &str) -> Result<Interval> {
50 match STEP_INTERVALS.get(name) {
51 Some(interval) => Ok(interval.clone()),
52 None => Interval::from_name(name),
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
58#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
59pub enum SolfegVariant {
60 Music21,
62 Humdrum,
64}
65
66pub const SOLFEG_SYLLABLES: [[&str; 5]; 7] = [
69 ["def", "de", "do", "di", "dis"],
70 ["raf", "ra", "re", "ri", "ris"],
71 ["mef", "me", "mi", "mis", "mish"],
72 ["fef", "fe", "fa", "fi", "fis"],
73 ["sef", "se", "sol", "si", "sis"],
74 ["lef", "le", "la", "li", "lis"],
75 ["tef", "te", "ti", "tis", "tish"],
76];
77
78pub const HUMDRUM_SOLFEG_SYLLABLES: [[&str; 5]; 7] = [
80 ["def", "de", "do", "di", "dis"],
81 ["raf", "ra", "re", "ri", "ris"],
82 ["mef", "me", "mi", "my", "mish"],
83 ["fef", "fe", "fa", "fi", "fis"],
84 ["sef", "se", "so", "si", "sis"],
85 ["lef", "le", "la", "li", "lis"],
86 ["tef", "te", "ti", "ty", "tish"],
87];
88
89#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
95#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
96#[non_exhaustive]
97#[must_use]
98pub enum ScaleType {
99 Major,
101 Minor,
103 Dorian,
105 Phrygian,
107 Lydian,
109 Mixolydian,
111 Locrian,
113 Hypodorian,
115 Hypophrygian,
117 Hypolydian,
119 Hypomixolydian,
121 Hypolocrian,
123 Hypoaeolian,
125 HarmonicMinor,
127 MelodicMinor,
129 Chromatic,
131 WholeTone,
133 Octatonic,
135 RagAsawari,
137 RagMarwa,
143}
144
145impl ScaleType {
146 pub const ALL: [ScaleType; 20] = [
148 Self::Major,
149 Self::Minor,
150 Self::Dorian,
151 Self::Phrygian,
152 Self::Lydian,
153 Self::Mixolydian,
154 Self::Locrian,
155 Self::Hypodorian,
156 Self::Hypophrygian,
157 Self::Hypolydian,
158 Self::Hypomixolydian,
159 Self::Hypolocrian,
160 Self::Hypoaeolian,
161 Self::HarmonicMinor,
162 Self::MelodicMinor,
163 Self::Chromatic,
164 Self::WholeTone,
165 Self::Octatonic,
166 Self::RagAsawari,
167 Self::RagMarwa,
168 ];
169
170 pub fn music21_name(self) -> &'static str {
172 match self {
173 Self::Major => "MajorScale",
174 Self::Minor => "MinorScale",
175 Self::Dorian => "DorianScale",
176 Self::Phrygian => "PhrygianScale",
177 Self::Lydian => "LydianScale",
178 Self::Mixolydian => "MixolydianScale",
179 Self::Locrian => "LocrianScale",
180 Self::Hypodorian => "HypodorianScale",
181 Self::Hypophrygian => "HypophrygianScale",
182 Self::Hypolydian => "HypolydianScale",
183 Self::Hypomixolydian => "HypomixolydianScale",
184 Self::Hypolocrian => "HypolocrianScale",
185 Self::Hypoaeolian => "HypoaeolianScale",
186 Self::HarmonicMinor => "HarmonicMinorScale",
187 Self::MelodicMinor => "MelodicMinorScale",
188 Self::Chromatic => "ChromaticScale",
189 Self::WholeTone => "WholeToneScale",
190 Self::Octatonic => "OctatonicScale",
191 Self::RagAsawari => "RagAsawari",
192 Self::RagMarwa => "RagMarwa",
193 }
194 }
195
196 pub fn music21_descriptive_name(self) -> &'static str {
203 match self {
204 Self::Major => "major",
205 Self::Minor => "minor",
206 Self::Dorian => "dorian",
207 Self::Phrygian => "phrygian",
208 Self::Lydian => "lydian",
209 Self::Mixolydian => "mixolydian",
210 Self::Locrian => "locrian",
211 Self::Hypodorian => "hypodorian",
212 Self::Hypophrygian => "hypophrygian",
213 Self::Hypolydian => "hypolydian",
214 Self::Hypomixolydian => "hypomixolydian",
215 Self::Hypolocrian => "hypolocrian",
216 Self::Hypoaeolian => "hypoaeolian",
217 Self::HarmonicMinor => "harmonic minor",
218 Self::MelodicMinor => "melodic minor",
219 Self::Chromatic => "Chromatic",
220 Self::WholeTone => "Whole tone",
221 Self::Octatonic => "Octatonic",
222 Self::RagAsawari => "Rag Asawari",
223 Self::RagMarwa => "Rag Marwa",
224 }
225 }
226
227 pub fn from_music21_name(name: &str) -> Option<Self> {
230 Self::ALL
231 .into_iter()
232 .find(|scale_type| scale_type.music21_name() == name)
233 }
234
235 pub fn tonic_degree(self) -> usize {
243 if self.is_plagal() { 4 } else { 1 }
244 }
245
246 pub fn dominant_degree(self) -> usize {
252 match self {
253 Self::Hypophrygian | Self::Hypomixolydian => 7,
254 _ if self.is_plagal() => 6,
255 _ => 5,
256 }
257 }
258
259 pub fn realization_steps(self) -> Vec<&'static str> {
268 let steps = self.steps();
269 let count = steps.len();
270 let start = (count + 1 - self.tonic_degree()) % count;
271 steps[start..]
272 .iter()
273 .chain(&steps[..start])
274 .copied()
275 .collect()
276 }
277
278 pub fn beyond_terminus(self) -> Option<&'static str> {
286 match self {
287 Self::RagMarwa => Some("m2"),
288 _ => None,
289 }
290 }
291
292 pub fn descending_form(self) -> Option<Self> {
301 match self {
302 Self::MelodicMinor | Self::RagAsawari => Some(Self::Minor),
303 _ => None,
304 }
305 }
306
307 pub fn ascending_degrees(self) -> Option<&'static [u8]> {
315 match self {
316 Self::RagAsawari => Some(&[1, 2, 4, 5, 6]),
317 _ => None,
318 }
319 }
320
321 pub fn descending_steps(self) -> Option<&'static [&'static str]> {
333 match self {
334 Self::RagMarwa => Some(&["m2", "A2", "M2", "m3", "M2", "d3", "-m2"]),
335 _ => None,
336 }
337 }
338
339 pub fn is_plagal(self) -> bool {
341 matches!(
342 self,
343 Self::Hypodorian
344 | Self::Hypophrygian
345 | Self::Hypolydian
346 | Self::Hypomixolydian
347 | Self::Hypolocrian
348 | Self::Hypoaeolian
349 )
350 }
351
352 pub(super) fn steps(self) -> &'static [&'static str] {
361 match self {
362 Self::Major => &["M2", "M2", "m2", "M2", "M2", "M2", "m2"],
363 Self::Minor | Self::Hypoaeolian => &["M2", "m2", "M2", "M2", "m2", "M2", "M2"],
364 Self::Dorian => &["M2", "m2", "M2", "M2", "M2", "m2", "M2"],
365 Self::Phrygian => &["m2", "M2", "M2", "M2", "m2", "M2", "M2"],
366 Self::Lydian => &["M2", "M2", "M2", "m2", "M2", "M2", "m2"],
367 Self::Mixolydian => &["M2", "M2", "m2", "M2", "M2", "m2", "M2"],
368 Self::Locrian => &["m2", "M2", "M2", "m2", "M2", "M2", "M2"],
369 Self::Hypodorian => &["M2", "m2", "M2", "M2", "M2", "m2", "M2"],
370 Self::Hypophrygian => &["m2", "M2", "M2", "M2", "m2", "M2", "M2"],
371 Self::Hypolydian => &["M2", "M2", "M2", "m2", "M2", "M2", "m2"],
372 Self::Hypomixolydian => &["M2", "M2", "m2", "M2", "M2", "m2", "M2"],
373 Self::Hypolocrian => &["m2", "M2", "M2", "m2", "M2", "M2", "M2"],
374 Self::HarmonicMinor => &["M2", "m2", "M2", "M2", "m2", "a2", "m2"],
375 Self::MelodicMinor => &["M2", "m2", "M2", "M2", "M2", "M2", "m2"],
376 Self::Chromatic => &["m2"; 12],
377 Self::WholeTone => &["M2"; 6],
378 Self::Octatonic => &["M2", "m2", "M2", "m2", "M2", "m2", "M2", "m2"],
379 Self::RagAsawari => &["M2", "m3", "M2", "m2", "M3"],
380 Self::RagMarwa => &["m2", "a2", "M2", "m3", "M2", "-M2", "m3"],
383 }
384 }
385
386 pub(super) fn simplification(self) -> Simplification {
388 match self {
389 Self::WholeTone | Self::Octatonic | Self::RagMarwa => Simplification::MaxAccidental,
390 Self::Chromatic | Self::RagAsawari => Simplification::MostCommon,
391 _ => Simplification::Exact,
392 }
393 }
394
395 pub fn degree_count(self) -> usize {
397 self.steps().len()
398 }
399}
400
401pub(super) const SCALE_STARTS: [&str; 15] = [
404 "C", "C#", "D-", "D", "D#", "E-", "E", "F", "F#", "G", "G#", "A", "B-", "B", "C-",
405];
406
407impl ScaleType {
408 pub fn derive_ranked(
412 self,
413 pitches: &[Pitch],
414 limit: Option<usize>,
415 ) -> Result<Vec<(usize, Scale)>> {
416 self.derive_ranked_by(pitches, limit, DegreeComparison::PitchClass)
417 }
418
419 pub fn derive_ranked_by(
425 self,
426 pitches: &[Pitch],
427 limit: Option<usize>,
428 comparison: DegreeComparison,
429 ) -> Result<Vec<(usize, Scale)>> {
430 let targets = pitches
431 .iter()
432 .map(|pitch| comparison.key(pitch))
433 .collect::<Vec<_>>();
434 let mut ranked = Vec::with_capacity(SCALE_STARTS.len());
435 for start in SCALE_STARTS {
436 let scale = Scale::new(self, Pitch::from_name(start)?);
437 let degrees = scale
438 .pitches()?
439 .iter()
440 .map(|pitch| comparison.key(pitch))
441 .collect::<Vec<_>>();
442 let matched = targets
443 .iter()
444 .filter(|target| degrees.contains(target))
445 .count();
446 ranked.push((matched, scale));
447 }
448 ranked.sort_by(|left, right| {
449 left.0
450 .cmp(&right.0)
451 .then_with(|| left.1.tonic().ps().total_cmp(&right.1.tonic().ps()))
452 });
453 ranked.reverse();
454 if let Some(limit) = limit {
455 ranked.truncate(limit);
456 }
457 Ok(ranked)
458 }
459
460 pub fn derive(self, pitches: &[Pitch]) -> Result<Scale> {
462 self.derive_ranked(pitches, Some(1))?
463 .pop()
464 .map(|(_, scale)| scale)
465 .ok_or_else(|| crate::error::Error::Scale("no candidate tonics".to_string()))
466 }
467
468 pub fn derive_all(self, pitches: &[Pitch]) -> Result<Vec<Scale>> {
471 Ok(self
472 .derive_ranked(pitches, None)?
473 .into_iter()
474 .filter(|(matched, _)| *matched == pitches.len())
475 .map(|(_, scale)| scale)
476 .collect())
477 }
478}
479
480pub(super) const MAX_RANGE_OCTAVES: usize = 12;
483
484#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
487#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
488pub enum DegreeComparison {
489 PitchClass,
491 Name,
493 Step,
495}
496
497impl DegreeComparison {
498 pub(super) fn key(self, pitch: &Pitch) -> String {
500 match self {
501 Self::PitchClass => pitch.pitch_class().number().to_string(),
502 Self::Name => pitch.name(),
503 Self::Step => pitch.name().chars().take(1).collect(),
504 }
505 }
506}
507
508pub(super) fn advance(
510 pitch: &Pitch,
511 interval: &Interval,
512 simplification: Simplification,
513) -> Result<Pitch> {
514 match simplification {
515 Simplification::MaxAccidental => {
516 interval.transpose_pitch_with_options(pitch, false, Some(1))
517 }
518 Simplification::Exact => interval.transpose_pitch_with_options(pitch, false, None),
519 Simplification::MostCommon => {
520 let mut transposed = interval.transpose_pitch_with_options(pitch, false, None)?;
521 if transposed.accidental().alter() != 0.0 {
522 transposed.simplify_enharmonic_in_place(true)?;
523 }
524 Ok(transposed)
525 }
526 }
527}
528
529#[cfg(test)]
531fn max_alter(pitches: &[Pitch]) -> crate::defaults::IntegerType {
532 pitches
533 .iter()
534 .map(|pitch| pitch.accidental().alter().abs() as crate::defaults::IntegerType)
535 .max()
536 .unwrap_or(0)
537}
538
539#[cfg(test)]
540mod tests {
541 use crate::defaults::{FloatType, IntegerType};
542 #[test]
543 fn degrees_are_found_by_pitch_class_name_or_step() {
544 use super::{DegreeComparison, Scale, ScaleType};
545 use crate::Pitch;
546
547 let major = Scale::new(ScaleType::Major, Pitch::from_name("C").unwrap());
548 let e_sharp = Pitch::from_name("E#").unwrap();
549 assert_eq!(
550 major
551 .degree_of_by(&e_sharp, DegreeComparison::PitchClass)
552 .unwrap(),
553 Some(4)
554 );
555 assert_eq!(
556 major
557 .degree_of_by(&e_sharp, DegreeComparison::Name)
558 .unwrap(),
559 None
560 );
561 assert_eq!(
562 major
563 .degree_of_by(&e_sharp, DegreeComparison::Step)
564 .unwrap(),
565 Some(3)
566 );
567 let e_flat = Pitch::from_name("E-").unwrap();
568 let (degree, accidental) = major.degree_and_accidental_of(&e_flat).unwrap();
569 assert_eq!(degree, 3);
570 assert_eq!(accidental.as_ref().map(|a| a.name()), Some("flat"));
571 let whole_tone = Scale::new(ScaleType::WholeTone, Pitch::from_name("C").unwrap());
572 assert!(
573 whole_tone
574 .degree_and_accidental_of(&Pitch::from_name("B").unwrap())
575 .is_err()
576 );
577
578 let marwa = Scale::new(ScaleType::RagMarwa, Pitch::from_name("C4").unwrap());
580 assert_eq!(
581 marwa
582 .degrees_of_by(&Pitch::from_name("A").unwrap(), DegreeComparison::Name)
583 .unwrap(),
584 [5, 7]
585 );
586 }
587
588 #[test]
589 fn a_scale_moves_to_its_relatives_parallels_and_a_new_tonic() {
590 use super::{Scale, ScaleType};
591 use crate::Pitch;
592
593 let tonic = |scale: &Scale| scale.tonic().name();
594 let c_minor = Scale::new(ScaleType::Minor, Pitch::from_name("C").unwrap());
595 assert_eq!(tonic(&c_minor.parallel_major()), "C");
596 assert_eq!(c_minor.parallel_major().scale_type(), ScaleType::Major);
597 assert_eq!(tonic(&c_minor.relative_major().unwrap()), "E-");
598 let c_major = Scale::new(ScaleType::Major, Pitch::from_name("C").unwrap());
599 assert_eq!(tonic(&c_major.relative_minor().unwrap()), "A");
600 assert_eq!(c_major.parallel_minor().scale_type(), ScaleType::Minor);
601 let mut moved = c_major.clone();
602 moved.set_tonic(Pitch::from_name("D").unwrap());
603 assert_eq!(tonic(&moved), "D");
604 assert_eq!(moved.pitches().unwrap()[1].name(), "E");
605 assert!(c_major.octave_duplicating());
606 assert!(c_major.is_realizable());
607 assert_eq!(
608 ScaleType::from_music21_name("MajorScale"),
609 Some(ScaleType::Major)
610 );
611 assert_eq!(ScaleType::from_music21_name("NoSuchScale"), None);
612 }
613
614 #[test]
615 fn a_scale_walks_down_as_well_as_up() {
616 use super::{Scale, ScaleType};
617 use crate::Pitch;
618
619 let names = |pitches: Vec<Pitch>| -> Vec<String> {
620 pitches.iter().map(Pitch::name_with_octave).collect()
621 };
622 let major = Scale::new(ScaleType::Major, Pitch::from_name("C4").unwrap());
623 assert_eq!(
624 names(major.pitches_descending().unwrap()),
625 ["C5", "B4", "A4", "G4", "F4", "E4", "D4", "C4"]
626 );
627 let low = Pitch::from_name("C3").unwrap();
628 let high = Pitch::from_name("C5").unwrap();
629 assert_eq!(
630 names(
631 major
632 .pitches_between_descending(&Pitch::from_name("G4").unwrap(), &high)
633 .unwrap()
634 ),
635 ["C5", "B4", "A4", "G4"]
636 );
637 assert_eq!(
638 names(
639 major
640 .pitches_from_scale_degrees_between(&[1, 5], &low, &high)
641 .unwrap()
642 ),
643 ["C3", "G3", "C4", "G4", "C5"]
644 );
645
646 let c_sharp = Pitch::from_name("C#4").unwrap();
649 assert_eq!(
650 major
651 .next_pitch_below(&c_sharp, 1)
652 .unwrap()
653 .name_with_octave(),
654 "C4"
655 );
656 assert_eq!(
657 major
658 .next_pitch_beside(&c_sharp, -1, true)
659 .unwrap()
660 .name_with_octave(),
661 "B3"
662 );
663 assert_eq!(
664 major
665 .next_pitch_beside(&c_sharp, -1, false)
666 .unwrap()
667 .name_with_octave(),
668 "C4"
669 );
670 assert_eq!(
671 major
672 .next_pitch_beside(&c_sharp, 1, false)
673 .unwrap()
674 .name_with_octave(),
675 "E4"
676 );
677
678 let marwa = Scale::new(ScaleType::RagMarwa, Pitch::from_name("C4").unwrap());
682 let d_flat = Pitch::from_name("D-4").unwrap();
683 assert_eq!(marwa.places_of(&d_flat).unwrap(), 1);
684 assert_eq!(
685 marwa
686 .next_pitch_above_from(&d_flat, 1, 0)
687 .unwrap()
688 .name_with_octave(),
689 "E4"
690 );
691 assert_eq!(
692 marwa
693 .next_pitch_below_from(&Pitch::from_name("C5").unwrap(), 1, 0)
694 .unwrap()
695 .name_with_octave(),
696 "A4"
697 );
698 }
699
700 #[test]
701 fn a_custom_scale_ranks_the_tonics_that_fit_a_set_of_pitches() {
702 use super::{DegreeComparison, Scale};
703 use crate::Pitch;
704
705 let pitches: Vec<Pitch> = ["C4", "D4", "E4", "F4", "G4", "A4", "B4", "C5"]
706 .iter()
707 .map(|name| Pitch::from_name(*name).unwrap())
708 .collect();
709 let custom = Scale::from_pitches(&pitches).unwrap();
710 assert!(custom.is_custom());
711 let targets: Vec<Pitch> = ["G4", "B4", "D5"]
712 .iter()
713 .map(|name| Pitch::from_name(*name).unwrap())
714 .collect();
715 let ranked = custom
716 .derive_ranked_by(&targets, Some(2), DegreeComparison::PitchClass)
717 .unwrap();
718 assert_eq!(ranked.len(), 2);
719 assert_eq!(ranked[0].0, 3);
720 assert!(ranked[0].1.is_custom());
721 }
722
723 #[test]
727 fn a_stream_is_tuned_onto_a_scale() {
728 use super::Scale;
729 use crate::stream::StreamElement;
730 use crate::{Chord, Note, Pitch, Stream};
731
732 let tuned: Vec<Pitch> = [
733 ("C4", 0.0),
734 ("D4", 20.0),
735 ("E4", 0.0),
736 ("F4", 0.0),
737 ("G4", 0.0),
738 ("A4", -15.0),
739 ("B4", 0.0),
740 ("C5", 0.0),
741 ]
742 .iter()
743 .map(|(name, cents)| {
744 let mut pitch = Pitch::from_name(*name).unwrap();
745 pitch.set_microtone_cents(*cents).unwrap();
746 pitch
747 })
748 .collect();
749 let scale = Scale::from_pitches(&tuned).unwrap();
750 let mut stream = Stream::new();
751 for name in ["D5", "F#4", "A3", "B#4", "G-5"] {
752 stream.push(Note::from_pitch(Pitch::from_name(name).unwrap()));
753 }
754 stream.push(Chord::new("C5 E5 G5").unwrap());
755 scale.tune(&mut stream).unwrap();
756 let sounding: Vec<(String, FloatType)> = stream
757 .events()
758 .iter()
759 .flat_map(|event| match event.element() {
760 StreamElement::Note(note) => {
761 vec![(note.pitch().name_with_octave(), note.pitch().ps())]
762 }
763 StreamElement::Chord(chord) => chord
764 .pitches()
765 .iter()
766 .map(|pitch| (pitch.name_with_octave(), pitch.ps()))
767 .collect(),
768 _ => Vec::new(),
769 })
770 .collect();
771 let expected: Vec<(String, FloatType)> = [
772 ("D5", 74.2),
773 ("F#4", 66.0),
774 ("A3", 56.85),
775 ("B#4", 72.0),
776 ("G-5", 78.0),
777 ("C5", 72.0),
778 ("E5", 76.0),
779 ("G5", 79.0),
780 ]
781 .iter()
782 .map(|(name, ps)| (name.to_string(), *ps))
783 .collect();
784 assert_eq!(sounding.len(), expected.len());
785 for ((name, ps), (wanted_name, wanted_ps)) in sounding.iter().zip(&expected) {
786 assert_eq!(name, wanted_name);
787 assert!(
788 (ps - wanted_ps).abs() < 1e-9,
789 "{name} sounds at {ps}, not {wanted_ps}"
790 );
791 }
792 }
793
794 #[test]
797 fn a_scale_carries_numerals_and_writes_itself_as_scala() {
798 use super::{Scale, ScaleType};
799 use crate::Pitch;
800 use crate::tuningsystem::scala::ScalaDegree;
801
802 let scale = Scale::new(ScaleType::Major, Pitch::from_name("A-4").unwrap());
803 let tonic = scale.roman_numeral(1).unwrap();
804 assert_eq!(tonic.to_chord().unwrap().root().unwrap().to_string(), "A-4");
805 let dominant = scale.roman_numeral(5).unwrap();
806 assert_eq!(
807 dominant.to_chord().unwrap().root().unwrap().to_string(),
808 "E-5"
809 );
810 assert_eq!(dominant.figure_and_key(), "V in A- major");
811 assert!(scale.roman_numeral(8).is_err());
812
813 let scala = scale.scala_data().unwrap();
814 assert_eq!(scala.len(), 7);
815 assert!(matches!(scala.degrees()[0], ScalaDegree::Ratio(_)));
816 let cents: Vec<i64> = scala
817 .degrees()
818 .iter()
819 .map(|degree| degree.cents().round() as i64)
820 .collect();
821 assert_eq!(cents, [0, 200, 400, 500, 700, 900, 1100]);
822 assert_eq!(scala.period().cents().round() as i64, 1200);
823 assert_eq!(scala.description(), "A- major");
824 }
825
826 #[test]
827 fn a_scale_can_be_given_by_its_notes() {
828 let given: Vec<Pitch> = ["C4", "E-4", "G-4", "A4"]
831 .iter()
832 .map(|name| Pitch::from_name(*name).expect("valid pitch"))
833 .collect();
834 let scale = Scale::from_pitches(&given).expect("a scale");
835 assert!(scale.is_custom());
836 assert_eq!(scale.degree_count(), 4);
837 let realized: Vec<String> = scale
838 .pitches()
839 .expect("realizes")
840 .iter()
841 .map(Pitch::name_with_octave)
842 .collect();
843 assert_eq!(realized, ["C4", "E-4", "G-4", "A4", "C5"]);
844 assert_eq!(scale.pitch_at_degree(4).unwrap().name_with_octave(), "A4");
845 assert_eq!(scale.pitch_at_degree(5).unwrap().name_with_octave(), "C4");
849 assert_eq!(
850 scale.degree_of(&Pitch::from_name("G-").unwrap()).unwrap(),
851 Some(3)
852 );
853
854 let range: Vec<String> = scale
856 .pitches_between(
857 &Pitch::from_name("E-5").unwrap(),
858 &Pitch::from_name("C6").unwrap(),
859 )
860 .expect("a range")
861 .iter()
862 .map(Pitch::name_with_octave)
863 .collect();
864 assert_eq!(range, ["E-5", "G-5", "A5", "C6"]);
865
866 assert!(Scale::from_pitches(&[]).is_err());
867 }
868 #[test]
869 fn a_range_starts_at_the_first_scale_pitch_inside_it() {
870 let scale = Scale::new(ScaleType::Major, Pitch::from_name("C").unwrap());
873 let range = scale
874 .pitches_between(
875 &Pitch::from_name("E-5").unwrap(),
876 &Pitch::from_name("G-7").unwrap(),
877 )
878 .unwrap();
879 let names: Vec<String> = range.iter().map(Pitch::name_with_octave).take(4).collect();
880 assert_eq!(names, ["E5", "F5", "G5", "A5"]);
881 assert_eq!(range.last().unwrap().name_with_octave(), "F7");
882
883 let octave = scale
885 .pitches_between(
886 &Pitch::from_name("C3").unwrap(),
887 &Pitch::from_name("C4").unwrap(),
888 )
889 .unwrap();
890 let names: Vec<String> = octave.iter().map(Pitch::name_with_octave).collect();
891 assert_eq!(names, ["C3", "D3", "E3", "F3", "G3", "A3", "B3", "C4"]);
892
893 let descending: Vec<String> = scale
896 .pitches_between(
897 &Pitch::from_name("C5").unwrap(),
898 &Pitch::from_name("C4").unwrap(),
899 )
900 .unwrap()
901 .iter()
902 .map(Pitch::name_with_octave)
903 .collect();
904 assert_eq!(descending, ["C5", "B4", "A4", "G4", "F4", "E4", "D4", "C4"]);
905 }
906
907 #[test]
908 fn scale_helpers_match_music21() {
909 let c_major = Scale::new(ScaleType::Major, Pitch::from_name("C4").unwrap());
910 let pitch = |name: &str| Pitch::from_name(name).unwrap();
911 let names = |pitches: &[Pitch]| -> Vec<String> {
912 pitches.iter().map(Pitch::name_with_octave).collect()
913 };
914
915 assert_eq!(c_major.degree_count(), 7);
916 assert_eq!(
917 Scale::new(ScaleType::Chromatic, pitch("C4")).degree_count(),
918 12
919 );
920 assert_eq!(
921 c_major
922 .transpose(&Interval::from_name("P5").unwrap())
923 .unwrap()
924 .tonic()
925 .name_with_octave(),
926 "G4"
927 );
928 assert_eq!(
929 names(&c_major.chord().unwrap().pitches()),
930 ["C4", "D4", "E4", "F4", "G4", "A4", "B4", "C5"]
931 );
932 assert_eq!(
933 names(
934 &c_major
935 .pitches_from_scale_degrees(&[1, 3, 5, 8, 10])
936 .unwrap()
937 ),
938 ["C4", "E4", "G4", "C5"]
939 );
940 assert_eq!(
941 c_major.interval_between_degrees(1, 5).unwrap().short_name(),
942 "P5"
943 );
944 assert_eq!(
945 c_major.interval_between_degrees(3, 7).unwrap().short_name(),
946 "P5"
947 );
948 assert_eq!(
949 c_major
950 .interval_between_degrees(5, 2)
951 .unwrap()
952 .directed_name(),
953 "P-4"
954 );
955 assert_eq!(
956 c_major.interval_between_degrees(2, 9).unwrap().short_name(),
957 "P1"
958 );
959
960 assert!(c_major.is_next(&pitch("D4"), &pitch("C4"), 1).unwrap());
961 assert!(c_major.is_next(&pitch("D5"), &pitch("C4"), 1).unwrap());
962 assert!(!c_major.is_next(&pitch("E4"), &pitch("C4"), 1).unwrap());
963 assert!(c_major.is_next(&pitch("E4"), &pitch("C4"), 2).unwrap());
964
965 let (matched, unmatched) = c_major
966 .match_pitches(&["C4", "E4", "G-4", "B-5", "A"].map(pitch))
967 .unwrap();
968 assert_eq!(names(&matched), ["C4", "E4", "A4"]);
969 assert_eq!(names(&unmatched), ["G-4", "B-5"]);
970
971 assert_eq!(
972 names(
973 &c_major
974 .find_missing(&["C4", "E4", "G4"].map(pitch))
975 .unwrap()
976 ),
977 ["D4", "F4", "A4", "B4"]
978 );
979 let a_minor = Scale::new(ScaleType::Minor, pitch("A"));
980 assert_eq!(
981 names(
982 &a_minor
983 .find_missing(&["A", "B", "C", "E"].map(pitch))
984 .unwrap()
985 ),
986 ["D5", "F5", "G5"]
987 );
988 assert!(
989 c_major
990 .find_missing(&["C4", "D4", "E4", "F4", "G4", "A4", "B4"].map(pitch))
991 .unwrap()
992 .is_empty()
993 );
994 }
995
996 #[test]
997 fn solfeg_matches_music21() {
998 let c_major = Scale::new(ScaleType::Major, Pitch::from_name("C4").unwrap());
999 let cases = [
1000 ("C4", "do", "do", "do"),
1001 ("C#4", "di", "di", "do"),
1002 ("D-4", "ra", "ra", "re"),
1003 ("E-4", "me", "me", "mi"),
1004 ("F#4", "fi", "fi", "fa"),
1005 ("G-4", "se", "se", "sol"),
1006 ("G4", "sol", "so", "sol"),
1007 ("A-4", "le", "le", "la"),
1008 ("B-4", "te", "te", "ti"),
1009 ("C-4", "de", "de", "do"),
1010 ("B#4", "tis", "ty", "ti"),
1011 ("F##4", "fis", "fis", "fa"),
1012 ("E#4", "mis", "my", "mi"),
1013 ("D##4", "ris", "ris", "re"),
1014 ];
1015 for (name, music21, humdrum, plain) in cases {
1016 let pitch = Pitch::from_name(name).unwrap();
1017 assert_eq!(
1018 c_major
1019 .solfeg(&pitch, SolfegVariant::Music21, true)
1020 .unwrap(),
1021 music21,
1022 "{name}"
1023 );
1024 assert_eq!(
1025 c_major
1026 .solfeg(&pitch, SolfegVariant::Humdrum, true)
1027 .unwrap(),
1028 humdrum,
1029 "{name}"
1030 );
1031 assert_eq!(
1032 c_major
1033 .solfeg(&pitch, SolfegVariant::Music21, false)
1034 .unwrap(),
1035 plain,
1036 "{name}"
1037 );
1038 }
1039 let chromatic = Scale::new(ScaleType::Chromatic, Pitch::from_name("C4").unwrap());
1040 assert_eq!(
1041 chromatic
1042 .solfeg(
1043 &Pitch::from_name("D4").unwrap(),
1044 SolfegVariant::Music21,
1045 true
1046 )
1047 .unwrap(),
1048 "mi"
1049 );
1050 assert!(
1051 chromatic
1052 .solfeg(
1053 &Pitch::from_name("B4").unwrap(),
1054 SolfegVariant::Music21,
1055 true
1056 )
1057 .is_err()
1058 );
1059 assert!(
1060 c_major
1061 .solfeg(
1062 &Pitch::from_name("C###4").unwrap(),
1063 SolfegVariant::Music21,
1064 true
1065 )
1066 .is_err()
1067 );
1068 }
1069 use super::*;
1070
1071 #[test]
1072 fn degrees_and_neighbouring_pitches_match_music21() {
1073 let scale = Scale::new(ScaleType::Major, Pitch::from_name("G4").unwrap());
1074 let degree = |name: &str| scale.degree_of(&Pitch::from_name(name).unwrap()).unwrap();
1075 assert_eq!(degree("G4"), Some(1));
1076 assert_eq!(degree("A4"), Some(2));
1077 assert_eq!(degree("F#5"), Some(7));
1078 assert_eq!(degree("B"), Some(3));
1079 assert_eq!(degree("B-4"), None);
1080 assert_eq!(degree("E3"), Some(6));
1081 assert_eq!(
1082 scale
1083 .degree_of_pitch_class(&Pitch::from_name("G-4").unwrap())
1084 .unwrap(),
1085 Some(7)
1086 );
1087
1088 let cases = [
1089 ("G4", true, 1, "A4"),
1090 ("G4", false, 1, "F#4"),
1091 ("F#4", true, 1, "G4"),
1092 ("B4", true, 3, "E5"),
1093 ("G4", false, 2, "E4"),
1094 ("E-4", true, 1, "E4"),
1095 ("E-4", false, 1, "D4"),
1096 ("B-4", false, 1, "A4"),
1097 ("G4", true, 8, "A5"),
1098 ("D5", true, 7, "D6"),
1099 ("G", true, 1, "A"),
1100 ("F#5", true, 1, "G5"),
1101 ("C3", false, 4, "F#2"),
1102 ];
1103 for (origin, ascending, steps, expected) in cases {
1104 let origin_pitch = Pitch::from_name(origin).unwrap();
1105 let next = if ascending {
1106 scale.next_pitch_above(&origin_pitch, steps)
1107 } else {
1108 scale.next_pitch_below(&origin_pitch, steps)
1109 };
1110 assert_eq!(
1111 next.unwrap().name_with_octave(),
1112 expected,
1113 "{origin} {ascending} {steps}"
1114 );
1115 }
1116 assert!(
1117 scale
1118 .next_pitch_above(&Pitch::from_name("G4").unwrap(), 0)
1119 .is_err()
1120 );
1121 }
1122
1123 #[test]
1124 fn derivation_matches_music21() {
1125 let pitches = |names: &[&str]| {
1126 names
1127 .iter()
1128 .map(|name| Pitch::from_name(*name).unwrap())
1129 .collect::<Vec<_>>()
1130 };
1131 type Row = (
1132 &'static [&'static str],
1133 ScaleType,
1134 &'static str,
1135 [(&'static str, usize); 4],
1136 &'static [&'static str],
1137 );
1138 let cases: [Row; 12] = [
1139 (
1140 &["C", "E", "G"],
1141 ScaleType::Major,
1142 "G",
1143 [("G", 3), ("F", 3), ("C", 3), ("B-", 2)],
1144 &["G", "F", "C"],
1145 ),
1146 (
1147 &["C", "E", "G"],
1148 ScaleType::Minor,
1149 "A",
1150 [("A", 3), ("E", 3), ("D", 3), ("B", 2)],
1151 &["A", "E", "D"],
1152 ),
1153 (
1154 &["C", "E", "G"],
1155 ScaleType::Dorian,
1156 "A",
1157 [("A", 3), ("G", 3), ("D", 3), ("B-", 2)],
1158 &["A", "G", "D"],
1159 ),
1160 (
1161 &["F#", "A", "C#", "E"],
1162 ScaleType::Major,
1163 "A",
1164 [("A", 4), ("E", 4), ("D", 4), ("B", 3)],
1165 &["A", "E", "D"],
1166 ),
1167 (
1168 &["F#", "A", "C#", "E"],
1169 ScaleType::Minor,
1170 "B",
1171 [("B", 4), ("F#", 4), ("D-", 4), ("C#", 4)],
1172 &["B", "F#", "D-", "C#", "C-"],
1173 ),
1174 (
1175 &["G#", "B", "D", "F"],
1176 ScaleType::HarmonicMinor,
1177 "A",
1178 [("A", 4), ("F#", 4), ("E-", 4), ("D#", 4)],
1179 &["A", "F#", "E-", "D#", "C"],
1180 ),
1181 (
1182 &["B-", "D", "F", "A-"],
1183 ScaleType::Major,
1184 "E-",
1185 [("E-", 4), ("D#", 4), ("B-", 3), ("G#", 3)],
1186 &["E-", "D#"],
1187 ),
1188 (
1189 &["B-", "D", "F", "A-"],
1190 ScaleType::Dorian,
1191 "F",
1192 [("F", 4), ("B-", 3), ("G#", 3), ("G", 3)],
1193 &["F"],
1194 ),
1195 (
1196 &["C", "D", "E", "F#", "G", "A", "B"],
1197 ScaleType::Major,
1198 "G",
1199 [("G", 7), ("D", 6), ("C", 6), ("A", 5)],
1200 &["G"],
1201 ),
1202 (
1203 &["C", "D", "E", "F#", "G", "A", "B"],
1204 ScaleType::Minor,
1205 "E",
1206 [("E", 7), ("B", 6), ("A", 6), ("C-", 6)],
1207 &["E"],
1208 ),
1209 (
1210 &["C", "C#", "D"],
1211 ScaleType::Major,
1212 "B-",
1213 [("B-", 2), ("A", 2), ("G#", 2), ("G", 2)],
1214 &[],
1215 ),
1216 (
1217 &["E-", "G", "B-"],
1218 ScaleType::Major,
1219 "B-",
1220 [("B-", 3), ("G#", 3), ("E-", 3), ("D#", 3)],
1221 &["B-", "G#", "E-", "D#"],
1222 ),
1223 ];
1224 for (names, scale_type, best, ranked, all) in cases {
1225 let input = pitches(names);
1226 assert_eq!(
1227 scale_type.derive(&input).unwrap().tonic().name(),
1228 best,
1229 "{scale_type:?} {names:?}"
1230 );
1231 let top = scale_type
1232 .derive_ranked(&input, Some(4))
1233 .unwrap()
1234 .into_iter()
1235 .map(|(matched, scale)| (scale.tonic().name(), matched))
1236 .collect::<Vec<_>>();
1237 let expected = ranked
1238 .iter()
1239 .map(|(tonic, matched)| (tonic.to_string(), *matched))
1240 .collect::<Vec<_>>();
1241 assert_eq!(top, expected, "{scale_type:?} {names:?}");
1242 let complete = scale_type
1243 .derive_all(&input)
1244 .unwrap()
1245 .iter()
1246 .map(|scale| scale.tonic().name())
1247 .collect::<Vec<_>>();
1248 assert_eq!(complete, all, "{scale_type:?} {names:?}");
1249 }
1250 }
1251
1252 #[test]
1253 fn degree_with_accidental_matches_music21() {
1254 let scale = Scale::new(ScaleType::Major, Pitch::from_name("C4").unwrap());
1255 let cases = [
1256 ("C4", 1, None),
1257 ("D4", 2, None),
1258 ("E-4", 3, Some("flat")),
1259 ("F#4", 4, Some("sharp")),
1260 ("G", 5, None),
1261 ("A#", 6, Some("sharp")),
1262 ("B-", 7, Some("flat")),
1263 ("D--4", 2, Some("double-flat")),
1264 ("C#4", 1, Some("sharp")),
1265 ("E#4", 3, Some("sharp")),
1266 ];
1267 for (name, degree, accidental) in cases {
1268 let (found, found_accidental) = scale
1269 .degree_and_accidental_of(&Pitch::from_name(name).unwrap())
1270 .unwrap();
1271 assert_eq!(found, degree, "{name}");
1272 assert_eq!(
1273 found_accidental
1274 .as_ref()
1275 .map(|accidental| accidental.name()),
1276 accidental,
1277 "{name}"
1278 );
1279 }
1280 }
1281
1282 fn names(scale_type: ScaleType, tonic: &str) -> Vec<String> {
1283 Scale::new(scale_type, Pitch::from_name(tonic).expect("valid tonic"))
1284 .pitches()
1285 .expect("scale realizes")
1286 .iter()
1287 .map(|pitch| pitch.name())
1288 .collect()
1289 }
1290
1291 #[test]
1292 fn realizes_the_church_modes_on_c() {
1293 assert_eq!(
1294 names(ScaleType::Major, "C4"),
1295 ["C", "D", "E", "F", "G", "A", "B", "C"]
1296 );
1297 assert_eq!(
1298 names(ScaleType::Minor, "C4"),
1299 ["C", "D", "E-", "F", "G", "A-", "B-", "C"]
1300 );
1301 assert_eq!(
1302 names(ScaleType::Dorian, "C4"),
1303 ["C", "D", "E-", "F", "G", "A", "B-", "C"]
1304 );
1305 assert_eq!(
1306 names(ScaleType::Phrygian, "C4"),
1307 ["C", "D-", "E-", "F", "G", "A-", "B-", "C"]
1308 );
1309 assert_eq!(
1310 names(ScaleType::Lydian, "C4"),
1311 ["C", "D", "E", "F#", "G", "A", "B", "C"]
1312 );
1313 assert_eq!(
1314 names(ScaleType::Mixolydian, "C4"),
1315 ["C", "D", "E", "F", "G", "A", "B-", "C"]
1316 );
1317 assert_eq!(
1318 names(ScaleType::Locrian, "C4"),
1319 ["C", "D-", "E-", "F", "G-", "A-", "B-", "C"]
1320 );
1321 }
1322
1323 #[test]
1324 fn realizes_the_altered_minors() {
1325 assert_eq!(
1326 names(ScaleType::HarmonicMinor, "C4"),
1327 ["C", "D", "E-", "F", "G", "A-", "B", "C"]
1328 );
1329 assert_eq!(
1330 names(ScaleType::MelodicMinor, "C4"),
1331 ["C", "D", "E-", "F", "G", "A", "B", "C"]
1332 );
1333 }
1334
1335 #[test]
1336 fn plagal_modes_share_their_authentic_pitches() {
1337 for (plagal, authentic) in [
1338 (ScaleType::Hypodorian, ScaleType::Dorian),
1339 (ScaleType::Hypophrygian, ScaleType::Phrygian),
1340 (ScaleType::Hypolydian, ScaleType::Lydian),
1341 (ScaleType::Hypomixolydian, ScaleType::Mixolydian),
1342 (ScaleType::Hypolocrian, ScaleType::Locrian),
1343 (ScaleType::Hypoaeolian, ScaleType::Minor),
1344 ] {
1345 let mut plagal_names = names(plagal, "C4");
1346 plagal_names.sort();
1347 plagal_names.dedup();
1348 let mut authentic_names = names(authentic, "C4");
1349 authentic_names.sort();
1350 authentic_names.dedup();
1351 assert_eq!(
1352 plagal_names, authentic_names,
1353 "{plagal:?} should share {authentic:?}'s pitch collection"
1354 );
1355 }
1356 }
1357
1358 #[test]
1359 fn a_plagal_mode_is_realized_a_fourth_below_its_final() {
1360 let scale = Scale::new(
1363 ScaleType::Hypodorian,
1364 Pitch::from_name("d").expect("valid tonic"),
1365 );
1366 let realized: Vec<String> = scale
1367 .pitches()
1368 .expect("scale realizes")
1369 .iter()
1370 .map(Pitch::name_with_octave)
1371 .collect();
1372 assert_eq!(
1373 realized,
1374 ["A3", "B3", "C4", "D4", "E4", "F4", "G4", "A4"],
1375 "a plagal mode starts a fourth below its final"
1376 );
1377 assert_eq!(scale.final_pitch().unwrap().name_with_octave(), "D4");
1378 assert_eq!(scale.dominant().unwrap().name_with_octave(), "F4");
1379
1380 let dorian = Scale::new(
1383 ScaleType::Dorian,
1384 Pitch::from_name("d").expect("valid tonic"),
1385 );
1386 assert_eq!(dorian.final_pitch().unwrap().name_with_octave(), "D4");
1387 assert_eq!(dorian.dominant().unwrap().name_with_octave(), "A4");
1388 }
1389
1390 #[test]
1391 fn a_leading_tone_is_a_semitone_below_the_final() {
1392 let minor = Scale::new(
1394 ScaleType::Minor,
1395 Pitch::from_name("c").expect("valid tonic"),
1396 );
1397 assert_eq!(minor.pitch_at_degree(7).unwrap().name(), "B-");
1398 assert_eq!(minor.leading_tone().unwrap().name(), "B");
1399 let major = Scale::new(
1401 ScaleType::Major,
1402 Pitch::from_name("C").expect("valid tonic"),
1403 );
1404 assert_eq!(major.leading_tone().unwrap().name_with_octave(), "B4");
1405 }
1406
1407 #[test]
1408 fn whole_tone_spells_upward_until_accidentals_run_out() {
1409 assert_eq!(
1411 names(ScaleType::WholeTone, "C4"),
1412 ["C", "D", "E", "F#", "G#", "A#", "B#"]
1413 );
1414 assert_eq!(
1416 names(ScaleType::WholeTone, "B4"),
1417 ["B", "C#", "D#", "E#", "G", "A", "B"]
1418 );
1419 }
1420
1421 #[test]
1422 fn octatonic_alternates_tone_and_semitone() {
1423 assert_eq!(
1424 names(ScaleType::Octatonic, "C4"),
1425 ["C", "D", "E-", "F", "G-", "A-", "A", "B", "C"]
1426 );
1427 assert_eq!(
1428 names(ScaleType::Octatonic, "G4"),
1429 ["G", "A", "B-", "C", "D-", "E-", "F-", "G-", "G"]
1430 );
1431 }
1432
1433 #[test]
1434 fn chromatic_uses_the_most_common_spelling() {
1435 assert_eq!(
1436 names(ScaleType::Chromatic, "C4"),
1437 [
1438 "C", "C#", "D", "E-", "E", "F", "F#", "G", "A-", "A", "B-", "B", "C"
1439 ]
1440 );
1441 }
1442
1443 #[test]
1444 fn rag_asawari_is_pentatonic() {
1445 assert_eq!(
1446 names(ScaleType::RagAsawari, "C4"),
1447 ["C", "D", "F", "G", "A-", "C"]
1448 );
1449 }
1450
1451 #[test]
1452 fn rag_marwa_dips_below_its_sixth_degree() {
1453 assert_eq!(
1458 names(ScaleType::RagMarwa, "C4"),
1459 ["C", "D-", "E", "F#", "A", "B", "A", "C", "D-"]
1460 );
1461 assert_eq!(
1462 names(ScaleType::RagMarwa, "E-4"),
1463 ["E-", "F-", "G", "A", "C", "D", "C", "E-", "F-"]
1464 );
1465 }
1466
1467 #[test]
1468 fn simplifying_scales_never_exceed_their_accidental_budget() {
1469 for scale_type in [ScaleType::WholeTone, ScaleType::Octatonic] {
1470 for tonic in ["C4", "G4", "D4", "A4", "E4", "B4", "F#4", "E-4", "G-4"] {
1471 let pitches = Scale::new(scale_type, Pitch::from_name(tonic).unwrap())
1472 .pitches()
1473 .unwrap();
1474 assert!(
1475 max_alter(&pitches) <= 1,
1476 "{scale_type:?} on {tonic} exceeded one accidental"
1477 );
1478 }
1479 }
1480 }
1481
1482 #[test]
1483 fn a_note_a_scale_stands_twice_has_two_notes_below_it() {
1484 let marwa = Scale::new(ScaleType::RagMarwa, Pitch::from_name("C4").unwrap()).descending();
1488 let from = Pitch::from_name("D-2").unwrap();
1489 assert_eq!(marwa.places_of(&from).unwrap(), 2);
1490 let below: Vec<String> = (0..2)
1491 .map(|place| {
1492 marwa
1493 .next_pitch_below_from(&from, 1, place)
1494 .unwrap()
1495 .name_with_octave()
1496 })
1497 .collect();
1498 assert_eq!(below, ["B1", "C2"]);
1499
1500 let major = Scale::new(ScaleType::Major, Pitch::from_name("C4").unwrap());
1503 let d = Pitch::from_name("D4").unwrap();
1504 assert_eq!(major.places_of(&d).unwrap(), 1);
1505 assert_eq!(
1506 major
1507 .next_pitch_below_from(&d, 1, 7)
1508 .unwrap()
1509 .name_with_octave(),
1510 "C4"
1511 );
1512 }
1513
1514 #[test]
1515 fn degree_lookup_matches_the_realized_pitches() {
1516 for scale_type in ScaleType::ALL {
1517 let scale = Scale::new(scale_type, Pitch::from_name("E-4").unwrap());
1518 let pitches = scale.pitches().unwrap();
1519 let degrees = scale
1523 .named_degrees()
1524 .unwrap_or_else(|| (1..=scale.degree_count() as IntegerType).collect());
1525 for (expected, degree) in pitches.iter().zip(degrees) {
1526 let actual = scale.pitch_at_degree(degree).unwrap();
1527 assert_eq!(
1528 actual.name_with_octave(),
1529 expected.name_with_octave(),
1530 "{scale_type:?} degree {degree}"
1531 );
1532 let found = scale.degree_of(expected).unwrap().unwrap();
1536 assert_eq!(
1537 scale
1538 .pitch_at_degree(found as IntegerType)
1539 .unwrap()
1540 .name_with_octave(),
1541 expected.name_with_octave(),
1542 "{scale_type:?} degree {degree} read back as {found}"
1543 );
1544 }
1545 if let Some(named) = scale.named_degrees() {
1548 for degree in 1..=*named.last().unwrap() {
1549 assert_eq!(
1550 scale.pitch_on_degree(degree).unwrap().is_some(),
1551 named.contains(°ree),
1552 "{scale_type:?} degree {degree}"
1553 );
1554 }
1555 }
1556 }
1557 }
1558
1559 #[test]
1560 fn a_degree_outside_the_scale_counts_round_it() {
1561 let scale = Scale::new(ScaleType::Phrygian, Pitch::from_name("G4").unwrap());
1563 for (degree, expected) in [
1564 (-1, "E-5"),
1565 (0, "F5"),
1566 (1, "G4"),
1567 (7, "F5"),
1568 (8, "G4"),
1569 (11, "C5"),
1570 (15, "G4"),
1571 ] {
1572 assert_eq!(
1573 scale.pitch_at_degree(degree).unwrap().name_with_octave(),
1574 expected,
1575 "degree {degree}"
1576 );
1577 }
1578 }
1579
1580 #[test]
1581 fn every_scale_realizes_on_every_common_tonic() {
1582 for scale_type in ScaleType::ALL {
1583 for tonic in [
1584 "C4", "G4", "D4", "A4", "E4", "B4", "F#4", "F4", "B-4", "E-4", "A-4",
1585 ] {
1586 let scale = Scale::new(scale_type, Pitch::from_name(tonic).unwrap());
1587 let pitches = scale.pitches().expect("scale realizes");
1588 let beyond = usize::from(scale_type.beyond_terminus().is_some());
1589 assert_eq!(
1590 pitches.len(),
1591 scale_type.degree_count() + 1 + beyond,
1592 "{scale_type:?} on {tonic}"
1593 );
1594 }
1595 }
1596 }
1597
1598 #[test]
1599 fn music21_names_are_distinct() {
1600 let mut names: Vec<&str> = ScaleType::ALL.iter().map(|s| s.music21_name()).collect();
1601 names.sort_unstable();
1602 let count = names.len();
1603 names.dedup();
1604 assert_eq!(names.len(), count, "music21 names must be unique");
1605 }
1606}