Skip to main content

music21_rs/scale/
stepscale.rs

1//! Scales built from a caller-supplied cycle of intervals.
2//!
3//! [`ScaleType`](super::ScaleType) covers music21's *named* `ConcreteScale`
4//! subclasses, which are fixed tables and so can be an enum. `CyclicalScale`
5//! and `OctaveRepeatingScale` are not: they take an arbitrary interval list at
6//! construction, so they are a runtime type rather than a variant.
7//!
8//! The two differ only in how they close:
9//!
10//! - [`StepScale::cyclical`] walks the intervals once and stops, so the scale
11//!   need not span or repeat at an octave — `["P5"]` from C is just `C G`.
12//! - [`StepScale::octave_repeating`] appends whatever interval is needed to
13//!   reach the octave above the tonic, so `["m3", "M3"]` from C becomes
14//!   `C E- G C`.
15
16use crate::defaults::{FloatType, IntegerType};
17use crate::error::Result;
18use crate::interval::{ChromaticInterval, Interval};
19use crate::pitch::Pitch;
20use crate::scale::Scale;
21use crate::sieve::Sieve;
22
23use std::sync::LazyLock;
24
25/// music21 defaults an absent interval list to a single minor second.
26static DEFAULT_STEP: LazyLock<Interval> =
27    LazyLock::new(|| Interval::from_name("m2").expect("m2 is a valid interval name"));
28
29/// A scale built by walking a cycle of intervals from a tonic.
30///
31/// ```
32/// use music21_rs::{Pitch, StepScale};
33///
34/// let tonic = Pitch::from_name("C4")?;
35/// let cyclical = StepScale::cyclical(tonic.clone(), &["m3", "M3"])?;
36/// let names: Vec<String> = cyclical.pitches()?.iter().map(|p| p.name()).collect();
37/// assert_eq!(names, ["C", "Eb", "G"]);
38///
39/// let repeating = StepScale::octave_repeating(tonic, &["m3", "M3"])?;
40/// let names: Vec<String> = repeating.pitches()?.iter().map(|p| p.name()).collect();
41/// assert_eq!(names, ["C", "Eb", "G", "C"]);
42/// # Ok::<(), music21_rs::Error>(())
43/// ```
44/// `Interval` implements neither `PartialEq` nor `Hash`, so neither is derived
45/// here; compare realized [`pitches`](StepScale::pitches) instead.
46#[derive(Clone, Debug)]
47#[must_use]
48pub struct StepScale {
49    tonic: Pitch,
50    steps: Vec<Interval>,
51    /// Whether the cycle was closed at the octave, which makes it a scale
52    /// that repeats there rather than a cycle walked from its tonic.
53    closed: bool,
54}
55
56impl StepScale {
57    /// Builds music21's `CyclicalScale`: the intervals walked once, no closing.
58    ///
59    /// An empty list defaults to a single `m2`, as music21 does.
60    pub fn cyclical(tonic: Pitch, steps: &[&str]) -> Result<Self> {
61        Ok(Self::cyclical_of(tonic, parse_steps(steps)?))
62    }
63
64    /// The same from intervals already in hand, which is the only way to
65    /// give a step that has no name: one measured in cents.
66    pub fn cyclical_of(tonic: Pitch, steps: Vec<Interval>) -> Self {
67        Self {
68            tonic,
69            steps: or_default(steps),
70            closed: false,
71        }
72    }
73
74    /// Builds music21's `OctaveRepeatingScale`: the intervals plus a closing
75    /// interval that completes the octave, where they do not reach it
76    /// already.
77    ///
78    /// An empty list defaults to a single `m2`, as music21 does.
79    ///
80    /// The closing interval is the *complement of the interval sum*, which is
81    /// how music21 derives it — not the interval between the last realized
82    /// pitch and the octave. The two differ in spelling whenever realization
83    /// respells a degree: three `m2` steps from C sum to `dd4`, whose
84    /// complement is `AA5`, so the scale closes on `B#` rather than `C`. Taking
85    /// it from the realized pitches would read the answer off already-simplified
86    /// output and lose that.
87    ///
88    /// music21's own behaviour for a cycle wider than an octave is erratic —
89    /// `["P5", "P5"]` returns pitches an octave above the tonic it was given,
90    /// and it mutates the caller's interval list in place. Neither is
91    /// reproduced here: the cycle is closed at the octave above the last pitch
92    /// and the input is left alone.
93    pub fn octave_repeating(tonic: Pitch, steps: &[&str]) -> Result<Self> {
94        Self::octave_repeating_of(tonic, parse_steps(steps)?)
95    }
96
97    /// The same from intervals already in hand.
98    pub fn octave_repeating_of(tonic: Pitch, steps: Vec<Interval>) -> Result<Self> {
99        let mut steps = or_default(steps);
100        // Steps a whole octave wide spelled as something else -- three major
101        // thirds come to `B#` -- have their last step respelled to land on
102        // the tonic, so `M3 M3 M3` is `M3 M3 d4`.
103        if let Ok(sum) = interval_sum(&tonic, &steps)
104            && sum.semitones() == 12.0
105            && sum.short_name() != "P8"
106        {
107            let last = steps.len() - 1;
108            let start = interval_sum(&tonic, &steps[..last])?
109                .transpose_pitch_with_options(&tonic, false, None)?;
110            let octave =
111                Interval::from_name("P8")?.transpose_pitch_with_options(&tonic, false, None)?;
112            steps[last] = Interval::between_pitches(&start, &octave)?;
113            return Ok(Self {
114                tonic,
115                steps,
116                closed: true,
117            });
118        }
119        let closing = match interval_sum(&tonic, &steps).and_then(|sum| sum.inversion()) {
120            Ok(closing) => closing,
121            // A sum no accidental can spell -- eleven minor seconds come to a
122            // twelfth diminished ten times over -- still has a size, and the
123            // cycle closes by what is left of the octave above it.
124            Err(_) => {
125                let risen: FloatType = steps.iter().map(Interval::semitones).sum();
126                let octaves = (risen / 12.0).floor() + 1.0;
127                Interval::from_semitones((octaves * 12.0 - risen).round() as IntegerType)?
128            }
129        };
130        // Steps that already reach the octave need no closing step.
131        if closing.short_name() != "P1" {
132            steps.push(closing);
133        }
134        Ok(Self {
135            tonic,
136            steps,
137            closed: true,
138        })
139    }
140
141    /// Builds music21's `SieveScale` from a Xenakis sieve expression.
142    ///
143    /// The sieve's interval widths become the cycle, which music21 then treats
144    /// as a `CyclicalScale` — so `"3@0"` from C is `C E-`, and the major-scale
145    /// sieve gives a major scale.
146    ///
147    /// The widths are counted in semitones; [`Self::sieve_by`] counts them in
148    /// anything else.
149    pub fn sieve(tonic: Pitch, expression: &str) -> Result<Self> {
150        Self::sieve_by(tonic, expression, 1.0)
151    }
152
153    /// The same with the sieve's unit said: music21's `eld`, the elementary
154    /// displacement, in semitones. A sieve of every integer counted in twos
155    /// is a whole-tone scale, and counted in halves is a scale of quarter
156    /// tones.
157    pub fn sieve_by(tonic: Pitch, expression: &str, eld: FloatType) -> Result<Self> {
158        let widths = Sieve::parse(expression)?.interval_widths()?;
159        let steps = widths
160            .into_iter()
161            .map(|width| {
162                Interval::from_chromatic(ChromaticInterval::new(FloatType::from(width) * eld)?)
163            })
164            .collect::<Result<Vec<_>>>()?;
165        Ok(Self {
166            tonic,
167            steps,
168            closed: false,
169        })
170    }
171
172    /// Returns the tonic pitch.
173    pub fn tonic(&self) -> &Pitch {
174        &self.tonic
175    }
176
177    /// Returns the step intervals, including any closing interval.
178    pub fn steps(&self) -> &[Interval] {
179        &self.steps
180    }
181
182    /// Returns the number of steps, which is one fewer than the pitch count.
183    pub fn degree_count(&self) -> usize {
184        self.steps.len()
185    }
186
187    /// The same cycle as a [`Scale`], which is what answers everything a
188    /// scale is asked past its own notes: a range of them, the degree a note
189    /// stands on, the note beside another.
190    pub fn scale(&self) -> Result<Scale> {
191        if self.closed {
192            Scale::from_steps(self.tonic.clone(), self.steps.clone())
193        } else {
194            Scale::from_cycle(self.tonic.clone(), self.steps.clone())
195        }
196    }
197
198    /// Returns the pitches of one pass through the cycle, starting at the tonic.
199    pub fn pitches(&self) -> Result<Vec<Pitch>> {
200        let mut pitches = Vec::with_capacity(self.steps.len() + 1);
201        pitches.push(self.tonic.clone());
202
203        let mut current = self.tonic.clone();
204        for step in &self.steps {
205            // music21's IntervalNetwork defaults to pitchSimplification
206            // 'maxAccidental' with a cap of one, which is what respells the
207            // third step of an m2 cycle from E-double-flat to D.
208            current = step.transpose_pitch_with_options(&current, false, Some(1))?;
209            pitches.push(current.clone());
210        }
211        Ok(pitches)
212    }
213}
214
215/// Returns the sum of `steps`, spelled exactly.
216///
217/// Computed by transposing a reference pitch with simplification switched off
218/// and measuring the result, so the sum keeps the accidentals the arithmetic
219/// actually produces rather than the ones a realized scale would show.
220fn interval_sum(reference: &Pitch, steps: &[Interval]) -> Result<Interval> {
221    let mut current = reference.clone();
222    for step in steps {
223        current = step.transpose_pitch_with_options(&current, false, None)?;
224    }
225    Interval::between_pitches(reference, &current)
226}
227
228/// music21 reads no steps at all as a single minor second.
229fn or_default(steps: Vec<Interval>) -> Vec<Interval> {
230    if steps.is_empty() {
231        vec![DEFAULT_STEP.clone()]
232    } else {
233        steps
234    }
235}
236
237fn parse_steps(steps: &[&str]) -> Result<Vec<Interval>> {
238    steps
239        .iter()
240        .map(|name| Interval::from_name(*name))
241        .collect()
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn a_step_scale_reports_its_tonic_and_steps() {
250        let tonic = Pitch::from_name("C4").unwrap();
251        let scale = StepScale::cyclical(tonic.clone(), &["M2", "M2"]).unwrap();
252        assert_eq!(scale.tonic(), &tonic);
253        assert_eq!(scale.steps().len(), 2);
254        assert_eq!(scale.degree_count(), 2);
255    }
256
257    #[test]
258    fn steps_reaching_the_octave_close_with_no_unison() {
259        let scale = StepScale::octave_repeating(
260            Pitch::from_name("C4").unwrap(),
261            &["m3", "M2", "m3", "M2", "M2"],
262        )
263        .unwrap();
264        assert_eq!(names(&scale), ["C", "Eb", "F", "Ab", "Bb", "C"]);
265    }
266
267    fn names(scale: &StepScale) -> Vec<String> {
268        scale
269            .pitches()
270            .expect("scale realizes")
271            .iter()
272            .map(|pitch| pitch.name())
273            .collect()
274    }
275
276    fn tonic(name: &str) -> Pitch {
277        Pitch::from_name(name).expect("valid tonic")
278    }
279
280    #[test]
281    fn a_step_scale_answers_as_a_scale() {
282        // Read off music21 11.0.0b9: `OctaveRepeatingScale('c4', ['m3', 'M3'])`
283        // asked for `getPitches('g2', 'g4')`, the degree of three notes, and
284        // `nextPitch` from `c4`, down from `e-5`, and two steps up from `g3`.
285        let scale = StepScale::octave_repeating(tonic("C4"), &["m3", "M3"])
286            .unwrap()
287            .scale()
288            .unwrap();
289        let spelled = |pitches: Vec<Pitch>| -> Vec<String> {
290            pitches.iter().map(Pitch::name_with_octave).collect()
291        };
292        assert_eq!(
293            spelled(scale.pitches_between(&tonic("G2"), &tonic("G4")).unwrap()),
294            ["G2", "C3", "Eb3", "G3", "C4", "Eb4", "G4"]
295        );
296        // The closing octave is the tonic again, not a fourth degree.
297        assert_eq!(scale.degree_count(), 3);
298        assert_eq!(scale.degree_of(&tonic("C4")).unwrap(), Some(1));
299        assert_eq!(scale.degree_of(&tonic("E-")).unwrap(), Some(2));
300        assert_eq!(scale.degree_of(&tonic("G5")).unwrap(), Some(3));
301        assert_eq!(scale.degree_of(&tonic("D4")).unwrap(), None);
302        let next = |origin: &str, steps: usize| {
303            scale
304                .next_pitch_above(&tonic(origin), steps)
305                .unwrap()
306                .name_with_octave()
307        };
308        assert_eq!(next("C4", 1), "Eb4");
309        assert_eq!(next("G3", 2), "Eb4");
310        assert_eq!(
311            scale
312                .next_pitch_below(&tonic("Eb5"), 1)
313                .unwrap()
314                .name_with_octave(),
315            "C5"
316        );
317    }
318
319    #[test]
320    fn a_respelling_scale_comes_down_by_its_own_walk() {
321        // music21: `OctaveRepeatingScale('d', ['m2', 'm2', 'm2'])`, going up
322        // and coming down. Going up the second step is an `F-`; coming down
323        // from `F` the same sound is an `E`, and the one below it a `D#`.
324        let scale = StepScale::octave_repeating(tonic("D"), &["m2", "m2", "m2"])
325            .unwrap()
326            .scale()
327            .unwrap();
328        let spelled = |pitches: Vec<Pitch>| -> Vec<String> {
329            pitches.iter().map(Pitch::name_with_octave).collect()
330        };
331        assert_eq!(
332            spelled(scale.pitches_between(&tonic("D4"), &tonic("D5")).unwrap()),
333            ["D4", "Eb4", "Fb4", "F4", "D5"]
334        );
335        assert_eq!(
336            spelled(
337                scale
338                    .pitches_between_descending(&tonic("D4"), &tonic("D5"))
339                    .unwrap()
340            ),
341            ["D5", "F4", "E4", "D#4", "D4"]
342        );
343    }
344
345    #[test]
346    fn a_cycle_that_does_not_close_at_the_octave_is_walked_both_ways() {
347        // music21's own `testCyclicalScales` and `testSieveScaleA`.
348        let spelled = |pitches: Vec<Pitch>| -> Vec<String> {
349            pitches.iter().map(ToString::to_string).collect()
350        };
351        let fifths = StepScale::cyclical(tonic("C4"), &["P5"])
352            .unwrap()
353            .scale()
354            .unwrap();
355        assert_eq!(spelled(fifths.pitches().unwrap()), ["C4", "G4"]);
356        assert_eq!(
357            spelled(fifths.pitches_between(&tonic("G2"), &tonic("G6")).unwrap()),
358            ["Bb2", "F3", "C4", "G4", "D5", "A5", "E6"]
359        );
360        // A cycle of one step has one degree, and every note of it is that.
361        assert_eq!(fifths.degree_of(&tonic("G4")).unwrap(), Some(1));
362        assert_eq!(fifths.degree_of(&tonic("Bb2")).unwrap(), Some(1));
363        // A degree is looked for an octave either side of the note, which is
364        // where music21 looks: the cycle's F# is six fifths up, far past F#3.
365        assert_eq!(fifths.degree_of(&tonic("F#3")).unwrap(), None);
366        assert_eq!(fifths.degree_of(&tonic("F3")).unwrap(), Some(1));
367
368        // A cycle of two steps may stand one name on both of them: G3 is its
369        // first degree and G4 its second, and both lie within an octave of G4.
370        let mixed = StepScale::cyclical(tonic("C4"), &["M2", "m3"])
371            .unwrap()
372            .scale()
373            .unwrap();
374        assert_eq!(
375            mixed
376                .degrees_of_by(&tonic("G4"), crate::scale::DegreeComparison::Name)
377                .unwrap(),
378            [1, 2]
379        );
380        assert_eq!(
381            fifths
382                .next_pitch_above(&tonic("G4"), 2)
383                .unwrap()
384                .to_string(),
385            "A5"
386        );
387
388        let seconds = StepScale::cyclical(tonic("C4"), &["m2", "m2"])
389            .unwrap()
390            .scale()
391            .unwrap();
392        assert_eq!(
393            spelled(seconds.pitches_between(&tonic("G4"), &tonic("G5")).unwrap()),
394            [
395                "G4", "Ab4", "A4", "Bb4", "Cb5", "C5", "Db5", "D5", "Eb5", "Fb5", "F5", "Gb5", "G5"
396            ]
397        );
398
399        // A sieve of every integer, counted in whole tones and then in
400        // quarter tones, each walked down from D4.
401        let whole = StepScale::sieve_by(tonic("D4"), "1@0", 2.0)
402            .unwrap()
403            .scale()
404            .unwrap();
405        assert_eq!(
406            spelled(whole.pitches_between(&tonic("C2"), &tonic("C3")).unwrap()),
407            ["C2", "D2", "Fb2", "Gb2", "Ab2", "Bb2", "C3"]
408        );
409        let quarter = StepScale::sieve_by(tonic("D4"), "1@0", 0.5)
410            .unwrap()
411            .scale()
412            .unwrap();
413        assert_eq!(
414            spelled(quarter.pitches_between(&tonic("C2"), &tonic("D2")).unwrap()),
415            ["C2", "C~2", "Db2", "D`2", "D2"]
416        );
417    }
418
419    #[test]
420    fn a_cycle_whose_sum_cannot_be_spelled_still_closes() {
421        let steps = ["m2"; 11];
422        let scale = StepScale::octave_repeating(tonic("C4"), &steps).unwrap();
423        let pitches = scale.pitches().unwrap();
424        assert_eq!(pitches.len(), 13);
425        assert_eq!(pitches.last().unwrap().ps(), 72.0);
426    }
427
428    #[test]
429    fn cyclical_walks_the_cycle_once() {
430        let scale = StepScale::cyclical(tonic("C4"), &["P5"]).unwrap();
431        assert_eq!(names(&scale), ["C", "G"]);
432
433        let scale = StepScale::cyclical(tonic("C4"), &["m3", "M3"]).unwrap();
434        assert_eq!(names(&scale), ["C", "Eb", "G"]);
435    }
436
437    #[test]
438    fn octave_repeating_closes_on_the_octave() {
439        let scale = StepScale::octave_repeating(tonic("C4"), &["m3", "M3"]).unwrap();
440        assert_eq!(names(&scale), ["C", "Eb", "G", "C"]);
441        let pitches = scale.pitches().unwrap();
442        assert_eq!(pitches.first().unwrap().octave(), Some(4));
443        assert_eq!(pitches.last().unwrap().octave(), Some(5));
444    }
445
446    #[test]
447    fn an_empty_interval_list_defaults_to_a_minor_second() {
448        // music21: CyclicalScale() is [C4, D-4]; OctaveRepeatingScale() is
449        // [C4, D-4, C5].
450        assert_eq!(
451            names(&StepScale::cyclical(tonic("C4"), &[]).unwrap()),
452            ["C", "Db"]
453        );
454        assert_eq!(
455            names(&StepScale::octave_repeating(tonic("C4"), &[]).unwrap()),
456            ["C", "Db", "C"]
457        );
458    }
459
460    #[test]
461    fn the_closing_interval_comes_from_the_interval_sum_not_the_pitches() {
462        // Three m2 steps sum to dd4, whose complement is AA5. Realization
463        // respells the third degree from E-double-flat to D, so reading the
464        // closing interval off the pitches would give a major sixth to C
465        // instead. music21 closes on B#, and so does this.
466        let scale = StepScale::octave_repeating(tonic("C4"), &["m2", "m2", "m2"]).unwrap();
467        assert_eq!(names(&scale), ["C", "Db", "D", "Eb", "B#"]);
468
469        // Where realization changes nothing, the two agree: M2+M2+m2 is P4 and
470        // its complement P5 closes on the octave.
471        let scale = StepScale::octave_repeating(tonic("C4"), &["M2", "M2", "m2"]).unwrap();
472        assert_eq!(names(&scale), ["C", "D", "E", "F", "C"]);
473        let scale = StepScale::octave_repeating(tonic("F#4"), &["M2", "M2", "m2"]).unwrap();
474        assert_eq!(names(&scale), ["F#", "G#", "A#", "B", "F#"]);
475    }
476
477    #[test]
478    fn a_cycle_wider_than_an_octave_closes_above_it() {
479        // Two fifths sum to M9, whose complement is m7, so the cycle closes an
480        // octave higher rather than folding back. music21 agrees on the
481        // intervals here but reports the pitches an octave off its own tonic.
482        let scale = StepScale::octave_repeating(tonic("C4"), &["P5", "P5"]).unwrap();
483        assert_eq!(names(&scale), ["C", "G", "D", "C"]);
484        let pitches = scale.pitches().unwrap();
485        assert_eq!(pitches.first().unwrap().octave(), Some(4));
486        assert_eq!(pitches.last().unwrap().octave(), Some(6));
487    }
488
489    #[test]
490    fn sieve_scales_match_music21() {
491        let cases: [(&str, &str, &[&str]); 6] = [
492            ("C4", "3@0", &["C", "Eb"]),
493            ("D4", "3@0", &["D", "F"]),
494            ("E-4", "2@0", &["Eb", "F"]),
495            (
496                "C2",
497                "(-3@2 & 4) | (-3@1 & 4@1) | (3@2 & 4@2) | (-3 & 4@3)",
498                &["C", "D", "E", "F", "G", "A", "B", "C"],
499            ),
500            (
501                "C4",
502                "3@0|7@0",
503                &["C", "Eb", "F#", "G", "A", "C", "D", "Eb", "F#", "A"],
504            ),
505            ("C4", "{3@0|4@0}", &["C", "Eb", "E", "F#", "G#", "A", "C"]),
506        ];
507
508        for (tonic_name, expression, expected) in cases {
509            let scale = StepScale::sieve(tonic(tonic_name), expression).expect("sieve realizes");
510            assert_eq!(names(&scale), expected, "{tonic_name} {expression}");
511        }
512    }
513
514    #[test]
515    fn a_sieve_with_no_intervals_errors() {
516        assert!(StepScale::sieve(tonic("C4"), "3@1").is_err());
517        assert!(StepScale::sieve(tonic("C4"), "not a sieve").is_err());
518    }
519
520    #[test]
521    fn malformed_interval_names_error_instead_of_panicking() {
522        assert!(StepScale::cyclical(tonic("C4"), &["nonsense"]).is_err());
523    }
524}