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::error::Result;
17use crate::interval::Interval;
18use crate::pitch::Pitch;
19use crate::sieve::Sieve;
20
21use std::sync::LazyLock;
22
23/// music21 defaults an absent interval list to a single minor second.
24static DEFAULT_STEP: LazyLock<Interval> =
25    LazyLock::new(|| Interval::from_name("m2").expect("m2 is a valid interval name"));
26
27/// A scale built by walking a cycle of intervals from a tonic.
28///
29/// ```
30/// use music21_rs::{Pitch, StepScale};
31///
32/// let tonic = Pitch::from_name("C4")?;
33/// let cyclical = StepScale::cyclical(tonic.clone(), &["m3", "M3"])?;
34/// let names: Vec<String> = cyclical.pitches()?.iter().map(|p| p.name()).collect();
35/// assert_eq!(names, ["C", "E-", "G"]);
36///
37/// let repeating = StepScale::octave_repeating(tonic, &["m3", "M3"])?;
38/// let names: Vec<String> = repeating.pitches()?.iter().map(|p| p.name()).collect();
39/// assert_eq!(names, ["C", "E-", "G", "C"]);
40/// # Ok::<(), music21_rs::Error>(())
41/// ```
42/// `Interval` implements neither `PartialEq` nor `Hash`, so neither is derived
43/// here; compare realized [`pitches`](StepScale::pitches) instead.
44#[derive(Clone, Debug)]
45#[must_use]
46pub struct StepScale {
47    tonic: Pitch,
48    steps: Vec<Interval>,
49}
50
51impl StepScale {
52    /// Builds music21's `CyclicalScale`: the intervals walked once, no closing.
53    ///
54    /// An empty list defaults to a single `m2`, as music21 does.
55    pub fn cyclical(tonic: Pitch, steps: &[&str]) -> Result<Self> {
56        Ok(Self {
57            tonic,
58            steps: parse_steps(steps)?,
59        })
60    }
61
62    /// Builds music21's `OctaveRepeatingScale`: the intervals plus a closing
63    /// interval that completes the octave.
64    ///
65    /// An empty list defaults to a single `m2`, as music21 does.
66    ///
67    /// The closing interval is the *complement of the interval sum*, which is
68    /// how music21 derives it — not the interval between the last realized
69    /// pitch and the octave. The two differ in spelling whenever realization
70    /// respells a degree: three `m2` steps from C sum to `dd4`, whose
71    /// complement is `AA5`, so the scale closes on `B#` rather than `C`. Taking
72    /// it from the realized pitches would read the answer off already-simplified
73    /// output and lose that.
74    ///
75    /// music21's own behaviour for a cycle wider than an octave is erratic —
76    /// `["P5", "P5"]` returns pitches an octave above the tonic it was given,
77    /// and it mutates the caller's interval list in place. Neither is
78    /// reproduced here: the cycle is closed at the octave above the last pitch
79    /// and the input is left alone.
80    pub fn octave_repeating(tonic: Pitch, steps: &[&str]) -> Result<Self> {
81        let mut steps = parse_steps(steps)?;
82        steps.push(interval_sum(&tonic, &steps)?.inversion()?);
83        Ok(Self { tonic, steps })
84    }
85
86    /// Builds music21's `SieveScale` from a Xenakis sieve expression.
87    ///
88    /// The sieve's interval widths become the cycle, which music21 then treats
89    /// as a `CyclicalScale` — so `"3@0"` from C is `C E-`, and the major-scale
90    /// sieve gives a major scale.
91    ///
92    /// music21's `SieveScale` also takes an `eld` (elementary displacement) to
93    /// scale the widths for non-semitone steps. Only the default of one
94    /// semitone is supported here, since the crate has no microtonal step type
95    /// to widen to.
96    pub fn sieve(tonic: Pitch, expression: &str) -> Result<Self> {
97        let widths = Sieve::parse(expression)?.interval_widths()?;
98        let steps = widths
99            .into_iter()
100            .map(Interval::from_semitones)
101            .collect::<Result<Vec<_>>>()?;
102        Ok(Self { tonic, steps })
103    }
104
105    /// Returns the tonic pitch.
106    pub fn tonic(&self) -> &Pitch {
107        &self.tonic
108    }
109
110    /// Returns the step intervals, including any closing interval.
111    pub fn steps(&self) -> &[Interval] {
112        &self.steps
113    }
114
115    /// Returns the number of steps, which is one fewer than the pitch count.
116    pub fn degree_count(&self) -> usize {
117        self.steps.len()
118    }
119
120    /// Returns the pitches of one pass through the cycle, starting at the tonic.
121    pub fn pitches(&self) -> Result<Vec<Pitch>> {
122        let mut pitches = Vec::with_capacity(self.steps.len() + 1);
123        pitches.push(self.tonic.clone());
124
125        let mut current = self.tonic.clone();
126        for step in &self.steps {
127            // music21's IntervalNetwork defaults to pitchSimplification
128            // 'maxAccidental' with a cap of one, which is what respells the
129            // third step of an m2 cycle from E-double-flat to D.
130            current = step.transpose_pitch_with_options(&current, false, Some(1))?;
131            pitches.push(current.clone());
132        }
133        Ok(pitches)
134    }
135}
136
137/// Returns the sum of `steps`, spelled exactly.
138///
139/// Computed by transposing a reference pitch with simplification switched off
140/// and measuring the result, so the sum keeps the accidentals the arithmetic
141/// actually produces rather than the ones a realized scale would show.
142fn interval_sum(reference: &Pitch, steps: &[Interval]) -> Result<Interval> {
143    let mut current = reference.clone();
144    for step in steps {
145        current = step.transpose_pitch_with_options(&current, false, None)?;
146    }
147    Interval::between_pitches(reference, &current)
148}
149
150fn parse_steps(steps: &[&str]) -> Result<Vec<Interval>> {
151    if steps.is_empty() {
152        return Ok(vec![DEFAULT_STEP.clone()]);
153    }
154    steps
155        .iter()
156        .map(|name| Interval::from_name(*name))
157        .collect()
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn a_step_scale_reports_its_tonic_and_steps() {
166        let tonic = Pitch::from_name("C4").unwrap();
167        let scale = StepScale::cyclical(tonic.clone(), &["M2", "M2"]).unwrap();
168        assert_eq!(scale.tonic(), &tonic);
169        assert_eq!(scale.steps().len(), 2);
170        assert_eq!(scale.degree_count(), 2);
171    }
172
173    fn names(scale: &StepScale) -> Vec<String> {
174        scale
175            .pitches()
176            .expect("scale realizes")
177            .iter()
178            .map(|pitch| pitch.name())
179            .collect()
180    }
181
182    fn tonic(name: &str) -> Pitch {
183        Pitch::from_name(name).expect("valid tonic")
184    }
185
186    #[test]
187    fn cyclical_walks_the_cycle_once() {
188        let scale = StepScale::cyclical(tonic("C4"), &["P5"]).unwrap();
189        assert_eq!(names(&scale), ["C", "G"]);
190
191        let scale = StepScale::cyclical(tonic("C4"), &["m3", "M3"]).unwrap();
192        assert_eq!(names(&scale), ["C", "E-", "G"]);
193    }
194
195    #[test]
196    fn octave_repeating_closes_on_the_octave() {
197        let scale = StepScale::octave_repeating(tonic("C4"), &["m3", "M3"]).unwrap();
198        assert_eq!(names(&scale), ["C", "E-", "G", "C"]);
199        let pitches = scale.pitches().unwrap();
200        assert_eq!(pitches.first().unwrap().octave(), Some(4));
201        assert_eq!(pitches.last().unwrap().octave(), Some(5));
202    }
203
204    #[test]
205    fn an_empty_interval_list_defaults_to_a_minor_second() {
206        // music21: CyclicalScale() is [C4, D-4]; OctaveRepeatingScale() is
207        // [C4, D-4, C5].
208        assert_eq!(
209            names(&StepScale::cyclical(tonic("C4"), &[]).unwrap()),
210            ["C", "D-"]
211        );
212        assert_eq!(
213            names(&StepScale::octave_repeating(tonic("C4"), &[]).unwrap()),
214            ["C", "D-", "C"]
215        );
216    }
217
218    #[test]
219    fn the_closing_interval_comes_from_the_interval_sum_not_the_pitches() {
220        // Three m2 steps sum to dd4, whose complement is AA5. Realization
221        // respells the third degree from E-double-flat to D, so reading the
222        // closing interval off the pitches would give a major sixth to C
223        // instead. music21 closes on B#, and so does this.
224        let scale = StepScale::octave_repeating(tonic("C4"), &["m2", "m2", "m2"]).unwrap();
225        assert_eq!(names(&scale), ["C", "D-", "D", "E-", "B#"]);
226
227        // Where realization changes nothing, the two agree: M2+M2+m2 is P4 and
228        // its complement P5 closes on the octave.
229        let scale = StepScale::octave_repeating(tonic("C4"), &["M2", "M2", "m2"]).unwrap();
230        assert_eq!(names(&scale), ["C", "D", "E", "F", "C"]);
231        let scale = StepScale::octave_repeating(tonic("F#4"), &["M2", "M2", "m2"]).unwrap();
232        assert_eq!(names(&scale), ["F#", "G#", "A#", "B", "F#"]);
233    }
234
235    #[test]
236    fn a_cycle_wider_than_an_octave_closes_above_it() {
237        // Two fifths sum to M9, whose complement is m7, so the cycle closes an
238        // octave higher rather than folding back. music21 agrees on the
239        // intervals here but reports the pitches an octave off its own tonic.
240        let scale = StepScale::octave_repeating(tonic("C4"), &["P5", "P5"]).unwrap();
241        assert_eq!(names(&scale), ["C", "G", "D", "C"]);
242        let pitches = scale.pitches().unwrap();
243        assert_eq!(pitches.first().unwrap().octave(), Some(4));
244        assert_eq!(pitches.last().unwrap().octave(), Some(6));
245    }
246
247    #[test]
248    fn sieve_scales_match_music21() {
249        let cases: [(&str, &str, &[&str]); 6] = [
250            ("C4", "3@0", &["C", "E-"]),
251            ("D4", "3@0", &["D", "F"]),
252            ("E-4", "2@0", &["E-", "F"]),
253            (
254                "C2",
255                "(-3@2 & 4) | (-3@1 & 4@1) | (3@2 & 4@2) | (-3 & 4@3)",
256                &["C", "D", "E", "F", "G", "A", "B", "C"],
257            ),
258            (
259                "C4",
260                "3@0|7@0",
261                &["C", "E-", "F#", "G", "A", "C", "D", "E-", "F#", "A"],
262            ),
263            ("C4", "{3@0|4@0}", &["C", "E-", "E", "F#", "G#", "A", "C"]),
264        ];
265
266        for (tonic_name, expression, expected) in cases {
267            let scale = StepScale::sieve(tonic(tonic_name), expression).expect("sieve realizes");
268            assert_eq!(names(&scale), expected, "{tonic_name} {expression}");
269        }
270    }
271
272    #[test]
273    fn a_sieve_with_no_intervals_errors() {
274        assert!(StepScale::sieve(tonic("C4"), "3@1").is_err());
275        assert!(StepScale::sieve(tonic("C4"), "not a sieve").is_err());
276    }
277
278    #[test]
279    fn malformed_interval_names_error_instead_of_panicking() {
280        assert!(StepScale::cyclical(tonic("C4"), &["nonsense"]).is_err());
281    }
282}