Skip to main content

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