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/// music21's `IntervalNetwork.pitchSimplification`.
22///
23/// Every named scale says which it uses. A scale given by its steps spells
24/// with at most one accidental unless it is told otherwise, which is what
25/// music21's interval network does; a Scala scale is told to take the most
26/// common spelling of each note.
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29pub enum Simplification {
30    /// Spell literally, however many accidentals that takes.
31    Exact,
32    /// Cap at one accidental, respelling anything beyond it.
33    MaxAccidental,
34    /// Respell to the most common spelling of the pitch class.
35    MostCommon,
36}
37
38impl Simplification {
39    /// The name music21 gives this rule on a network, or `None` for spelling
40    /// exactly, which music21 writes as `None`.
41    #[must_use]
42    pub fn music21_name(self) -> Option<&'static str> {
43        match self {
44            Self::Exact => None,
45            Self::MaxAccidental => Some("maxAccidental"),
46            Self::MostCommon => Some("mostCommon"),
47        }
48    }
49}
50
51/// The step intervals the scale tables use, parsed once.
52///
53/// A scale given by its notes rather than by a name can step by anything, so
54/// this is a fast path and not the whole world: a step it does not hold is
55/// parsed on the spot.
56static STEP_INTERVALS: LazyLock<HashMap<&'static str, Interval>> = LazyLock::new(|| {
57    ["m2", "M2", "a2", "m3", "M3", "-M2"]
58        .into_iter()
59        .map(|name| {
60            let interval =
61                Interval::from_name(name).expect("scale step intervals are valid interval names");
62            (name, interval)
63        })
64        .collect()
65});
66
67pub(super) fn step_interval(name: &str) -> Result<Interval> {
68    match STEP_INTERVALS.get(name) {
69        Some(interval) => Ok(interval.clone()),
70        None => Interval::from_name(name),
71    }
72}
73
74/// Which set of solfège syllables [`Scale::solfeg`] uses.
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
76#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
77pub enum SolfegVariant {
78    /// music21's own table.
79    Music21,
80    /// The Humdrum spellings, which differ in `my`, `so` and `ty`.
81    Humdrum,
82}
83
84/// music21's `_solfegSyllables`: for each of the seven degrees, the syllable
85/// at alterations of -2, -1, 0, +1 and +2.
86pub const SOLFEG_SYLLABLES: [[&str; 5]; 7] = [
87    ["def", "de", "do", "di", "dis"],
88    ["raf", "ra", "re", "ri", "ris"],
89    ["mef", "me", "mi", "mis", "mish"],
90    ["fef", "fe", "fa", "fi", "fis"],
91    ["sef", "se", "sol", "si", "sis"],
92    ["lef", "le", "la", "li", "lis"],
93    ["tef", "te", "ti", "tis", "tish"],
94];
95
96/// music21's `_humdrumSolfegSyllables`, laid out like [`SOLFEG_SYLLABLES`].
97pub const HUMDRUM_SOLFEG_SYLLABLES: [[&str; 5]; 7] = [
98    ["def", "de", "do", "di", "dis"],
99    ["raf", "ra", "re", "ri", "ris"],
100    ["mef", "me", "mi", "my", "mish"],
101    ["fef", "fe", "fa", "fi", "fis"],
102    ["sef", "se", "so", "si", "sis"],
103    ["lef", "le", "la", "li", "lis"],
104    ["tef", "te", "ti", "ty", "tish"],
105];
106
107/// A named scale from music21's scale module.
108///
109/// Ordered as music21 defines them: the seven church modes, their plagal
110/// counterparts, the altered minors, then the symmetrical and non-Western
111/// scales.
112#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
113#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
114#[must_use]
115pub enum ScaleType {
116    /// Major (Ionian).
117    Major,
118    /// Natural minor (Aeolian).
119    Minor,
120    /// Dorian mode.
121    Dorian,
122    /// Phrygian mode.
123    Phrygian,
124    /// Lydian mode.
125    Lydian,
126    /// Mixolydian mode.
127    Mixolydian,
128    /// Locrian mode.
129    Locrian,
130    /// Hypodorian mode. Shares Dorian's pitches; the ambitus differs.
131    Hypodorian,
132    /// Hypophrygian mode. Shares Phrygian's pitches.
133    Hypophrygian,
134    /// Hypolydian mode. Shares Lydian's pitches.
135    Hypolydian,
136    /// Hypomixolydian mode. Shares Mixolydian's pitches.
137    Hypomixolydian,
138    /// Hypolocrian mode. Shares Locrian's pitches.
139    Hypolocrian,
140    /// Hypoaeolian mode. Shares natural minor's pitches.
141    Hypoaeolian,
142    /// Harmonic minor, with a raised seventh.
143    HarmonicMinor,
144    /// Ascending melodic minor.
145    MelodicMinor,
146    /// Twelve-tone chromatic scale.
147    Chromatic,
148    /// Six-tone whole-tone scale.
149    WholeTone,
150    /// Eight-tone octatonic scale, alternating tone and semitone.
151    Octatonic,
152    /// Rag Asawari, as a five-tone ascending scale.
153    RagAsawari,
154    /// Rag Marwa, as a seven-step ascending scale.
155    ///
156    /// Not monotonic: music21's ascending network dips back down a major
157    /// second from the sixth degree before rising a minor third to the octave,
158    /// so the realized pitches repeat a note and briefly descend.
159    RagMarwa,
160}
161
162impl ScaleType {
163    /// Every scale type, in declaration order.
164    pub const ALL: [ScaleType; 20] = [
165        Self::Major,
166        Self::Minor,
167        Self::Dorian,
168        Self::Phrygian,
169        Self::Lydian,
170        Self::Mixolydian,
171        Self::Locrian,
172        Self::Hypodorian,
173        Self::Hypophrygian,
174        Self::Hypolydian,
175        Self::Hypomixolydian,
176        Self::Hypolocrian,
177        Self::Hypoaeolian,
178        Self::HarmonicMinor,
179        Self::MelodicMinor,
180        Self::Chromatic,
181        Self::WholeTone,
182        Self::Octatonic,
183        Self::RagAsawari,
184        Self::RagMarwa,
185    ];
186
187    /// Returns the music21 class name for this scale.
188    pub fn music21_name(self) -> &'static str {
189        match self {
190            Self::Major => "MajorScale",
191            Self::Minor => "MinorScale",
192            Self::Dorian => "DorianScale",
193            Self::Phrygian => "PhrygianScale",
194            Self::Lydian => "LydianScale",
195            Self::Mixolydian => "MixolydianScale",
196            Self::Locrian => "LocrianScale",
197            Self::Hypodorian => "HypodorianScale",
198            Self::Hypophrygian => "HypophrygianScale",
199            Self::Hypolydian => "HypolydianScale",
200            Self::Hypomixolydian => "HypomixolydianScale",
201            Self::Hypolocrian => "HypolocrianScale",
202            Self::Hypoaeolian => "HypoaeolianScale",
203            Self::HarmonicMinor => "HarmonicMinorScale",
204            Self::MelodicMinor => "MelodicMinorScale",
205            Self::Chromatic => "ChromaticScale",
206            Self::WholeTone => "WholeToneScale",
207            Self::Octatonic => "OctatonicScale",
208            Self::RagAsawari => "RagAsawari",
209            Self::RagMarwa => "RagMarwa",
210        }
211    }
212
213    /// The words music21 puts after the tonic when it names a scale:
214    /// `"C major"`, `"C harmonic minor"`, `"C Rag Asawari"`.
215    ///
216    /// The capitalisation is upstream's and is not consistent — the modes are
217    /// lower case, the others are not — which is why this is a table rather
218    /// than something derived from the class name.
219    pub fn music21_descriptive_name(self) -> &'static str {
220        match self {
221            Self::Major => "major",
222            Self::Minor => "minor",
223            Self::Dorian => "dorian",
224            Self::Phrygian => "phrygian",
225            Self::Lydian => "lydian",
226            Self::Mixolydian => "mixolydian",
227            Self::Locrian => "locrian",
228            Self::Hypodorian => "hypodorian",
229            Self::Hypophrygian => "hypophrygian",
230            Self::Hypolydian => "hypolydian",
231            Self::Hypomixolydian => "hypomixolydian",
232            Self::Hypolocrian => "hypolocrian",
233            Self::Hypoaeolian => "hypoaeolian",
234            Self::HarmonicMinor => "harmonic minor",
235            Self::MelodicMinor => "melodic minor",
236            Self::Chromatic => "Chromatic",
237            Self::WholeTone => "Whole tone",
238            Self::Octatonic => "Octatonic",
239            Self::RagAsawari => "Rag Asawari",
240            Self::RagMarwa => "Rag Marwa",
241        }
242    }
243
244    /// The scale type music21 calls by this class name, such as
245    /// `"MajorScale"`.
246    pub fn from_music21_name(name: &str) -> Option<Self> {
247        Self::ALL
248            .into_iter()
249            .find(|scale_type| scale_type.music21_name() == name)
250    }
251
252    /// Which degree of the realized scale is its final: music21's
253    /// `tonicDegree`.
254    ///
255    /// A plagal mode — the six that music21 prefixes `Hypo` — runs from a
256    /// fourth below its final to a fifth above it, so the note the music
257    /// comes to rest on is its fourth degree and not its first. Every other
258    /// scale here starts on its own tonic.
259    pub fn tonic_degree(self) -> usize {
260        if self.is_plagal() { 4 } else { 1 }
261    }
262
263    /// Which degree is the reciting tone: music21's `dominantDegree`.
264    ///
265    /// A fifth above the final in the authentic modes. The plagal ones took
266    /// theirs a third above the authentic dominant and then moved it off any
267    /// B, which is why hypophrygian and hypomixolydian differ from the rest.
268    pub fn dominant_degree(self) -> usize {
269        match self {
270            Self::Hypophrygian | Self::Hypomixolydian => 7,
271            _ if self.is_plagal() => 6,
272            _ => 5,
273        }
274    }
275
276    /// The steps walked from the pitch the scale is *realized* from, which
277    /// for a plagal mode is not its final.
278    ///
279    /// `steps` is the collection written from the final, which is
280    /// how every one of these scales is named. A plagal mode is realized
281    /// from a fourth below that, so its walk starts three steps earlier in
282    /// the same cycle — the rotation that puts the final at the degree
283    /// [`Self::tonic_degree`] names.
284    pub fn realization_steps(self) -> Vec<&'static str> {
285        let steps = self.steps();
286        let count = steps.len();
287        let start = (count + 1 - self.tonic_degree()) % count;
288        steps[start..]
289            .iter()
290            .chain(&steps[..start])
291            .copied()
292            .collect()
293    }
294
295    /// The step a scale carries on past its own octave, where it has one.
296    ///
297    /// Rag Marwa is the only one here: its network keeps going a semitone
298    /// above the terminus, so the collection realized from `C` closes on the
299    /// octave and then sounds the `D-` above it. music21 calls it "a pitch
300    /// beyond the terminus" and gives it whenever the realization is not
301    /// bounded by a range.
302    pub fn beyond_terminus(self) -> Option<&'static str> {
303        match self {
304            Self::RagMarwa => Some("m2"),
305            _ => None,
306        }
307    }
308
309    /// The collection this scale uses coming down, where that is not the
310    /// one it uses going up.
311    ///
312    /// The melodic minor is the familiar case — it raises its sixth and
313    /// seventh degrees ascending and lets them fall descending, which is the
314    /// natural minor — and Rag Asawari's avaroha is that same collection.
315    /// Every other scale here descends through the notes it ascends
316    /// through, and answers `None`.
317    pub fn descending_form(self) -> Option<Self> {
318        match self {
319            Self::MelodicMinor | Self::RagAsawari => Some(Self::Minor),
320            _ => None,
321        }
322    }
323
324    /// The degrees the notes of this scale's ascent stand on, where they are
325    /// not simply counted off one to a note.
326    ///
327    /// Rag Asawari's aroha leaves out the third and the seventh, so its five
328    /// notes stand on the first, second, fourth, fifth and sixth degrees. It
329    /// has no third going up at all — which is what music21 answers when
330    /// asked for one — and its fourth degree is its third note.
331    pub fn ascending_degrees(self) -> Option<&'static [u8]> {
332        match self {
333            Self::RagAsawari => Some(&[1, 2, 4, 5, 6]),
334            _ => None,
335        }
336    }
337
338    /// The steps this scale is walked by coming down, where coming down is
339    /// not simply the ascending pattern read backwards and no other named
340    /// scale is that pattern.
341    ///
342    /// Rag Marwa's avarohana is the one: from its tonic it rises a semitone
343    /// to the flat second *above* the octave and falls from there, which is
344    /// music21's network edge from the high terminus upward before anything
345    /// descends. Written as a rising list from the tonic it closes by falling
346    /// back onto it, so the flat second is both the second degree and the
347    /// seventh — which is what music21 answers when asked which degree a
348    /// `D-` is coming down.
349    pub fn descending_steps(self) -> Option<&'static [&'static str]> {
350        match self {
351            Self::RagMarwa => Some(&["m2", "A2", "M2", "m3", "M2", "d3", "-m2"]),
352            _ => None,
353        }
354    }
355
356    /// Whether this is a plagal mode, whose range sits below its final.
357    pub fn is_plagal(self) -> bool {
358        matches!(
359            self,
360            Self::Hypodorian
361                | Self::Hypophrygian
362                | Self::Hypolydian
363                | Self::Hypomixolydian
364                | Self::Hypolocrian
365                | Self::Hypoaeolian
366        )
367    }
368
369    /// Returns the step intervals walked from the tonic.
370    ///
371    /// Almost every scale ascends throughout; Rag Marwa is the exception, and
372    /// carries one descending step (`-M2`), matching its network edge.
373    ///
374    /// These are music21's `IntervalNetwork` edges, not the intervals between
375    /// the pitches it finally reports — the two differ wherever simplification
376    /// respells a degree.
377    pub(super) fn steps(self) -> &'static [&'static str] {
378        match self {
379            Self::Major => &["M2", "M2", "m2", "M2", "M2", "M2", "m2"],
380            Self::Minor | Self::Hypoaeolian => &["M2", "m2", "M2", "M2", "m2", "M2", "M2"],
381            Self::Dorian => &["M2", "m2", "M2", "M2", "M2", "m2", "M2"],
382            Self::Phrygian => &["m2", "M2", "M2", "M2", "m2", "M2", "M2"],
383            Self::Lydian => &["M2", "M2", "M2", "m2", "M2", "M2", "m2"],
384            Self::Mixolydian => &["M2", "M2", "m2", "M2", "M2", "m2", "M2"],
385            Self::Locrian => &["m2", "M2", "M2", "m2", "M2", "M2", "M2"],
386            Self::Hypodorian => &["M2", "m2", "M2", "M2", "M2", "m2", "M2"],
387            Self::Hypophrygian => &["m2", "M2", "M2", "M2", "m2", "M2", "M2"],
388            Self::Hypolydian => &["M2", "M2", "M2", "m2", "M2", "M2", "m2"],
389            Self::Hypomixolydian => &["M2", "M2", "m2", "M2", "M2", "m2", "M2"],
390            Self::Hypolocrian => &["m2", "M2", "M2", "m2", "M2", "M2", "M2"],
391            Self::HarmonicMinor => &["M2", "m2", "M2", "M2", "m2", "a2", "m2"],
392            Self::MelodicMinor => &["M2", "m2", "M2", "M2", "M2", "M2", "m2"],
393            Self::Chromatic => &["m2"; 12],
394            Self::WholeTone => &["M2"; 6],
395            Self::Octatonic => &["M2", "m2", "M2", "m2", "M2", "m2", "M2", "m2"],
396            Self::RagAsawari => &["M2", "m3", "M2", "m2", "M3"],
397            // The sixth step is music21's `M-2` edge: a descending major
398            // second inside an otherwise ascending network.
399            Self::RagMarwa => &["m2", "a2", "M2", "m3", "M2", "-M2", "m3"],
400        }
401    }
402
403    /// Whether this scale is a cycle walked from its tonic both ways rather
404    /// than a pattern repeated at the octave, as music21 builds its whole-tone
405    /// and chromatic scales. The two spell differently wherever the steps
406    /// respell as they go: the whole-tone scale on B- comes down through `A-`,
407    /// `G-` and `F-`, not up to them through `G#`.
408    pub fn is_cycle(self) -> bool {
409        matches!(self, Self::WholeTone | Self::Chromatic)
410    }
411
412    /// Returns how this scale respells pitches, matching music21's network.
413    pub(super) fn simplification(self) -> Simplification {
414        match self {
415            Self::WholeTone | Self::Octatonic | Self::RagMarwa => Simplification::MaxAccidental,
416            Self::Chromatic | Self::RagAsawari => Simplification::MostCommon,
417            _ => Simplification::Exact,
418        }
419    }
420
421    /// Returns the number of distinct degrees in one octave.
422    pub fn degree_count(self) -> usize {
423        self.steps().len()
424    }
425}
426
427/// The tonics music21's `IntervalNetwork.find` tries, in its order. Ties in
428/// the ranking fall back to this order reversed.
429pub(super) const SCALE_STARTS: [&str; 15] = [
430    "C", "C#", "Db", "D", "D#", "Eb", "E", "F", "F#", "G", "G#", "A", "Bb", "B", "Cb",
431];
432
433impl ScaleType {
434    /// Ranks every candidate tonic by how many of `pitches` fall in this
435    /// scale type built on it, best first, as music21's `deriveRanked` does.
436    /// Pitches are compared by pitch class and duplicates count separately.
437    ///
438    /// Each candidate is realized between the lowest and the highest of
439    /// `pitches`, so what is compared is the scale where the pitches are: a
440    /// scale that comes down differently from how it goes up, or changes from
441    /// one octave to the next, is compared as it stands there.
442    pub fn derive_ranked(
443        self,
444        pitches: &[Pitch],
445        limit: Option<usize>,
446    ) -> Result<Vec<(usize, Scale)>> {
447        self.derive_ranked_by(pitches, limit, DegreeComparison::PitchClass)
448    }
449
450    /// The same, saying how a pitch is matched against a scale degree.
451    ///
452    /// By pitch class an `E#` is in C major, since the scale has an `F`; by
453    /// name it is not. music21 offers both, and its `deriveRanked` defaults
454    /// to the first.
455    pub fn derive_ranked_by(
456        self,
457        pitches: &[Pitch],
458        limit: Option<usize>,
459        comparison: DegreeComparison,
460    ) -> Result<Vec<(usize, Scale)>> {
461        rank_on_every_start(pitches, limit, comparison, |tonic| Scale::new(self, tonic))
462    }
463
464    /// Returns this scale type on the tonic that fits `pitches` best.
465    pub fn derive(self, pitches: &[Pitch]) -> Result<Scale> {
466        self.derive_ranked(pitches, Some(1))?
467            .pop()
468            .map(|(_, scale)| scale)
469            .ok_or_else(|| crate::error::Error::Scale("no candidate tonics".to_string()))
470    }
471
472    /// Returns every tonic on which this scale type contains all of `pitches`,
473    /// best-ranked first.
474    pub fn derive_all(self, pitches: &[Pitch]) -> Result<Vec<Scale>> {
475        Ok(self
476            .derive_ranked(pitches, None)?
477            .into_iter()
478            .filter(|(matched, _)| *matched == pitches.len())
479            .map(|(_, scale)| scale)
480            .collect())
481    }
482}
483
484/// `place` put on every tonic `SCALE_STARTS` names, each counted by how many
485/// of `pitches` it holds between their lowest and highest, best first:
486/// music21's `deriveRanked`, ties going to the higher tonic.
487pub(super) fn rank_on_every_start(
488    pitches: &[Pitch],
489    limit: Option<usize>,
490    comparison: DegreeComparison,
491    place: impl Fn(Pitch) -> Scale,
492) -> Result<Vec<(usize, Scale)>> {
493    let targets = pitches
494        .iter()
495        .map(|pitch| comparison.key(pitch))
496        .collect::<Vec<_>>();
497    let Some((lowest, highest)) = target_range(pitches) else {
498        return Ok(Vec::new());
499    };
500    let mut ranked = Vec::with_capacity(SCALE_STARTS.len());
501    for start in SCALE_STARTS {
502        let scale = place(Pitch::from_name(start)?);
503        let degrees = scale
504            .pitches_between(lowest, highest)?
505            .iter()
506            .map(|pitch| comparison.key(pitch))
507            .collect::<Vec<_>>();
508        let matched = targets
509            .iter()
510            .filter(|target| degrees.contains(target))
511            .count();
512        ranked.push((matched, scale));
513    }
514    ranked.sort_by(|left, right| {
515        left.0
516            .cmp(&right.0)
517            .then_with(|| left.1.tonic().ps().total_cmp(&right.1.tonic().ps()))
518    });
519    ranked.reverse();
520    if let Some(limit) = limit {
521        ranked.truncate(limit);
522    }
523    Ok(ranked)
524}
525
526/// The lowest and the highest of some pitches, which is the range music21's
527/// `filterPitchList` realizes a candidate scale over; `None` for no pitches.
528pub(super) fn target_range(pitches: &[Pitch]) -> Option<(&Pitch, &Pitch)> {
529    let lowest = pitches.iter().min_by(|a, b| a.ps().total_cmp(&b.ps()))?;
530    let highest = pitches.iter().max_by(|a, b| a.ps().total_cmp(&b.ps()))?;
531    Some((lowest, highest))
532}
533
534/// How many octaves of a scale [`Scale::pitches_between`] will walk, which
535/// is the whole of music21's pitch space and then some.
536pub(super) const MAX_RANGE_OCTAVES: usize = 12;
537
538/// How a pitch is matched against a scale degree: music21's
539/// `comparisonAttribute`.
540#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
541#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
542pub enum DegreeComparison {
543    /// By sounding note, so `E#` matches the `F` of C major.
544    PitchClass,
545    /// By written note, so it does not.
546    Name,
547    /// By letter alone, so an `E-` matches the `E` of C major.
548    Step,
549}
550
551impl DegreeComparison {
552    /// What two pitches have to share to count as the same degree.
553    pub(super) fn key(self, pitch: &Pitch) -> String {
554        match self {
555            Self::PitchClass => pitch.pitch_class().number().to_string(),
556            Self::Name => pitch.name(),
557            Self::Step => pitch.name().chars().take(1).collect(),
558        }
559    }
560}
561
562/// Transposes one scale step, applying the scale's simplification.
563pub(super) fn advance(
564    pitch: &Pitch,
565    interval: &Interval,
566    simplification: Simplification,
567) -> Result<Pitch> {
568    match simplification {
569        Simplification::MaxAccidental => {
570            interval.transpose_pitch_with_options(pitch, false, Some(1))
571        }
572        // music21 transposes with its default cap of four accidentals when it
573        // is not told to spell with one, and respells past that.
574        Simplification::Exact => interval.transpose_pitch_with_options(pitch, false, Some(4)),
575        Simplification::MostCommon => {
576            let mut transposed = interval.transpose_pitch_with_options(pitch, false, Some(4))?;
577            if transposed.accidental().alter() != 0.0 {
578                transposed.simplify_enharmonic_in_place(true)?;
579            }
580            Ok(transposed)
581        }
582    }
583}
584
585/// Returns the maximum accidental count used by a scale, for tests.
586#[cfg(test)]
587fn max_alter(pitches: &[Pitch]) -> crate::defaults::IntegerType {
588    pitches
589        .iter()
590        .map(|pitch| pitch.accidental().alter().abs() as crate::defaults::IntegerType)
591        .max()
592        .unwrap_or(0)
593}
594
595#[cfg(test)]
596mod tests {
597    use crate::defaults::{FloatType, IntegerType};
598    #[test]
599    fn a_scala_scale_is_a_cycle_walked_from_its_tonic() {
600        use super::Scale;
601        use crate::Pitch;
602        use crate::tuningsystem::scala::ScalaScale;
603
604        // music21: `scale.ScalaScale('c4', 'slendro').getPitches('c3', 'c5')`.
605        // Below the tonic the notes are reached coming down and spelled for
606        // it, so the fifth degree is B-3 forty cents flat under the tonic
607        // and A~4 ten cents sharp over it.
608        let slendro = ScalaScale::parse(
609            "! slendro.scl
610!
611Observed Javanese Slendro scale, Helmholtz/Ellis p. 518, nr.94
612 5
613!
614 228.00000
615 484.00000
616 728.00000
617 960.00000
618 2/1
619",
620        )
621        .unwrap();
622        let scale = Scale::from_scala(Pitch::from_name("C4").unwrap(), &slendro).unwrap();
623        let spelled: Vec<String> = scale
624            .pitches_between(
625                &Pitch::from_name("C3").unwrap(),
626                &Pitch::from_name("C5").unwrap(),
627            )
628            .unwrap()
629            .iter()
630            .map(|pitch| match pitch.microtone() {
631                Some(microtone) if microtone.cents() != 0.0 => {
632                    format!("{}{microtone}", pitch.name_with_octave())
633                }
634                _ => pitch.name_with_octave(),
635            })
636            .collect();
637        assert_eq!(
638            spelled,
639            [
640                "C3",
641                "D~3(-22c)",
642                "F3(-16c)",
643                "G~3(-22c)",
644                "Bb3(-40c)",
645                "C4",
646                "D~4(-22c)",
647                "F4(-16c)",
648                "G~4(-22c)",
649                "A~4(+10c)",
650                "C5"
651            ]
652        );
653        assert!(!scale.octave_duplicating());
654    }
655
656    #[test]
657    fn degrees_are_found_by_pitch_class_name_or_step() {
658        use super::{DegreeComparison, Scale, ScaleType};
659        use crate::Pitch;
660
661        let major = Scale::new(ScaleType::Major, Pitch::from_name("C").unwrap());
662        let e_sharp = Pitch::from_name("E#").unwrap();
663        assert_eq!(
664            major
665                .degree_of_by(&e_sharp, DegreeComparison::PitchClass)
666                .unwrap(),
667            Some(4)
668        );
669        assert_eq!(
670            major
671                .degree_of_by(&e_sharp, DegreeComparison::Name)
672                .unwrap(),
673            None
674        );
675        assert_eq!(
676            major
677                .degree_of_by(&e_sharp, DegreeComparison::Step)
678                .unwrap(),
679            Some(3)
680        );
681        let e_flat = Pitch::from_name("Eb").unwrap();
682        let (degree, accidental) = major.degree_and_accidental_of(&e_flat).unwrap();
683        assert_eq!(degree, 3);
684        assert_eq!(accidental.as_ref().map(|a| a.name()), Some("flat"));
685        let whole_tone = Scale::new(ScaleType::WholeTone, Pitch::from_name("C").unwrap());
686        assert!(
687            whole_tone
688                .degree_and_accidental_of(&Pitch::from_name("B").unwrap())
689                .is_err()
690        );
691
692        // Rag Marwa names its A twice, as the fifth degree and the seventh.
693        let marwa = Scale::new(ScaleType::RagMarwa, Pitch::from_name("C4").unwrap());
694        assert_eq!(
695            marwa
696                .degrees_of_by(&Pitch::from_name("A").unwrap(), DegreeComparison::Name)
697                .unwrap(),
698            [5, 7]
699        );
700    }
701
702    #[test]
703    fn a_scale_moves_to_its_relatives_parallels_and_a_new_tonic() {
704        use super::{Scale, ScaleType};
705        use crate::Pitch;
706
707        let tonic = |scale: &Scale| scale.tonic().name();
708        let c_minor = Scale::new(ScaleType::Minor, Pitch::from_name("C").unwrap());
709        assert_eq!(tonic(&c_minor.parallel_major()), "C");
710        assert_eq!(c_minor.parallel_major().scale_type(), ScaleType::Major);
711        assert_eq!(tonic(&c_minor.relative_major().unwrap()), "Eb");
712        let c_major = Scale::new(ScaleType::Major, Pitch::from_name("C").unwrap());
713        assert_eq!(tonic(&c_major.relative_minor().unwrap()), "A");
714        assert_eq!(c_major.parallel_minor().scale_type(), ScaleType::Minor);
715        let mut moved = c_major.clone();
716        moved.set_tonic(Pitch::from_name("D").unwrap());
717        assert_eq!(tonic(&moved), "D");
718        assert_eq!(moved.pitches().unwrap()[1].name(), "E");
719        assert!(c_major.octave_duplicating());
720        assert!(c_major.is_realizable());
721        assert_eq!(
722            ScaleType::from_music21_name("MajorScale"),
723            Some(ScaleType::Major)
724        );
725        assert_eq!(ScaleType::from_music21_name("NoSuchScale"), None);
726    }
727
728    #[test]
729    fn a_range_is_walked_from_where_music21_starts_it() {
730        // Every answer read off music21 11.0.0b9.
731        use super::Scale;
732        use crate::Pitch;
733        let pitch = |name: &str| Pitch::from_name(name).unwrap();
734        let named = |pitches: Vec<Pitch>| -> Vec<String> {
735            pitches.iter().map(Pitch::name_with_octave).collect()
736        };
737        let between = |scale_type: ScaleType, tonic: &str, low: &str, high: &str| {
738            named(
739                Scale::new(scale_type, pitch(tonic))
740                    .pitches_between(&pitch(low), &pitch(high))
741                    .unwrap(),
742            )
743        };
744        let coming_down = |scale_type: ScaleType, tonic: &str| {
745            named(
746                Scale::new(scale_type, pitch(tonic))
747                    .pitches_descending()
748                    .unwrap(),
749            )
750        };
751
752        // A whole-tone scale is a cycle, walked down to the range from its
753        // tonic above it.
754        assert_eq!(
755            between(ScaleType::WholeTone, "Bb4", "C#4", "G#4"),
756            ["D4", "Fb4", "Gb4", "Ab4"]
757        );
758        // An octave-repeating scale starts from the octave of its tonic just
759        // below the range, however far below the range the tonic is.
760        assert_eq!(
761            between(ScaleType::Octatonic, "G#2", "C#4", "G#4"),
762            ["C#4", "D4", "E4", "F4", "G4", "Ab4"]
763        );
764        // And stops at the first note at or past the top, so Rag Marwa's
765        // turn back down to G4 is not counted twice.
766        assert_eq!(
767            between(ScaleType::RagMarwa, "Bb3", "C#4", "G#4"),
768            ["D4", "E4", "G4"]
769        );
770        // Coming down, a scale is walked down, and spells as it goes.
771        assert_eq!(
772            coming_down(ScaleType::RagAsawari, "Bb3"),
773            ["Bb4", "Ab4", "F#4", "F4", "Eb4", "C#4", "C4", "Bb3"]
774        );
775        assert_eq!(
776            coming_down(ScaleType::RagMarwa, "C4"),
777            ["Db5", "C5", "Db5", "B4", "A4", "F#4", "E4", "Db4", "C4"]
778        );
779    }
780
781    #[test]
782    fn a_scale_is_derived_from_where_the_pitches_are() {
783        use super::DegreeComparison;
784        use crate::Pitch;
785        // music21 realizes each candidate between the lowest and highest
786        // pitch it is given, so a spelling the scale only has elsewhere does
787        // not count. Read off music21 11.0.0b9.
788        let pitches: Vec<Pitch> = ["F#", "A", "C#", "E#"]
789            .iter()
790            .map(|name| Pitch::from_name(*name).unwrap())
791            .collect();
792        let ranked: Vec<(usize, String)> = ScaleType::WholeTone
793            .derive_ranked_by(&pitches, Some(2), DegreeComparison::Name)
794            .unwrap()
795            .into_iter()
796            .map(|(matched, scale)| (matched, scale.tonic().name()))
797            .collect();
798        assert_eq!(ranked, [(3, "D#".to_string()), (3, "C#".to_string())]);
799    }
800
801    #[test]
802    fn a_scale_walks_down_as_well_as_up() {
803        use super::{Scale, ScaleType};
804        use crate::Pitch;
805
806        let names = |pitches: Vec<Pitch>| -> Vec<String> {
807            pitches.iter().map(Pitch::name_with_octave).collect()
808        };
809        let major = Scale::new(ScaleType::Major, Pitch::from_name("C4").unwrap());
810        assert_eq!(
811            names(major.pitches_descending().unwrap()),
812            ["C5", "B4", "A4", "G4", "F4", "E4", "D4", "C4"]
813        );
814        let low = Pitch::from_name("C3").unwrap();
815        let high = Pitch::from_name("C5").unwrap();
816        assert_eq!(
817            names(
818                major
819                    .pitches_between_descending(&Pitch::from_name("G4").unwrap(), &high)
820                    .unwrap()
821            ),
822            ["C5", "B4", "A4", "G4"]
823        );
824        assert_eq!(
825            names(
826                major
827                    .pitches_from_scale_degrees_between(&[1, 5], &low, &high)
828                    .unwrap()
829            ),
830            ["C3", "G3", "C4", "G4", "C5"]
831        );
832
833        // A pitch off the scale comes onto it first: below C#4 in C major
834        // is B3 from the lower neighbour and C4 from the upper.
835        let c_sharp = Pitch::from_name("C#4").unwrap();
836        assert_eq!(
837            major
838                .next_pitch_below(&c_sharp, 1)
839                .unwrap()
840                .name_with_octave(),
841            "C4"
842        );
843        assert_eq!(
844            major
845                .next_pitch_beside(&c_sharp, -1, true)
846                .unwrap()
847                .name_with_octave(),
848            "B3"
849        );
850        assert_eq!(
851            major
852                .next_pitch_beside(&c_sharp, -1, false)
853                .unwrap()
854                .name_with_octave(),
855            "C4"
856        );
857        assert_eq!(
858            major
859                .next_pitch_beside(&c_sharp, 1, false)
860                .unwrap()
861                .name_with_octave(),
862            "E4"
863        );
864
865        // Rag Marwa stands D- in two places, as the note above its tonic and
866        // the one it passes through coming down from the octave, and each
867        // has its own neighbour.
868        let marwa = Scale::new(ScaleType::RagMarwa, Pitch::from_name("C4").unwrap());
869        let d_flat = Pitch::from_name("D-4").unwrap();
870        assert_eq!(marwa.places_of(&d_flat).unwrap(), 1);
871        assert_eq!(
872            marwa
873                .next_pitch_above_from(&d_flat, 1, 0)
874                .unwrap()
875                .name_with_octave(),
876            "E4"
877        );
878        assert_eq!(
879            marwa
880                .next_pitch_below_from(&Pitch::from_name("C5").unwrap(), 1, 0)
881                .unwrap()
882                .name_with_octave(),
883            "A4"
884        );
885    }
886
887    #[test]
888    fn a_custom_scale_ranks_the_tonics_that_fit_a_set_of_pitches() {
889        use super::{DegreeComparison, Scale};
890        use crate::Pitch;
891
892        let pitches: Vec<Pitch> = ["C4", "D4", "E4", "F4", "G4", "A4", "B4", "C5"]
893            .iter()
894            .map(|name| Pitch::from_name(*name).unwrap())
895            .collect();
896        let custom = Scale::from_pitches(&pitches).unwrap();
897        assert!(custom.is_custom());
898        let targets: Vec<Pitch> = ["G4", "B4", "D5"]
899            .iter()
900            .map(|name| Pitch::from_name(*name).unwrap())
901            .collect();
902        let ranked = custom
903            .derive_ranked_by(&targets, Some(2), DegreeComparison::PitchClass)
904            .unwrap();
905        assert_eq!(ranked.len(), 2);
906        assert_eq!(ranked[0].0, 3);
907        assert!(ranked[0].1.is_custom());
908    }
909
910    /// A scale carrying its own tuning retunes the notes it names, in their
911    /// own octaves and spellings, and leaves the notes it does not name
912    /// where they are.
913    #[test]
914    fn a_stream_is_tuned_onto_a_scale() {
915        use super::Scale;
916        use crate::stream::StreamElement;
917        use crate::{Chord, Note, Pitch, Stream};
918
919        let tuned: Vec<Pitch> = [
920            ("C4", 0.0),
921            ("D4", 20.0),
922            ("E4", 0.0),
923            ("F4", 0.0),
924            ("G4", 0.0),
925            ("A4", -15.0),
926            ("B4", 0.0),
927            ("C5", 0.0),
928        ]
929        .iter()
930        .map(|(name, cents)| {
931            let mut pitch = Pitch::from_name(*name).unwrap();
932            pitch.set_microtone_cents(*cents).unwrap();
933            pitch
934        })
935        .collect();
936        let scale = Scale::from_pitches(&tuned).unwrap();
937        let mut stream = Stream::new();
938        for name in ["D5", "F#4", "A3", "B#4", "Gb5"] {
939            stream.push(Note::from_pitch(Pitch::from_name(name).unwrap()));
940        }
941        stream.push(Chord::new("C5 E5 G5").unwrap());
942        scale.tune(&mut stream).unwrap();
943        let sounding: Vec<(String, FloatType)> = stream
944            .events()
945            .iter()
946            .flat_map(|event| match event.element() {
947                StreamElement::Note(note) => {
948                    vec![(note.pitch().name_with_octave(), note.pitch().ps())]
949                }
950                StreamElement::Chord(chord) => chord
951                    .pitches()
952                    .iter()
953                    .map(|pitch| (pitch.name_with_octave(), pitch.ps()))
954                    .collect(),
955                _ => Vec::new(),
956            })
957            .collect();
958        let expected: Vec<(String, FloatType)> = [
959            ("D5", 74.2),
960            ("F#4", 66.0),
961            ("A3", 56.85),
962            ("B#4", 72.0),
963            ("Gb5", 78.0),
964            ("C5", 72.0),
965            ("E5", 76.0),
966            ("G5", 79.0),
967        ]
968        .iter()
969        .map(|(name, ps)| (name.to_string(), *ps))
970        .collect();
971        assert_eq!(sounding.len(), expected.len());
972        for ((name, ps), (wanted_name, wanted_ps)) in sounding.iter().zip(&expected) {
973            assert_eq!(name, wanted_name);
974            assert!(
975                (ps - wanted_ps).abs() < 1e-9,
976                "{name} sounds at {ps}, not {wanted_ps}"
977            );
978        }
979    }
980
981    /// music21's own `romanNumeral` example, and the Scala reading of a
982    /// major scale.
983    #[test]
984    fn a_scale_carries_numerals_and_writes_itself_as_scala() {
985        use super::{Scale, ScaleType};
986        use crate::Pitch;
987        use crate::tuningsystem::scala::ScalaDegree;
988
989        let scale = Scale::new(ScaleType::Major, Pitch::from_name("Ab4").unwrap());
990        let tonic = scale.roman_numeral(1).unwrap();
991        assert_eq!(tonic.to_chord().unwrap().root().unwrap().to_string(), "Ab4");
992        let dominant = scale.roman_numeral(5).unwrap();
993        assert_eq!(
994            dominant.to_chord().unwrap().root().unwrap().to_string(),
995            "Eb5"
996        );
997        assert_eq!(dominant.figure_and_key(), "V in Ab major");
998        assert!(scale.roman_numeral(8).is_err());
999
1000        let scala = scale.scala_data().unwrap();
1001        assert_eq!(scala.len(), 7);
1002        assert!(matches!(scala.degrees()[0], ScalaDegree::Ratio(_)));
1003        let cents: Vec<i64> = scala
1004            .degrees()
1005            .iter()
1006            .map(|degree| degree.cents().round() as i64)
1007            .collect();
1008        assert_eq!(cents, [0, 200, 400, 500, 700, 900, 1100]);
1009        assert_eq!(scala.period().cents().round() as i64, 1200);
1010        assert_eq!(scala.description(), "Ab major");
1011    }
1012
1013    #[test]
1014    fn a_scale_can_be_given_by_its_notes() {
1015        // music21's own example: a scale of four notes, which repeats at the
1016        // octave like any other.
1017        let given: Vec<Pitch> = ["C4", "Eb4", "Gb4", "A4"]
1018            .iter()
1019            .map(|name| Pitch::from_name(*name).expect("valid pitch"))
1020            .collect();
1021        let scale = Scale::from_pitches(&given).expect("a scale");
1022        assert!(scale.is_custom());
1023        assert_eq!(scale.degree_count(), 4);
1024        let realized: Vec<String> = scale
1025            .pitches()
1026            .expect("realizes")
1027            .iter()
1028            .map(Pitch::name_with_octave)
1029            .collect();
1030        assert_eq!(realized, ["C4", "Eb4", "Gb4", "A4", "C5"]);
1031        assert_eq!(scale.pitch_at_degree(4).unwrap().name_with_octave(), "A4");
1032        // The fifth degree of a four-note scale is the first again, in the
1033        // octave the scale stands in — music21 reads a degree within the one
1034        // octave its network holds.
1035        assert_eq!(scale.pitch_at_degree(5).unwrap().name_with_octave(), "C4");
1036        assert_eq!(
1037            scale.degree_of(&Pitch::from_name("G-").unwrap()).unwrap(),
1038            Some(3)
1039        );
1040
1041        // And it walks a range the way a named scale does.
1042        let range: Vec<String> = scale
1043            .pitches_between(
1044                &Pitch::from_name("Eb5").unwrap(),
1045                &Pitch::from_name("C6").unwrap(),
1046            )
1047            .expect("a range")
1048            .iter()
1049            .map(Pitch::name_with_octave)
1050            .collect();
1051        assert_eq!(range, ["Eb5", "Gb5", "A5", "C6"]);
1052
1053        assert!(Scale::from_pitches(&[]).is_err());
1054    }
1055    #[test]
1056    fn a_range_starts_at_the_first_scale_pitch_inside_it() {
1057        // music21's own example: C major from E-flat 5 to G-flat 7 starts on
1058        // E5, the first scale pitch that is not below the bottom.
1059        let scale = Scale::new(ScaleType::Major, Pitch::from_name("C").unwrap());
1060        let range = scale
1061            .pitches_between(
1062                &Pitch::from_name("E-5").unwrap(),
1063                &Pitch::from_name("G-7").unwrap(),
1064            )
1065            .unwrap();
1066        let names: Vec<String> = range.iter().map(Pitch::name_with_octave).take(4).collect();
1067        assert_eq!(names, ["E5", "F5", "G5", "A5"]);
1068        assert_eq!(range.last().unwrap().name_with_octave(), "F7");
1069
1070        // A range of exactly one octave has both ends in it.
1071        let octave = scale
1072            .pitches_between(
1073                &Pitch::from_name("C3").unwrap(),
1074                &Pitch::from_name("C4").unwrap(),
1075            )
1076            .unwrap();
1077        let names: Vec<String> = octave.iter().map(Pitch::name_with_octave).collect();
1078        assert_eq!(names, ["C3", "D3", "E3", "F3", "G3", "A3", "B3", "C4"]);
1079
1080        // Asked the other way round, the same range comes back descending,
1081        // which is what music21 does.
1082        let descending: Vec<String> = scale
1083            .pitches_between(
1084                &Pitch::from_name("C5").unwrap(),
1085                &Pitch::from_name("C4").unwrap(),
1086            )
1087            .unwrap()
1088            .iter()
1089            .map(Pitch::name_with_octave)
1090            .collect();
1091        assert_eq!(descending, ["C5", "B4", "A4", "G4", "F4", "E4", "D4", "C4"]);
1092    }
1093
1094    #[test]
1095    fn scale_helpers_match_music21() {
1096        let c_major = Scale::new(ScaleType::Major, Pitch::from_name("C4").unwrap());
1097        let pitch = |name: &str| Pitch::from_name(name).unwrap();
1098        let names = |pitches: &[Pitch]| -> Vec<String> {
1099            pitches.iter().map(Pitch::name_with_octave).collect()
1100        };
1101
1102        assert_eq!(c_major.degree_count(), 7);
1103        assert_eq!(
1104            Scale::new(ScaleType::Chromatic, pitch("C4")).degree_count(),
1105            12
1106        );
1107        assert_eq!(
1108            c_major
1109                .transpose(&Interval::from_name("P5").unwrap())
1110                .unwrap()
1111                .tonic()
1112                .name_with_octave(),
1113            "G4"
1114        );
1115        assert_eq!(
1116            names(&c_major.chord().unwrap().pitches()),
1117            ["C4", "D4", "E4", "F4", "G4", "A4", "B4", "C5"]
1118        );
1119        assert_eq!(
1120            names(
1121                &c_major
1122                    .pitches_from_scale_degrees(&[1, 3, 5, 8, 10])
1123                    .unwrap()
1124            ),
1125            ["C4", "E4", "G4", "C5"]
1126        );
1127        assert_eq!(
1128            c_major.interval_between_degrees(1, 5).unwrap().short_name(),
1129            "P5"
1130        );
1131        assert_eq!(
1132            c_major.interval_between_degrees(3, 7).unwrap().short_name(),
1133            "P5"
1134        );
1135        assert_eq!(
1136            c_major
1137                .interval_between_degrees(5, 2)
1138                .unwrap()
1139                .directed_name(),
1140            "P-4"
1141        );
1142        assert_eq!(
1143            c_major.interval_between_degrees(2, 9).unwrap().short_name(),
1144            "P1"
1145        );
1146
1147        assert!(c_major.is_next(&pitch("D4"), &pitch("C4"), 1).unwrap());
1148        assert!(c_major.is_next(&pitch("D5"), &pitch("C4"), 1).unwrap());
1149        assert!(!c_major.is_next(&pitch("E4"), &pitch("C4"), 1).unwrap());
1150        assert!(c_major.is_next(&pitch("E4"), &pitch("C4"), 2).unwrap());
1151
1152        let (matched, unmatched) = c_major
1153            .match_pitches(&["C4", "E4", "Gb4", "Bb5", "A"].map(pitch))
1154            .unwrap();
1155        assert_eq!(names(&matched), ["C4", "E4", "A4"]);
1156        assert_eq!(names(&unmatched), ["Gb4", "Bb5"]);
1157
1158        assert_eq!(
1159            names(
1160                &c_major
1161                    .find_missing(&["C4", "E4", "G4"].map(pitch))
1162                    .unwrap()
1163            ),
1164            ["D4", "F4", "A4", "B4"]
1165        );
1166        let a_minor = Scale::new(ScaleType::Minor, pitch("A"));
1167        assert_eq!(
1168            names(
1169                &a_minor
1170                    .find_missing(&["A", "B", "C", "E"].map(pitch))
1171                    .unwrap()
1172            ),
1173            ["D5", "F5", "G5"]
1174        );
1175        assert!(
1176            c_major
1177                .find_missing(&["C4", "D4", "E4", "F4", "G4", "A4", "B4"].map(pitch))
1178                .unwrap()
1179                .is_empty()
1180        );
1181    }
1182
1183    #[test]
1184    fn solfeg_matches_music21() {
1185        let c_major = Scale::new(ScaleType::Major, Pitch::from_name("C4").unwrap());
1186        let cases = [
1187            ("C4", "do", "do", "do"),
1188            ("C#4", "di", "di", "do"),
1189            ("D-4", "ra", "ra", "re"),
1190            ("E-4", "me", "me", "mi"),
1191            ("F#4", "fi", "fi", "fa"),
1192            ("G-4", "se", "se", "sol"),
1193            ("G4", "sol", "so", "sol"),
1194            ("A-4", "le", "le", "la"),
1195            ("B-4", "te", "te", "ti"),
1196            ("C-4", "de", "de", "do"),
1197            ("B#4", "tis", "ty", "ti"),
1198            ("F##4", "fis", "fis", "fa"),
1199            ("E#4", "mis", "my", "mi"),
1200            ("D##4", "ris", "ris", "re"),
1201        ];
1202        for (name, music21, humdrum, plain) in cases {
1203            let pitch = Pitch::from_name(name).unwrap();
1204            assert_eq!(
1205                c_major
1206                    .solfeg(&pitch, SolfegVariant::Music21, true)
1207                    .unwrap(),
1208                music21,
1209                "{name}"
1210            );
1211            assert_eq!(
1212                c_major
1213                    .solfeg(&pitch, SolfegVariant::Humdrum, true)
1214                    .unwrap(),
1215                humdrum,
1216                "{name}"
1217            );
1218            assert_eq!(
1219                c_major
1220                    .solfeg(&pitch, SolfegVariant::Music21, false)
1221                    .unwrap(),
1222                plain,
1223                "{name}"
1224            );
1225        }
1226        let chromatic = Scale::new(ScaleType::Chromatic, Pitch::from_name("C4").unwrap());
1227        assert_eq!(
1228            chromatic
1229                .solfeg(
1230                    &Pitch::from_name("D4").unwrap(),
1231                    SolfegVariant::Music21,
1232                    true
1233                )
1234                .unwrap(),
1235            "mi"
1236        );
1237        assert!(
1238            chromatic
1239                .solfeg(
1240                    &Pitch::from_name("B4").unwrap(),
1241                    SolfegVariant::Music21,
1242                    true
1243                )
1244                .is_err()
1245        );
1246        assert!(
1247            c_major
1248                .solfeg(
1249                    &Pitch::from_name("C###4").unwrap(),
1250                    SolfegVariant::Music21,
1251                    true
1252                )
1253                .is_err()
1254        );
1255    }
1256    use super::*;
1257
1258    #[test]
1259    fn degrees_and_neighbouring_pitches_match_music21() {
1260        let scale = Scale::new(ScaleType::Major, Pitch::from_name("G4").unwrap());
1261        let degree = |name: &str| scale.degree_of(&Pitch::from_name(name).unwrap()).unwrap();
1262        assert_eq!(degree("G4"), Some(1));
1263        assert_eq!(degree("A4"), Some(2));
1264        assert_eq!(degree("F#5"), Some(7));
1265        assert_eq!(degree("B"), Some(3));
1266        assert_eq!(degree("B-4"), None);
1267        assert_eq!(degree("E3"), Some(6));
1268        assert_eq!(
1269            scale
1270                .degree_of_pitch_class(&Pitch::from_name("G-4").unwrap())
1271                .unwrap(),
1272            Some(7)
1273        );
1274
1275        let cases = [
1276            ("G4", true, 1, "A4"),
1277            ("G4", false, 1, "F#4"),
1278            ("F#4", true, 1, "G4"),
1279            ("B4", true, 3, "E5"),
1280            ("G4", false, 2, "E4"),
1281            ("E-4", true, 1, "E4"),
1282            ("E-4", false, 1, "D4"),
1283            ("B-4", false, 1, "A4"),
1284            ("G4", true, 8, "A5"),
1285            ("D5", true, 7, "D6"),
1286            ("G", true, 1, "A"),
1287            ("F#5", true, 1, "G5"),
1288            ("C3", false, 4, "F#2"),
1289        ];
1290        for (origin, ascending, steps, expected) in cases {
1291            let origin_pitch = Pitch::from_name(origin).unwrap();
1292            let next = if ascending {
1293                scale.next_pitch_above(&origin_pitch, steps)
1294            } else {
1295                scale.next_pitch_below(&origin_pitch, steps)
1296            };
1297            assert_eq!(
1298                next.unwrap().name_with_octave(),
1299                expected,
1300                "{origin} {ascending} {steps}"
1301            );
1302        }
1303        assert!(
1304            scale
1305                .next_pitch_above(&Pitch::from_name("G4").unwrap(), 0)
1306                .is_err()
1307        );
1308    }
1309
1310    #[test]
1311    fn derivation_matches_music21() {
1312        let pitches = |names: &[&str]| {
1313            names
1314                .iter()
1315                .map(|name| Pitch::from_name(*name).unwrap())
1316                .collect::<Vec<_>>()
1317        };
1318        type Row = (
1319            &'static [&'static str],
1320            ScaleType,
1321            &'static str,
1322            [(&'static str, usize); 4],
1323            &'static [&'static str],
1324        );
1325        let cases: [Row; 12] = [
1326            (
1327                &["C", "E", "G"],
1328                ScaleType::Major,
1329                "G",
1330                [("G", 3), ("F", 3), ("C", 3), ("Bb", 2)],
1331                &["G", "F", "C"],
1332            ),
1333            (
1334                &["C", "E", "G"],
1335                ScaleType::Minor,
1336                "A",
1337                [("A", 3), ("E", 3), ("D", 3), ("B", 2)],
1338                &["A", "E", "D"],
1339            ),
1340            (
1341                &["C", "E", "G"],
1342                ScaleType::Dorian,
1343                "A",
1344                [("A", 3), ("G", 3), ("D", 3), ("Bb", 2)],
1345                &["A", "G", "D"],
1346            ),
1347            (
1348                &["F#", "A", "C#", "E"],
1349                ScaleType::Major,
1350                "A",
1351                [("A", 4), ("E", 4), ("D", 4), ("B", 3)],
1352                &["A", "E", "D"],
1353            ),
1354            (
1355                &["F#", "A", "C#", "E"],
1356                ScaleType::Minor,
1357                "B",
1358                [("B", 4), ("F#", 4), ("Db", 4), ("C#", 4)],
1359                &["B", "F#", "Db", "C#", "Cb"],
1360            ),
1361            (
1362                &["G#", "B", "D", "F"],
1363                ScaleType::HarmonicMinor,
1364                "A",
1365                [("A", 4), ("F#", 4), ("Eb", 4), ("D#", 4)],
1366                &["A", "F#", "Eb", "D#", "C"],
1367            ),
1368            (
1369                &["Bb", "D", "F", "A-"],
1370                ScaleType::Major,
1371                "Eb",
1372                [("Eb", 4), ("D#", 4), ("Bb", 3), ("G#", 3)],
1373                &["Eb", "D#"],
1374            ),
1375            (
1376                &["Bb", "D", "F", "A-"],
1377                ScaleType::Dorian,
1378                "F",
1379                [("F", 4), ("Bb", 3), ("G#", 3), ("G", 3)],
1380                &["F"],
1381            ),
1382            (
1383                &["C", "D", "E", "F#", "G", "A", "B"],
1384                ScaleType::Major,
1385                "G",
1386                [("G", 7), ("D", 6), ("C", 6), ("A", 5)],
1387                &["G"],
1388            ),
1389            (
1390                &["C", "D", "E", "F#", "G", "A", "B"],
1391                ScaleType::Minor,
1392                "E",
1393                [("E", 7), ("B", 6), ("A", 6), ("Cb", 6)],
1394                &["E"],
1395            ),
1396            (
1397                &["C", "C#", "D"],
1398                ScaleType::Major,
1399                "Bb",
1400                [("Bb", 2), ("A", 2), ("G#", 2), ("G", 2)],
1401                &[],
1402            ),
1403            (
1404                &["Eb", "G", "Bb"],
1405                ScaleType::Major,
1406                "Bb",
1407                [("Bb", 3), ("G#", 3), ("Eb", 3), ("D#", 3)],
1408                &["Bb", "G#", "Eb", "D#"],
1409            ),
1410        ];
1411        for (names, scale_type, best, ranked, all) in cases {
1412            let input = pitches(names);
1413            assert_eq!(
1414                scale_type.derive(&input).unwrap().tonic().name(),
1415                best,
1416                "{scale_type:?} {names:?}"
1417            );
1418            let top = scale_type
1419                .derive_ranked(&input, Some(4))
1420                .unwrap()
1421                .into_iter()
1422                .map(|(matched, scale)| (scale.tonic().name(), matched))
1423                .collect::<Vec<_>>();
1424            let expected = ranked
1425                .iter()
1426                .map(|(tonic, matched)| (tonic.to_string(), *matched))
1427                .collect::<Vec<_>>();
1428            assert_eq!(top, expected, "{scale_type:?} {names:?}");
1429            let complete = scale_type
1430                .derive_all(&input)
1431                .unwrap()
1432                .iter()
1433                .map(|scale| scale.tonic().name())
1434                .collect::<Vec<_>>();
1435            assert_eq!(complete, all, "{scale_type:?} {names:?}");
1436        }
1437    }
1438
1439    #[test]
1440    fn degree_with_accidental_matches_music21() {
1441        let scale = Scale::new(ScaleType::Major, Pitch::from_name("C4").unwrap());
1442        let cases = [
1443            ("C4", 1, None),
1444            ("D4", 2, None),
1445            ("E-4", 3, Some("flat")),
1446            ("F#4", 4, Some("sharp")),
1447            ("G", 5, None),
1448            ("A#", 6, Some("sharp")),
1449            ("Bb", 7, Some("flat")),
1450            ("D--4", 2, Some("double-flat")),
1451            ("C#4", 1, Some("sharp")),
1452            ("E#4", 3, Some("sharp")),
1453        ];
1454        for (name, degree, accidental) in cases {
1455            let (found, found_accidental) = scale
1456                .degree_and_accidental_of(&Pitch::from_name(name).unwrap())
1457                .unwrap();
1458            assert_eq!(found, degree, "{name}");
1459            assert_eq!(
1460                found_accidental
1461                    .as_ref()
1462                    .map(|accidental| accidental.name()),
1463                accidental,
1464                "{name}"
1465            );
1466        }
1467    }
1468
1469    fn names(scale_type: ScaleType, tonic: &str) -> Vec<String> {
1470        Scale::new(scale_type, Pitch::from_name(tonic).expect("valid tonic"))
1471            .pitches()
1472            .expect("scale realizes")
1473            .iter()
1474            .map(|pitch| pitch.name())
1475            .collect()
1476    }
1477
1478    #[test]
1479    fn realizes_the_church_modes_on_c() {
1480        assert_eq!(
1481            names(ScaleType::Major, "C4"),
1482            ["C", "D", "E", "F", "G", "A", "B", "C"]
1483        );
1484        assert_eq!(
1485            names(ScaleType::Minor, "C4"),
1486            ["C", "D", "Eb", "F", "G", "Ab", "Bb", "C"]
1487        );
1488        assert_eq!(
1489            names(ScaleType::Dorian, "C4"),
1490            ["C", "D", "Eb", "F", "G", "A", "Bb", "C"]
1491        );
1492        assert_eq!(
1493            names(ScaleType::Phrygian, "C4"),
1494            ["C", "Db", "Eb", "F", "G", "Ab", "Bb", "C"]
1495        );
1496        assert_eq!(
1497            names(ScaleType::Lydian, "C4"),
1498            ["C", "D", "E", "F#", "G", "A", "B", "C"]
1499        );
1500        assert_eq!(
1501            names(ScaleType::Mixolydian, "C4"),
1502            ["C", "D", "E", "F", "G", "A", "Bb", "C"]
1503        );
1504        assert_eq!(
1505            names(ScaleType::Locrian, "C4"),
1506            ["C", "Db", "Eb", "F", "Gb", "Ab", "Bb", "C"]
1507        );
1508    }
1509
1510    #[test]
1511    fn realizes_the_altered_minors() {
1512        assert_eq!(
1513            names(ScaleType::HarmonicMinor, "C4"),
1514            ["C", "D", "Eb", "F", "G", "Ab", "B", "C"]
1515        );
1516        assert_eq!(
1517            names(ScaleType::MelodicMinor, "C4"),
1518            ["C", "D", "Eb", "F", "G", "A", "B", "C"]
1519        );
1520    }
1521
1522    #[test]
1523    fn plagal_modes_share_their_authentic_pitches() {
1524        for (plagal, authentic) in [
1525            (ScaleType::Hypodorian, ScaleType::Dorian),
1526            (ScaleType::Hypophrygian, ScaleType::Phrygian),
1527            (ScaleType::Hypolydian, ScaleType::Lydian),
1528            (ScaleType::Hypomixolydian, ScaleType::Mixolydian),
1529            (ScaleType::Hypolocrian, ScaleType::Locrian),
1530            (ScaleType::Hypoaeolian, ScaleType::Minor),
1531        ] {
1532            let mut plagal_names = names(plagal, "C4");
1533            plagal_names.sort();
1534            plagal_names.dedup();
1535            let mut authentic_names = names(authentic, "C4");
1536            authentic_names.sort();
1537            authentic_names.dedup();
1538            assert_eq!(
1539                plagal_names, authentic_names,
1540                "{plagal:?} should share {authentic:?}'s pitch collection"
1541            );
1542        }
1543    }
1544
1545    #[test]
1546    fn a_plagal_mode_is_realized_a_fourth_below_its_final() {
1547        // music21's own example: the hypodorian on D runs A3 to A4, and the
1548        // note it comes to rest on is its fourth degree.
1549        let scale = Scale::new(
1550            ScaleType::Hypodorian,
1551            Pitch::from_name("d").expect("valid tonic"),
1552        );
1553        let realized: Vec<String> = scale
1554            .pitches()
1555            .expect("scale realizes")
1556            .iter()
1557            .map(Pitch::name_with_octave)
1558            .collect();
1559        assert_eq!(
1560            realized,
1561            ["A3", "B3", "C4", "D4", "E4", "F4", "G4", "A4"],
1562            "a plagal mode starts a fourth below its final"
1563        );
1564        assert_eq!(scale.final_pitch().unwrap().name_with_octave(), "D4");
1565        assert_eq!(scale.dominant().unwrap().name_with_octave(), "F4");
1566
1567        // An authentic mode starts on its own final, and its dominant is the
1568        // fifth degree.
1569        let dorian = Scale::new(
1570            ScaleType::Dorian,
1571            Pitch::from_name("d").expect("valid tonic"),
1572        );
1573        assert_eq!(dorian.final_pitch().unwrap().name_with_octave(), "D4");
1574        assert_eq!(dorian.dominant().unwrap().name_with_octave(), "A4");
1575    }
1576
1577    #[test]
1578    fn a_leading_tone_is_a_semitone_below_the_final() {
1579        // In a minor scale that is not the seventh degree the scale has.
1580        let minor = Scale::new(
1581            ScaleType::Minor,
1582            Pitch::from_name("c").expect("valid tonic"),
1583        );
1584        assert_eq!(minor.pitch_at_degree(7).unwrap().name(), "Bb");
1585        assert_eq!(minor.leading_tone().unwrap().name(), "B");
1586        // In a major scale it already is.
1587        let major = Scale::new(
1588            ScaleType::Major,
1589            Pitch::from_name("C").expect("valid tonic"),
1590        );
1591        assert_eq!(major.leading_tone().unwrap().name_with_octave(), "B4");
1592    }
1593
1594    #[test]
1595    fn whole_tone_spells_upward_until_accidentals_run_out() {
1596        // From C the scale can stay on sharps and closes on B#, not C.
1597        assert_eq!(
1598            names(ScaleType::WholeTone, "C4"),
1599            ["C", "D", "E", "F#", "G#", "A#", "B#"]
1600        );
1601        // From B a literal spelling would need G##, so music21 respells.
1602        assert_eq!(
1603            names(ScaleType::WholeTone, "B4"),
1604            ["B", "C#", "D#", "E#", "G", "A", "B"]
1605        );
1606    }
1607
1608    #[test]
1609    fn octatonic_alternates_tone_and_semitone() {
1610        assert_eq!(
1611            names(ScaleType::Octatonic, "C4"),
1612            ["C", "D", "Eb", "F", "Gb", "Ab", "A", "B", "C"]
1613        );
1614        assert_eq!(
1615            names(ScaleType::Octatonic, "G4"),
1616            ["G", "A", "Bb", "C", "Db", "Eb", "Fb", "Gb", "G"]
1617        );
1618    }
1619
1620    #[test]
1621    fn chromatic_uses_the_most_common_spelling() {
1622        assert_eq!(
1623            names(ScaleType::Chromatic, "C4"),
1624            [
1625                "C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B", "C"
1626            ]
1627        );
1628    }
1629
1630    #[test]
1631    fn rag_asawari_is_pentatonic() {
1632        assert_eq!(
1633            names(ScaleType::RagAsawari, "C4"),
1634            ["C", "D", "F", "G", "Ab", "C"]
1635        );
1636    }
1637
1638    #[test]
1639    fn rag_marwa_dips_below_its_sixth_degree() {
1640        // music21's ascending network steps down a M2 from B before closing on
1641        // C, so A appears twice and the line is not monotonic — and then it
1642        // carries on a semitone past the octave, which is the one scale here
1643        // that ends above where it closed.
1644        assert_eq!(
1645            names(ScaleType::RagMarwa, "C4"),
1646            ["C", "Db", "E", "F#", "A", "B", "A", "C", "Db"]
1647        );
1648        assert_eq!(
1649            names(ScaleType::RagMarwa, "E-4"),
1650            ["Eb", "Fb", "G", "A", "C", "D", "C", "Eb", "Fb"]
1651        );
1652    }
1653
1654    #[test]
1655    fn simplifying_scales_never_exceed_their_accidental_budget() {
1656        for scale_type in [ScaleType::WholeTone, ScaleType::Octatonic] {
1657            for tonic in ["C4", "G4", "D4", "A4", "E4", "B4", "F#4", "E-4", "G-4"] {
1658                let pitches = Scale::new(scale_type, Pitch::from_name(tonic).unwrap())
1659                    .pitches()
1660                    .unwrap();
1661                assert!(
1662                    max_alter(&pitches) <= 1,
1663                    "{scale_type:?} on {tonic} exceeded one accidental"
1664                );
1665            }
1666        }
1667    }
1668
1669    #[test]
1670    fn a_note_a_scale_stands_twice_has_two_notes_below_it() {
1671        // Rag Marwa comes down through the flat second twice over: once
1672        // above the tonic, its second degree, and once above the octave, its
1673        // seventh. `D-2` may be read as either, and the note below it is `C2`
1674        // or `B1` accordingly. The places run lowest degree first, and the
1675        // plain step below reads the note as the first of them.
1676        let marwa = Scale::new(ScaleType::RagMarwa, Pitch::from_name("C4").unwrap()).descending();
1677        let from = Pitch::from_name("D-2").unwrap();
1678        assert_eq!(marwa.places_of(&from).unwrap(), 2);
1679        let below: Vec<String> = (0..2)
1680            .map(|place| {
1681                marwa
1682                    .next_pitch_below_from(&from, 1, place)
1683                    .unwrap()
1684                    .name_with_octave()
1685            })
1686            .collect();
1687        assert_eq!(below, ["C2", "B1"]);
1688        assert_eq!(
1689            marwa.next_pitch_below(&from, 1).unwrap().name_with_octave(),
1690            "C2"
1691        );
1692
1693        // A note the scale stands only once is the same whichever place is
1694        // asked for, so a caller that always asks for the first is right.
1695        let major = Scale::new(ScaleType::Major, Pitch::from_name("C4").unwrap());
1696        let d = Pitch::from_name("D4").unwrap();
1697        assert_eq!(major.places_of(&d).unwrap(), 1);
1698        assert_eq!(
1699            major
1700                .next_pitch_below_from(&d, 1, 7)
1701                .unwrap()
1702                .name_with_octave(),
1703            "C4"
1704        );
1705    }
1706
1707    #[test]
1708    fn degree_lookup_matches_the_realized_pitches() {
1709        for scale_type in ScaleType::ALL {
1710            let scale = Scale::new(scale_type, Pitch::from_name("E-4").unwrap());
1711            let pitches = scale.pitches().unwrap();
1712            // Every degree but the closing octave, which is the first degree
1713            // again as far as a degree lookup is concerned. Asked by the
1714            // scale's own degrees, since Rag Asawari's are not its positions.
1715            let degrees = scale
1716                .named_degrees()
1717                .unwrap_or_else(|| (1..=scale.degree_count() as IntegerType).collect());
1718            for (expected, degree) in pitches.iter().zip(degrees) {
1719                let actual = scale.pitch_at_degree(degree).unwrap();
1720                assert_eq!(
1721                    actual.name_with_octave(),
1722                    expected.name_with_octave(),
1723                    "{scale_type:?} degree {degree}"
1724                );
1725                // Read back the other way: the degree the note is found on
1726                // stands on that note. Not necessarily the degree asked for
1727                // — Rag Marwa's A is both its fifth and its seventh.
1728                let found = scale.degree_of(expected).unwrap().unwrap();
1729                assert_eq!(
1730                    scale
1731                        .pitch_at_degree(found as IntegerType)
1732                        .unwrap()
1733                        .name_with_octave(),
1734                    expected.name_with_octave(),
1735                    "{scale_type:?} degree {degree} read back as {found}"
1736                );
1737            }
1738            // A degree the scale does not have is nothing, rather than the
1739            // note that would stand there had the degrees been counted off.
1740            if let Some(named) = scale.named_degrees() {
1741                for degree in 1..=*named.last().unwrap() {
1742                    assert_eq!(
1743                        scale.pitch_on_degree(degree).unwrap().is_some(),
1744                        named.contains(&degree),
1745                        "{scale_type:?} degree {degree}"
1746                    );
1747                }
1748            }
1749        }
1750    }
1751
1752    #[test]
1753    fn a_degree_outside_the_scale_counts_round_it() {
1754        // music21's own answers for `scale.PhrygianScale('g').pitchFromDegree`.
1755        let scale = Scale::new(ScaleType::Phrygian, Pitch::from_name("G4").unwrap());
1756        for (degree, expected) in [
1757            (-1, "Eb5"),
1758            (0, "F5"),
1759            (1, "G4"),
1760            (7, "F5"),
1761            (8, "G4"),
1762            (11, "C5"),
1763            (15, "G4"),
1764        ] {
1765            assert_eq!(
1766                scale.pitch_at_degree(degree).unwrap().name_with_octave(),
1767                expected,
1768                "degree {degree}"
1769            );
1770        }
1771    }
1772
1773    #[test]
1774    fn every_scale_realizes_on_every_common_tonic() {
1775        for scale_type in ScaleType::ALL {
1776            for tonic in [
1777                "C4", "G4", "D4", "A4", "E4", "B4", "F#4", "F4", "B-4", "E-4", "A-4",
1778            ] {
1779                let scale = Scale::new(scale_type, Pitch::from_name(tonic).unwrap());
1780                let pitches = scale.pitches().expect("scale realizes");
1781                let beyond = usize::from(scale_type.beyond_terminus().is_some());
1782                assert_eq!(
1783                    pitches.len(),
1784                    scale_type.degree_count() + 1 + beyond,
1785                    "{scale_type:?} on {tonic}"
1786                );
1787            }
1788        }
1789    }
1790
1791    #[test]
1792    fn music21_names_are_distinct() {
1793        let mut names: Vec<&str> = ScaleType::ALL.iter().map(|s| s.music21_name()).collect();
1794        names.sort_unstable();
1795        let count = names.len();
1796        names.dedup();
1797        assert_eq!(names.len(), count, "music21 names must be unique");
1798    }
1799}