Skip to main content

music21_rs/scale/
scaletype.rs

1//! The named scales music21 exposes as `ConcreteScale` subclasses.
2//!
3//! Each scale is a sequence of step intervals walked from the tonic (upward,
4//! except for the one descending step Rag Marwa's network contains),
5//! matching the edges of music21's `IntervalNetwork`, plus the pitch
6//! simplification that network applies. Both together are needed: the steps
7//! alone give the right pitch classes but the wrong spelling for scales that
8//! run out of reasonable accidentals, which is why a C whole-tone scale ends
9//! `A#` rather than `B-` but a B whole-tone scale does not end `A##`.
10
11use 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/// How a scale respells pitches that would otherwise pile up accidentals.
21///
22/// Mirrors music21's `IntervalNetwork.pitchSimplification`.
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub(super) enum Simplification {
25    /// Spell literally, however many accidentals that takes.
26    Exact,
27    /// Cap at one accidental, respelling anything beyond it.
28    MaxAccidental,
29    /// Respell to the most common spelling of the pitch class.
30    MostCommon,
31}
32
33/// The step intervals the scale tables use, parsed once.
34///
35/// A scale given by its notes rather than by a name can step by anything, so
36/// this is a fast path and not the whole world: a step it does not hold is
37/// parsed on the spot.
38static 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/// Which set of solfège syllables [`Scale::solfeg`] uses.
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
58#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
59pub enum SolfegVariant {
60    /// music21's own table.
61    Music21,
62    /// The Humdrum spellings, which differ in `my`, `so` and `ty`.
63    Humdrum,
64}
65
66/// music21's `_solfegSyllables`: for each of the seven degrees, the syllable
67/// at alterations of -2, -1, 0, +1 and +2.
68pub 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
78/// music21's `_humdrumSolfegSyllables`, laid out like [`SOLFEG_SYLLABLES`].
79pub 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/// A named scale from music21's scale module.
90///
91/// Ordered as music21 defines them: the seven church modes, their plagal
92/// counterparts, the altered minors, then the symmetrical and non-Western
93/// scales.
94#[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 (Ionian).
100    Major,
101    /// Natural minor (Aeolian).
102    Minor,
103    /// Dorian mode.
104    Dorian,
105    /// Phrygian mode.
106    Phrygian,
107    /// Lydian mode.
108    Lydian,
109    /// Mixolydian mode.
110    Mixolydian,
111    /// Locrian mode.
112    Locrian,
113    /// Hypodorian mode. Shares Dorian's pitches; the ambitus differs.
114    Hypodorian,
115    /// Hypophrygian mode. Shares Phrygian's pitches.
116    Hypophrygian,
117    /// Hypolydian mode. Shares Lydian's pitches.
118    Hypolydian,
119    /// Hypomixolydian mode. Shares Mixolydian's pitches.
120    Hypomixolydian,
121    /// Hypolocrian mode. Shares Locrian's pitches.
122    Hypolocrian,
123    /// Hypoaeolian mode. Shares natural minor's pitches.
124    Hypoaeolian,
125    /// Harmonic minor, with a raised seventh.
126    HarmonicMinor,
127    /// Ascending melodic minor.
128    MelodicMinor,
129    /// Twelve-tone chromatic scale.
130    Chromatic,
131    /// Six-tone whole-tone scale.
132    WholeTone,
133    /// Eight-tone octatonic scale, alternating tone and semitone.
134    Octatonic,
135    /// Rag Asawari, as a five-tone ascending scale.
136    RagAsawari,
137    /// Rag Marwa, as a seven-step ascending scale.
138    ///
139    /// Not monotonic: music21's ascending network dips back down a major
140    /// second from the sixth degree before rising a minor third to the octave,
141    /// so the realized pitches repeat a note and briefly descend.
142    RagMarwa,
143}
144
145impl ScaleType {
146    /// Every scale type, in declaration order.
147    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    /// Returns the music21 class name for this scale.
171    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    /// The words music21 puts after the tonic when it names a scale:
197    /// `"C major"`, `"C harmonic minor"`, `"C Rag Asawari"`.
198    ///
199    /// The capitalisation is upstream's and is not consistent — the modes are
200    /// lower case, the others are not — which is why this is a table rather
201    /// than something derived from the class name.
202    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    /// The scale type music21 calls by this class name, such as
228    /// `"MajorScale"`.
229    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    /// Which degree of the realized scale is its final: music21's
236    /// `tonicDegree`.
237    ///
238    /// A plagal mode — the six that music21 prefixes `Hypo` — runs from a
239    /// fourth below its final to a fifth above it, so the note the music
240    /// comes to rest on is its fourth degree and not its first. Every other
241    /// scale here starts on its own tonic.
242    pub fn tonic_degree(self) -> usize {
243        if self.is_plagal() { 4 } else { 1 }
244    }
245
246    /// Which degree is the reciting tone: music21's `dominantDegree`.
247    ///
248    /// A fifth above the final in the authentic modes. The plagal ones took
249    /// theirs a third above the authentic dominant and then moved it off any
250    /// B, which is why hypophrygian and hypomixolydian differ from the rest.
251    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    /// The steps walked from the pitch the scale is *realized* from, which
260    /// for a plagal mode is not its final.
261    ///
262    /// `steps` is the collection written from the final, which is
263    /// how every one of these scales is named. A plagal mode is realized
264    /// from a fourth below that, so its walk starts three steps earlier in
265    /// the same cycle — the rotation that puts the final at the degree
266    /// [`Self::tonic_degree`] names.
267    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    /// The step a scale carries on past its own octave, where it has one.
279    ///
280    /// Rag Marwa is the only one here: its network keeps going a semitone
281    /// above the terminus, so the collection realized from `C` closes on the
282    /// octave and then sounds the `D-` above it. music21 calls it "a pitch
283    /// beyond the terminus" and gives it whenever the realization is not
284    /// bounded by a range.
285    pub fn beyond_terminus(self) -> Option<&'static str> {
286        match self {
287            Self::RagMarwa => Some("m2"),
288            _ => None,
289        }
290    }
291
292    /// The collection this scale uses coming down, where that is not the
293    /// one it uses going up.
294    ///
295    /// The melodic minor is the familiar case — it raises its sixth and
296    /// seventh degrees ascending and lets them fall descending, which is the
297    /// natural minor — and Rag Asawari's avaroha is that same collection.
298    /// Every other scale here descends through the notes it ascends
299    /// through, and answers `None`.
300    pub fn descending_form(self) -> Option<Self> {
301        match self {
302            Self::MelodicMinor | Self::RagAsawari => Some(Self::Minor),
303            _ => None,
304        }
305    }
306
307    /// The degrees the notes of this scale's ascent stand on, where they are
308    /// not simply counted off one to a note.
309    ///
310    /// Rag Asawari's aroha leaves out the third and the seventh, so its five
311    /// notes stand on the first, second, fourth, fifth and sixth degrees. It
312    /// has no third going up at all — which is what music21 answers when
313    /// asked for one — and its fourth degree is its third note.
314    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    /// The steps this scale is walked by coming down, where coming down is
322    /// not simply the ascending pattern read backwards and no other named
323    /// scale is that pattern.
324    ///
325    /// Rag Marwa's avarohana is the one: from its tonic it rises a semitone
326    /// to the flat second *above* the octave and falls from there, which is
327    /// music21's network edge from the high terminus upward before anything
328    /// descends. Written as a rising list from the tonic it closes by falling
329    /// back onto it, so the flat second is both the second degree and the
330    /// seventh — which is what music21 answers when asked which degree a
331    /// `D-` is coming down.
332    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    /// Whether this is a plagal mode, whose range sits below its final.
340    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    /// Returns the step intervals walked from the tonic.
353    ///
354    /// Almost every scale ascends throughout; Rag Marwa is the exception, and
355    /// carries one descending step (`-M2`), matching its network edge.
356    ///
357    /// These are music21's `IntervalNetwork` edges, not the intervals between
358    /// the pitches it finally reports — the two differ wherever simplification
359    /// respells a degree.
360    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            // The sixth step is music21's `M-2` edge: a descending major
381            // second inside an otherwise ascending network.
382            Self::RagMarwa => &["m2", "a2", "M2", "m3", "M2", "-M2", "m3"],
383        }
384    }
385
386    /// Returns how this scale respells pitches, matching music21's network.
387    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    /// Returns the number of distinct degrees in one octave.
396    pub fn degree_count(self) -> usize {
397        self.steps().len()
398    }
399}
400
401/// The tonics music21's `IntervalNetwork.find` tries, in its order. Ties in
402/// the ranking fall back to this order reversed.
403pub(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    /// Ranks every candidate tonic by how many of `pitches` fall in this
409    /// scale type built on it, best first, as music21's `deriveRanked` does.
410    /// Pitches are compared by pitch class and duplicates count separately.
411    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    /// The same, saying how a pitch is matched against a scale degree.
420    ///
421    /// By pitch class an `E#` is in C major, since the scale has an `F`; by
422    /// name it is not. music21 offers both, and its `deriveRanked` defaults
423    /// to the first.
424    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    /// Returns this scale type on the tonic that fits `pitches` best.
461    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    /// Returns every tonic on which this scale type contains all of `pitches`,
469    /// best-ranked first.
470    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
480/// How many octaves of a scale [`Scale::pitches_between`] will walk, which
481/// is the whole of music21's pitch space and then some.
482pub(super) const MAX_RANGE_OCTAVES: usize = 12;
483
484/// How a pitch is matched against a scale degree: music21's
485/// `comparisonAttribute`.
486#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
487#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
488pub enum DegreeComparison {
489    /// By sounding note, so `E#` matches the `F` of C major.
490    PitchClass,
491    /// By written note, so it does not.
492    Name,
493    /// By letter alone, so an `E-` matches the `E` of C major.
494    Step,
495}
496
497impl DegreeComparison {
498    /// What two pitches have to share to count as the same degree.
499    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
508/// Transposes one scale step, applying the scale's simplification.
509pub(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/// Returns the maximum accidental count used by a scale, for tests.
530#[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        // Rag Marwa names its A twice, as the fifth degree and the seventh.
579        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        // A pitch off the scale comes onto it first: below C#4 in C major
647        // is B3 from the lower neighbour and C4 from the upper.
648        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        // Rag Marwa stands D- in two places, as the note above its tonic and
679        // the one it passes through coming down from the octave, and each
680        // has its own neighbour.
681        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    /// A scale carrying its own tuning retunes the notes it names, in their
724    /// own octaves and spellings, and leaves the notes it does not name
725    /// where they are.
726    #[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    /// music21's own `romanNumeral` example, and the Scala reading of a
795    /// major scale.
796    #[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        // music21's own example: a scale of four notes, which repeats at the
829        // octave like any other.
830        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        // The fifth degree of a four-note scale is the first again, in the
846        // octave the scale stands in — music21 reads a degree within the one
847        // octave its network holds.
848        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        // And it walks a range the way a named scale does.
855        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        // music21's own example: C major from E-flat 5 to G-flat 7 starts on
871        // E5, the first scale pitch that is not below the bottom.
872        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        // A range of exactly one octave has both ends in it.
884        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        // Asked the other way round, the same range comes back descending,
894        // which is what music21 does.
895        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        // music21's own example: the hypodorian on D runs A3 to A4, and the
1361        // note it comes to rest on is its fourth degree.
1362        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        // An authentic mode starts on its own final, and its dominant is the
1381        // fifth degree.
1382        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        // In a minor scale that is not the seventh degree the scale has.
1393        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        // In a major scale it already is.
1400        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        // From C the scale can stay on sharps and closes on B#, not C.
1410        assert_eq!(
1411            names(ScaleType::WholeTone, "C4"),
1412            ["C", "D", "E", "F#", "G#", "A#", "B#"]
1413        );
1414        // From B a literal spelling would need G##, so music21 respells.
1415        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        // music21's ascending network steps down a M2 from B before closing on
1454        // C, so A appears twice and the line is not monotonic — and then it
1455        // carries on a semitone past the octave, which is the one scale here
1456        // that ends above where it closed.
1457        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        // Rag Marwa comes down through the flat second twice over: once
1485        // above the octave and once above the tonic. music21 reads `D-2` as
1486        // either, and the note below it is `B1` or `C2` accordingly.
1487        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        // A note the scale stands only once is the same whichever place is
1501        // asked for, so a caller that always asks for the first is right.
1502        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            // Every degree but the closing octave, which is the first degree
1520            // again as far as a degree lookup is concerned. Asked by the
1521            // scale's own degrees, since Rag Asawari's are not its positions.
1522            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                // Read back the other way: the degree the note is found on
1533                // stands on that note. Not necessarily the degree asked for
1534                // — Rag Marwa's A is both its fifth and its seventh.
1535                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            // A degree the scale does not have is nothing, rather than the
1546            // note that would stand there had the degrees been counted off.
1547            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(&degree),
1552                        "{scale_type:?} degree {degree}"
1553                    );
1554                }
1555            }
1556        }
1557    }
1558
1559    #[test]
1560    fn a_degree_outside_the_scale_counts_round_it() {
1561        // music21's own answers for `scale.PhrygianScale('g').pitchFromDegree`.
1562        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}