Skip to main content

music21_rs/tuningsystem/
mos.rs

1//! Moments of symmetry — the scales a single generator makes.
2//!
3//! Stack one interval inside a period, fold everything back into that period,
4//! and for certain numbers of notes the result comes out with exactly two step
5//! sizes. That is a *moment of symmetry*, and the diatonic scale is the famous
6//! one: seven fifths folded into an octave give five whole tones and two
7//! semitones, `5L 2s`. Stop at five notes instead and it is the pentatonic,
8//! `2L 3s`; carry on to twelve and every step is the same, which is where the
9//! piano comes from.
10//!
11//! The pattern and the tuning are separate things here. [`Mos`] is the pattern
12//! alone — how many large steps and how many small — and says nothing about
13//! how wide either is. [`MosScale`] is a generator and a period in cents with
14//! a note count, and answers in cents; asking it for its [`MosScale::pattern`]
15//! is what connects the two.
16
17use crate::defaults::{FloatType, UnsignedIntegerType};
18use crate::error::{Error, Result};
19
20use std::fmt::{Display, Formatter};
21use std::str::FromStr;
22
23/// How near two steps must be, in cents, to count as the same size.
24///
25/// Generous next to the floating-point error of folding a generator into a
26/// period, and far under any step distinction a scale actually makes.
27const STEP_TOLERANCE: FloatType = 1e-6;
28
29/// The width of the octave, in cents, which is the usual period.
30pub const OCTAVE_CENTS: FloatType = 1200.0;
31
32/// How many large and small steps a moment of symmetry has.
33///
34/// This is the pattern with no tuning attached — `5L 2s` is the diatonic
35/// scale whether its whole tone is 200 cents or 193, and `2L 5s` is mavila's
36/// anti-diatonic, where the pattern is the same shape but the two step sizes
37/// have traded places.
38///
39/// ```
40/// use music21_rs::tuningsystem::Mos;
41///
42/// let diatonic: Mos = "5L 2s".parse()?;
43/// assert_eq!(diatonic.notes(), 7);
44/// assert_eq!(diatonic.brightest_word(), "LLLsLLs");
45/// # Ok::<(), music21_rs::Error>(())
46/// ```
47#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49#[must_use]
50pub struct Mos {
51    large: UnsignedIntegerType,
52    small: UnsignedIntegerType,
53}
54
55impl Mos {
56    /// Builds a pattern of `large` large steps and `small` small ones.
57    ///
58    /// Errors when neither kind is present, since a scale with no steps at all
59    /// is not a scale.
60    pub fn new(large: UnsignedIntegerType, small: UnsignedIntegerType) -> Result<Self> {
61        if large == 0 && small == 0 {
62            return Err(Error::TuningSystem(
63                "a moment of symmetry needs at least one step".to_owned(),
64            ));
65        }
66        Ok(Self { large, small })
67    }
68
69    /// How many large steps the pattern has.
70    #[must_use]
71    pub fn large(self) -> UnsignedIntegerType {
72        self.large
73    }
74
75    /// How many small steps the pattern has.
76    #[must_use]
77    pub fn small(self) -> UnsignedIntegerType {
78        self.small
79    }
80
81    /// How many notes there are to a period, which is every step counted.
82    #[must_use]
83    pub fn notes(self) -> UnsignedIntegerType {
84        self.large + self.small
85    }
86
87    /// The pattern with its two step sizes traded.
88    ///
89    /// `5L 2s` is the diatonic scale and `2L 5s` is mavila's anti-diatonic:
90    /// the same seven notes generated the same way, with the fifth flat enough
91    /// that what was the large step became the small one.
92    pub fn inverted(self) -> Self {
93        Self {
94            large: self.small,
95            small: self.large,
96        }
97    }
98
99    /// The brightest mode's step word, `L` for a large step and `s` for a small.
100    ///
101    /// Brightest is the mode whose large steps come earliest, which for `5L 2s`
102    /// is Lydian — `LLLsLLs`, the diatonic scale from `F` with no accidentals.
103    #[must_use]
104    pub fn brightest_word(self) -> String {
105        let notes = self.notes();
106        // The upper mechanical word of slope large/notes, which is the mode
107        // with its large steps as early as they will go.
108        (0..notes)
109            .map(|position| {
110                let before = ceiling_ratio(position * self.large, notes);
111                let after = ceiling_ratio((position + 1) * self.large, notes);
112                if after > before { 'L' } else { 's' }
113            })
114            .collect()
115    }
116}
117
118/// `numerator / denominator`, rounded up, in whole numbers.
119fn ceiling_ratio(numerator: UnsignedIntegerType, denominator: UnsignedIntegerType) -> u64 {
120    let numerator = u64::from(numerator);
121    let denominator = u64::from(denominator);
122    numerator.div_ceil(denominator)
123}
124
125impl Display for Mos {
126    /// Writes the pattern the way the literature does, `5L 2s`.
127    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
128        write!(f, "{}L {}s", self.large, self.small)
129    }
130}
131
132impl FromStr for Mos {
133    type Err = Error;
134
135    /// Reads `5L 2s`, with the space optional.
136    fn from_str(text: &str) -> Result<Self> {
137        let malformed =
138            || Error::TuningSystem(format!("{text} is not a pattern; a pattern reads 5L 2s"));
139        let (large, rest) = text
140            .trim()
141            .split_once('L')
142            .ok_or_else(malformed)
143            .map(|(large, rest)| (large.trim(), rest.trim()))?;
144        let small = rest.strip_suffix('s').ok_or_else(malformed)?.trim();
145        Self::new(
146            large.parse().map_err(|_| malformed())?,
147            small.parse().map_err(|_| malformed())?,
148        )
149    }
150}
151
152/// A generator and a period, folded into a scale of so many notes.
153///
154/// The generator and period are cents, so nothing here assumes an octave:
155/// Bohlen-Pierce generates inside a period of `1901.955` cents and works the
156/// same way.
157///
158/// ```
159/// use music21_rs::tuningsystem::{MosScale, OCTAVE_CENTS};
160///
161/// // Seven fifths folded into an octave are the diatonic scale. Stacked
162/// // upwards from the tonic they land in its brightest mode, Lydian.
163/// let diatonic = MosScale::new(701.955, OCTAVE_CENTS, 7)?;
164/// assert_eq!(diatonic.pattern()?.to_string(), "5L 2s");
165/// assert_eq!(diatonic.word()?, "LLLsLLs");
166/// # Ok::<(), music21_rs::Error>(())
167/// ```
168#[derive(Clone, Copy, Debug, PartialEq)]
169#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
170#[must_use]
171pub struct MosScale {
172    generator: FloatType,
173    period: FloatType,
174    notes: UnsignedIntegerType,
175}
176
177impl MosScale {
178    /// Builds a scale of `notes` notes from a generator inside a period.
179    ///
180    /// The generator is folded into the period, so handing over a fifth of
181    /// `701.955` and a fifth of `1901.955` build the same scale. Errors on a
182    /// period that is not positive, on a generator that folds to nothing, and
183    /// on a scale of no notes.
184    pub fn new(
185        generator: FloatType,
186        period: FloatType,
187        notes: UnsignedIntegerType,
188    ) -> Result<Self> {
189        if !period.is_finite() || period <= 0.0 {
190            return Err(Error::TuningSystem(format!(
191                "{period} cents is not a period a scale can be generated in"
192            )));
193        }
194        if !generator.is_finite() {
195            return Err(Error::TuningSystem(
196                "a generator has to be a real number of cents".to_owned(),
197            ));
198        }
199        if notes == 0 {
200            return Err(Error::TuningSystem(
201                "a scale of no notes is not a scale".to_owned(),
202            ));
203        }
204        let folded = generator.rem_euclid(period);
205        if folded < STEP_TOLERANCE || period - folded < STEP_TOLERANCE {
206            return Err(Error::TuningSystem(format!(
207                "a generator of {generator} cents folds to the period itself and generates nothing"
208            )));
209        }
210        Ok(Self {
211            generator: folded,
212            period,
213            notes,
214        })
215    }
216
217    /// Builds a scale whose generator is `steps` of an equal division.
218    ///
219    /// This is the wiki's `3\22` — three steps of 22 equal divisions of the
220    /// period — which is how a generator is usually named once a temperament
221    /// has been pinned to an equal temperament that supports it.
222    pub fn from_equal_division(
223        steps: UnsignedIntegerType,
224        divisions: UnsignedIntegerType,
225        period: FloatType,
226        notes: UnsignedIntegerType,
227    ) -> Result<Self> {
228        if divisions == 0 {
229            return Err(Error::TuningSystem(
230                "a period cannot be divided into no steps".to_owned(),
231            ));
232        }
233        let generator = period * FloatType::from(steps) / FloatType::from(divisions);
234        Self::new(generator, period, notes)
235    }
236
237    /// The generator, in cents, folded into the period.
238    #[must_use]
239    pub fn generator(self) -> FloatType {
240        self.generator
241    }
242
243    /// The period, in cents.
244    #[must_use]
245    pub fn period(self) -> FloatType {
246        self.period
247    }
248
249    /// How many notes there are to a period.
250    #[must_use]
251    pub fn notes(self) -> UnsignedIntegerType {
252        self.notes
253    }
254
255    /// The scale's degrees in cents, rising from nought inside one period.
256    #[must_use]
257    pub fn degrees(self) -> Vec<FloatType> {
258        let mut degrees = (0..self.notes)
259            .map(|index| (FloatType::from(index) * self.generator).rem_euclid(self.period))
260            .collect::<Vec<_>>();
261        degrees.sort_by(|left, right| left.partial_cmp(right).expect("cents are real numbers"));
262        degrees
263    }
264
265    /// The width of each step in cents, from the tonic round to the period.
266    #[must_use]
267    pub fn steps(self) -> Vec<FloatType> {
268        let degrees = self.degrees();
269        degrees
270            .iter()
271            .zip(degrees.iter().skip(1))
272            .map(|(lower, higher)| higher - lower)
273            .chain(std::iter::once(
274                self.period - degrees.last().copied().unwrap_or(0.0),
275            ))
276            .collect()
277    }
278
279    /// The distinct step widths, largest first.
280    fn step_sizes(self) -> Vec<FloatType> {
281        let mut sizes: Vec<FloatType> = Vec::new();
282        for step in self.steps() {
283            if !sizes
284                .iter()
285                .any(|known| (known - step).abs() < STEP_TOLERANCE)
286            {
287                sizes.push(step);
288            }
289        }
290        sizes.sort_by(|left, right| right.partial_cmp(left).expect("cents are real numbers"));
291        sizes
292    }
293
294    /// Whether the scale really is a moment of symmetry — two step sizes, no more.
295    #[must_use]
296    pub fn is_moment_of_symmetry(self) -> bool {
297        self.step_sizes().len() == 2
298    }
299
300    /// Whether every step came out the same width, which is an equal division.
301    #[must_use]
302    pub fn is_equal(self) -> bool {
303        self.step_sizes().len() == 1
304    }
305
306    /// How many large and small steps the scale has.
307    ///
308    /// Errors when the scale is not a moment of symmetry at all: either every
309    /// step is the same width, which is an equal division, or there are three
310    /// widths or more, which is the note count between two moments.
311    pub fn pattern(self) -> Result<Mos> {
312        let sizes = self.step_sizes();
313        let [large, small] = sizes[..] else {
314            return Err(Error::TuningSystem(format!(
315                "{} notes of a generator of {:.3} cents give {} step sizes, not two",
316                self.notes,
317                self.generator,
318                sizes.len()
319            )));
320        };
321        let count = |width: FloatType| {
322            self.steps()
323                .iter()
324                .filter(|step| (*step - width).abs() < STEP_TOLERANCE)
325                .count() as UnsignedIntegerType
326        };
327        Mos::new(count(large), count(small))
328    }
329
330    /// The scale's own step word, from its tonic rather than from its brightest mode.
331    ///
332    /// Errors where [`MosScale::pattern`] does.
333    pub fn word(self) -> Result<String> {
334        let sizes = self.step_sizes();
335        let [large, _] = sizes[..] else {
336            let _ = self.pattern()?;
337            unreachable!("pattern rejects anything but two step sizes");
338        };
339        Ok(self
340            .steps()
341            .iter()
342            .map(|step| {
343                if (step - large).abs() < STEP_TOLERANCE {
344                    'L'
345                } else {
346                    's'
347                }
348            })
349            .collect())
350    }
351}
352
353/// Every note count up to `most` at which a generator makes a moment of symmetry.
354///
355/// This is the list a temperament page gives under "MOS scales": for the fifth
356/// it is 2, 3, 4, 5, 7, 12, 17… — the pentatonic and the diatonic among them.
357/// A count where every step comes out the same width is left out, since an
358/// equal division is not a moment of symmetry.
359pub fn moment_of_symmetry_sizes(
360    generator: FloatType,
361    period: FloatType,
362    most: UnsignedIntegerType,
363) -> Result<Vec<UnsignedIntegerType>> {
364    // Built once so a bad generator or period is an error rather than a silence.
365    let _ = MosScale::new(generator, period, 1)?;
366    Ok((2..=most)
367        .filter(|&notes| {
368            MosScale::new(generator, period, notes).is_ok_and(MosScale::is_moment_of_symmetry)
369        })
370        .collect())
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376
377    #[test]
378    fn a_pattern_is_read_from_its_name() {
379        let diatonic: Mos = "5L 2s".parse().unwrap();
380        assert_eq!(diatonic.notes(), 7);
381        assert_eq!("2L5s".parse::<Mos>().unwrap().notes(), 7);
382        assert!("5L".parse::<Mos>().is_err());
383        assert!("xL 2s".parse::<Mos>().is_err());
384    }
385
386    #[test]
387    fn the_diatonic_scale_is_five_large_steps_and_two_small() {
388        let diatonic = MosScale::new(701.955, OCTAVE_CENTS, 7).expect("a fifth");
389        assert!(diatonic.is_moment_of_symmetry());
390        assert_eq!(diatonic.pattern().expect("a moment").to_string(), "5L 2s");
391        // Stacked upwards from the tonic the fifths land in Lydian, the mode
392        // with its large steps as early as they go.
393        assert_eq!(diatonic.word().expect("a moment"), "LLLsLLs");
394        assert_eq!(diatonic.degrees().len(), 7);
395        let steps = diatonic.steps();
396        assert!((steps[0] - 203.910).abs() < 1e-3, "{steps:?}");
397        assert!((steps[3] - 90.225).abs() < 1e-3, "{steps:?}");
398        assert!((steps.iter().sum::<FloatType>() - OCTAVE_CENTS).abs() < 1e-9);
399    }
400
401    #[test]
402    fn the_pentatonic_is_the_moment_before_the_diatonic() {
403        let pentatonic = MosScale::new(701.955, OCTAVE_CENTS, 5).expect("a fifth");
404        assert_eq!(pentatonic.pattern().expect("a moment").to_string(), "2L 3s");
405    }
406
407    #[test]
408    fn twelve_fifths_come_out_equal_and_so_are_not_a_moment() {
409        let chromatic = MosScale::new(700.0, OCTAVE_CENTS, 12).expect("a fifth");
410        assert!(chromatic.is_equal());
411        assert!(!chromatic.is_moment_of_symmetry());
412        assert!(chromatic.pattern().is_err());
413    }
414
415    #[test]
416    fn a_count_between_two_moments_has_three_step_sizes() {
417        let six = MosScale::new(701.955, OCTAVE_CENTS, 6).expect("a fifth");
418        assert!(!six.is_moment_of_symmetry());
419        assert!(six.pattern().is_err());
420    }
421
422    /// Porcupine, mavila and blackwood, each read off the equal temperament
423    /// the wiki names their generator in.
424    #[test]
425    fn named_temperaments_give_the_patterns_they_are_listed_with() {
426        for (steps, divisions, notes, pattern) in [
427            (3, 22, 7, "1L 6s"),  // porcupine[7], generator 3\22
428            (3, 22, 8, "7L 1s"),  // porcupine[8]
429            (9, 16, 7, "2L 5s"),  // mavila[7], the anti-diatonic, generator 9\16
430            (9, 16, 9, "7L 2s"),  // mavila[9]
431            (13, 23, 7, "2L 5s"), // mavila again, its fifth read in 23edo
432            (7, 12, 7, "5L 2s"),  // the diatonic scale in 12edo
433            (7, 12, 5, "2L 3s"),  // the pentatonic in 12edo
434            (18, 31, 7, "5L 2s"), // meantone's fifth in 31edo
435            // A generator that is not a fifth at all says so rather than
436            // being forced into the pattern of one.
437            (5, 17, 7, "3L 4s"),
438        ] {
439            let scale = MosScale::from_equal_division(steps, divisions, OCTAVE_CENTS, notes)
440                .expect("a generator");
441            assert_eq!(
442                scale.pattern().expect("a moment").to_string(),
443                pattern,
444                "{steps}\\{divisions} at {notes} notes"
445            );
446        }
447    }
448
449    #[test]
450    fn a_pattern_writes_and_reads_the_way_it_is_named() {
451        let diatonic = Mos::new(5, 2).expect("a pattern");
452        assert_eq!(diatonic.to_string(), "5L 2s");
453        assert_eq!("5L 2s".parse::<Mos>().expect("a pattern"), diatonic);
454        assert_eq!("5L2s".parse::<Mos>().expect("a pattern"), diatonic);
455        assert_eq!(diatonic.notes(), 7);
456        assert_eq!(diatonic.inverted().to_string(), "2L 5s");
457        assert!("5L".parse::<Mos>().is_err());
458        assert!("five L 2s".parse::<Mos>().is_err());
459        assert!(Mos::new(0, 0).is_err());
460    }
461
462    /// The brightest mode of the diatonic is Lydian, which is where the word starts.
463    #[test]
464    fn the_brightest_mode_puts_its_large_steps_first() {
465        for (pattern, word) in [
466            ("5L 2s", "LLLsLLs"),
467            ("2L 5s", "LssLsss"),
468            ("1L 6s", "Lssssss"),
469            ("7L 1s", "LLLLLLLs"),
470            ("2L 3s", "LsLss"),
471            ("3L 4s", "LsLsLss"),
472        ] {
473            let mos: Mos = pattern.parse().expect("a pattern");
474            assert_eq!(mos.brightest_word(), word, "{pattern}");
475            assert_eq!(mos.brightest_word().len(), mos.notes() as usize);
476        }
477    }
478
479    #[test]
480    fn the_moments_of_a_fifth_are_the_ones_the_wiki_lists() {
481        // Four fifths give three step sizes, so four notes is not among them —
482        // the moments are the two familiar scales and the counts around them.
483        let sizes = moment_of_symmetry_sizes(701.955, OCTAVE_CENTS, 12).expect("a fifth");
484        assert_eq!(sizes, vec![2, 3, 5, 7, 12]);
485    }
486
487    /// Nothing here assumes an octave: Bohlen-Pierce generates in a twelfth.
488    #[test]
489    fn a_period_need_not_be_an_octave() {
490        let tritave = 1200.0 * 3.0_f64.log2();
491        let lambda = MosScale::from_equal_division(3, 13, tritave, 9).expect("a generator");
492        assert_eq!(lambda.pattern().expect("a moment").to_string(), "4L 5s");
493        assert!((lambda.period() - 1901.955).abs() < 1e-3);
494        assert!((lambda.steps().iter().sum::<FloatType>() - tritave).abs() < 1e-9);
495    }
496
497    #[test]
498    fn a_generator_that_folds_to_the_period_generates_nothing() {
499        assert!(MosScale::new(1200.0, OCTAVE_CENTS, 7).is_err());
500        assert!(MosScale::new(0.0, OCTAVE_CENTS, 7).is_err());
501        assert!(MosScale::new(701.955, 0.0, 7).is_err());
502        assert!(MosScale::new(701.955, OCTAVE_CENTS, 0).is_err());
503        assert!(MosScale::from_equal_division(3, 0, OCTAVE_CENTS, 7).is_err());
504        assert!(moment_of_symmetry_sizes(0.0, OCTAVE_CENTS, 12).is_err());
505    }
506
507    #[test]
508    fn a_generator_is_folded_into_its_period() {
509        let fifth = MosScale::new(701.955, OCTAVE_CENTS, 7).expect("a fifth");
510        let twelfth = MosScale::new(1901.955, OCTAVE_CENTS, 7).expect("a twelfth");
511        assert!((fifth.generator() - twelfth.generator()).abs() < 1e-9);
512        assert_eq!(
513            fifth.pattern().expect("a moment"),
514            twelfth.pattern().expect("a moment")
515        );
516    }
517}