Skip to main content

music21_rs/tuningsystem/
equal.rs

1//! Equal divisions of any interval, not only of the octave.
2//!
3//! Plenty of tunings do not repeat at the octave: Bohlen-Pierce divides a
4//! twelfth into thirteen and never sounds an octave, and Carlos's Alpha, Beta
5//! and Gamma repeat at nothing in particular. An [`EqualDivision`] divides
6//! whatever period it is given, the octave included, and it is what
7//! [`crate::tuningsystem::TuningSystem::Equal`] holds.
8//!
9//! The period is kept as cents so that any interval can be one, and the ratio
10//! it came from is kept beside it so the division can still name itself the
11//! way the literature does — `13edt` rather than `13 equal steps of 1901.955
12//! cents`.
13//!
14//! ```
15//! use music21_rs::tuningsystem::EqualDivision;
16//!
17//! let bohlen_pierce = EqualDivision::tritave(13)?;
18//! assert_eq!(bohlen_pierce.to_string(), "13edt");
19//! assert!((bohlen_pierce.step_cents() - 146.304).abs() < 1e-3);
20//! // Its ninth degree is nowhere near an octave, which is the point of it.
21//! assert!((bohlen_pierce.cents_at(8) - 1170.4).abs() < 0.1);
22//! # Ok::<(), music21_rs::Error>(())
23//! ```
24
25use crate::defaults::{FloatType, IntegerType, UnsignedIntegerType};
26use crate::error::{Error, Result};
27use crate::tuningsystem::monzo::{Monzo, PRIMES, Val};
28use crate::tuningsystem::mos::{MosScale, OCTAVE_CENTS};
29
30use std::fmt::{Display, Formatter};
31use std::str::FromStr;
32
33/// The tritave, `3/1`, in cents — Bohlen-Pierce's period.
34pub const TRITAVE_CENTS: FloatType = 1_901.955_000_865_388_7;
35
36/// An equal division of an arbitrary interval.
37///
38/// `12edo` is twelve equal divisions of the octave, `13edt` thirteen of the
39/// tritave. Nothing here assumes the period is an octave, so a degree past the
40/// period simply carries on into the next one — which for Bohlen-Pierce means
41/// the pitches never line up with an octave at all.
42#[derive(Clone, Copy, Debug, PartialEq)]
43#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
44#[must_use]
45pub struct EqualDivision {
46    divisions: UnsignedIntegerType,
47    period_cents: FloatType,
48    period_ratio: Option<(IntegerType, IntegerType)>,
49}
50
51impl EqualDivision {
52    /// Divides a period given in cents.
53    ///
54    /// Errors on a period that is not a positive real number, or on no
55    /// divisions at all.
56    pub fn new(divisions: UnsignedIntegerType, period_cents: FloatType) -> Result<Self> {
57        if divisions == 0 {
58            return Err(Error::TuningSystem(
59                "a period cannot be divided into no steps".to_owned(),
60            ));
61        }
62        if !period_cents.is_finite() || period_cents <= 0.0 {
63            return Err(Error::TuningSystem(format!(
64                "{period_cents} cents is not an interval that can be divided"
65            )));
66        }
67        Ok(Self {
68            divisions,
69            period_cents,
70            period_ratio: None,
71        })
72    }
73
74    /// Divides the interval `numerator/denominator`.
75    ///
76    /// The ratio is remembered, so the division names itself after it.
77    pub fn of_ratio(
78        divisions: UnsignedIntegerType,
79        numerator: IntegerType,
80        denominator: IntegerType,
81    ) -> Result<Self> {
82        if numerator <= 0 || denominator <= 0 || numerator == denominator {
83            return Err(Error::TuningSystem(format!(
84                "{numerator}/{denominator} is not an interval that can be divided"
85            )));
86        }
87        let ratio = FloatType::from(numerator) / FloatType::from(denominator);
88        let mut division = Self::new(divisions, OCTAVE_CENTS * ratio.log2())?;
89        division.period_ratio = Some((numerator, denominator));
90        Ok(division)
91    }
92
93    /// Divides the octave — an EDO, and what everyone means by default.
94    pub fn octave(divisions: UnsignedIntegerType) -> Result<Self> {
95        Self::of_ratio(divisions, 2, 1)
96    }
97
98    /// Divides the octave into a count that cannot be nought, so it cannot
99    /// fail and can build a constant: `EqualDivision::edo(NonZeroU32::MIN)`
100    /// is a single step to the octave.
101    pub const fn edo(divisions: std::num::NonZeroU32) -> Self {
102        Self {
103            divisions: divisions.get(),
104            period_cents: OCTAVE_CENTS,
105            period_ratio: Some((2, 1)),
106        }
107    }
108
109    /// Divides the tritave, `3/1` — an EDT, which is Bohlen-Pierce's period.
110    pub fn tritave(divisions: UnsignedIntegerType) -> Result<Self> {
111        Self::of_ratio(divisions, 3, 1)
112    }
113
114    /// How many steps the period is divided into.
115    #[must_use]
116    pub fn divisions(&self) -> UnsignedIntegerType {
117        self.divisions
118    }
119
120    /// The period, in cents.
121    #[must_use]
122    pub fn period_cents(&self) -> FloatType {
123        self.period_cents
124    }
125
126    /// The ratio the period was given as, where it was given as one.
127    #[must_use]
128    pub fn period_ratio(&self) -> Option<(IntegerType, IntegerType)> {
129        self.period_ratio
130    }
131
132    /// Whether the period is an octave, which is what makes this an ordinary EDO.
133    #[must_use]
134    pub fn repeats_at_the_octave(&self) -> bool {
135        (self.period_cents - OCTAVE_CENTS).abs() < 1e-9
136    }
137
138    /// One step, in cents.
139    #[must_use]
140    pub fn step_cents(&self) -> FloatType {
141        self.period_cents / FloatType::from(self.divisions)
142    }
143
144    /// How far above the tonic `degree` steps sit, in cents.
145    ///
146    /// A degree past the period carries on into the next one, and a negative
147    /// degree goes below the tonic.
148    #[must_use]
149    pub fn cents_at(&self, degree: IntegerType) -> FloatType {
150        FloatType::from(degree) * self.step_cents()
151    }
152
153    /// The frequency ratio of `degree` steps above the tonic.
154    #[must_use]
155    pub fn ratio_at(&self, degree: IntegerType) -> FloatType {
156        (2.0 as FloatType).powf(self.cents_at(degree) / OCTAVE_CENTS)
157    }
158
159    /// Every degree of one period, in cents, from nought up to but not including the period.
160    #[must_use]
161    pub fn degrees(&self) -> Vec<FloatType> {
162        (0..self.divisions)
163            .map(|degree| FloatType::from(degree) * self.step_cents())
164            .collect()
165    }
166
167    /// The degree nearest `cents` above the tonic.
168    #[must_use]
169    pub fn nearest_degree(&self, cents: FloatType) -> IntegerType {
170        (cents / self.step_cents()).round() as IntegerType
171    }
172
173    /// How far the nearest degree sits from `cents`; positive is sharp.
174    #[must_use]
175    pub fn error_at_cents(&self, cents: FloatType) -> FloatType {
176        self.cents_at(self.nearest_degree(cents)) - cents
177    }
178
179    /// The degree nearest `interval`, and how far off it sounds in cents.
180    ///
181    /// This is how well the division does a just interval, which for an EDO is
182    /// the whole question of whether it is worth using.
183    #[must_use]
184    pub fn approximation_of(&self, interval: &Monzo) -> (IntegerType, FloatType) {
185        let cents = interval.cents();
186        (self.nearest_degree(cents), self.error_at_cents(cents))
187    }
188
189    /// The patent val of this division, read up to `limit`.
190    ///
191    /// Each prime is mapped to the nearest whole number of *steps*, which for
192    /// an octave division is the familiar patent val. For a period that is not
193    /// an octave this is the generalized reading, and the octave itself then
194    /// gets a mapping like any other prime rather than being the period.
195    /// Errors for a limit that is not a prime the module carries.
196    pub fn patent_val(&self, limit: IntegerType) -> Result<Val> {
197        let width = PRIMES
198            .iter()
199            .position(|&prime| prime == limit)
200            .map(|index| index + 1)
201            .ok_or_else(|| {
202                Error::TuningSystem(format!("{limit} is not a prime this module carries"))
203            })?;
204        Ok(Val::new(
205            PRIMES[..width]
206                .iter()
207                .map(|&prime| self.nearest_degree(OCTAVE_CENTS * FloatType::from(prime).log2()))
208                .collect::<Vec<_>>(),
209        ))
210    }
211
212    /// The scale of `notes` notes made by stacking `steps` of this division.
213    ///
214    /// This is the wiki's `3\22` — three steps of twenty-two — handed to the
215    /// moment-of-symmetry machinery.
216    pub fn mos(&self, steps: UnsignedIntegerType, notes: UnsignedIntegerType) -> Result<MosScale> {
217        MosScale::from_equal_division(steps, self.divisions, self.period_cents, notes)
218    }
219}
220
221impl Display for EqualDivision {
222    /// Names the division the way the literature does: `12edo`, `13edt`, `9ed3/2`.
223    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
224        write!(f, "{}", self.divisions)?;
225        match self.period_ratio {
226            Some((2, 1)) => write!(f, "edo"),
227            Some((3, 1)) => write!(f, "edt"),
228            Some((numerator, 1)) => write!(f, "ed{numerator}"),
229            Some((numerator, denominator)) => write!(f, "ed{numerator}/{denominator}"),
230            None => write!(f, "ed{:.4}c", self.period_cents),
231        }
232    }
233}
234
235impl FromStr for EqualDivision {
236    type Err = Error;
237
238    /// Reads `12edo`, `13edt`, `13ed3` and `9ed3/2`.
239    fn from_str(text: &str) -> Result<Self> {
240        let text = text.trim();
241        let malformed =
242            || Error::TuningSystem(format!("{text} is not an equal division; one reads 13edt"));
243        let (count, period) = text.split_once("ed").ok_or_else(malformed)?;
244        let divisions = count.parse().map_err(|_| malformed())?;
245        match period {
246            "o" => Self::octave(divisions),
247            "t" => Self::tritave(divisions),
248            // An irrational period, written in cents as `Display` writes it.
249            cents if cents.ends_with('c') => Self::new(
250                divisions,
251                cents[..cents.len() - 1].parse().map_err(|_| malformed())?,
252            ),
253            _ => {
254                let (numerator, denominator) = match period.split_once('/') {
255                    Some((numerator, denominator)) => (numerator, denominator),
256                    None => (period, "1"),
257                };
258                Self::of_ratio(
259                    divisions,
260                    numerator.parse().map_err(|_| malformed())?,
261                    denominator.parse().map_err(|_| malformed())?,
262                )
263            }
264        }
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn an_equal_division_is_read_from_its_name() {
274        assert_eq!("13edt".parse::<EqualDivision>().unwrap().divisions(), 13);
275        assert_eq!("12edo".parse::<EqualDivision>().unwrap().divisions(), 12);
276        assert_eq!("9ed3/2".parse::<EqualDivision>().unwrap().divisions(), 9);
277        assert_eq!("13ed3".parse::<EqualDivision>().unwrap().divisions(), 13);
278        assert!("13".parse::<EqualDivision>().is_err());
279        assert!("xedo".parse::<EqualDivision>().is_err());
280        assert!("9ed3/x".parse::<EqualDivision>().is_err());
281    }
282
283    #[test]
284    fn an_octave_division_is_the_equal_temperament_everyone_means() {
285        let twelve = EqualDivision::octave(12).expect("an octave");
286        assert_eq!(twelve.to_string(), "12edo");
287        assert!(twelve.repeats_at_the_octave());
288        assert!((twelve.step_cents() - 100.0).abs() < 1e-9);
289        assert!((twelve.cents_at(7) - 700.0).abs() < 1e-9);
290        assert!((twelve.ratio_at(12) - 2.0).abs() < 1e-9);
291        assert_eq!(twelve.degrees().len(), 12);
292        // The patent val of an octave division is the familiar one.
293        assert_eq!(
294            twelve.patent_val(5).expect("the 5-limit"),
295            Val::new([12, 19, 28])
296        );
297    }
298
299    #[test]
300    fn bohlen_pierce_divides_a_twelfth_and_never_reaches_an_octave() {
301        let bohlen_pierce = EqualDivision::tritave(13).expect("a tritave");
302        assert_eq!(bohlen_pierce.to_string(), "13edt");
303        assert!(!bohlen_pierce.repeats_at_the_octave());
304        assert!((bohlen_pierce.period_cents() - TRITAVE_CENTS).abs() < 1e-6);
305        assert!((bohlen_pierce.step_cents() - 146.3042).abs() < 1e-4);
306        // No degree lands on an octave: the nearest is eight steps, and it is
307        // nearly thirty cents flat.
308        let octave = Monzo::from_ratio(2, 1).expect("the octave");
309        let (degree, error) = bohlen_pierce.approximation_of(&octave);
310        assert_eq!(degree, 8);
311        assert!((error + 29.6).abs() < 0.1, "{error}");
312        // Its own period, though, is exact.
313        let tritave = Monzo::from_ratio(3, 1).expect("the tritave");
314        assert!(bohlen_pierce.approximation_of(&tritave).1.abs() < 1e-6);
315    }
316
317    /// Carlos Alpha, Beta and Gamma repeat at nothing in particular, which is
318    /// exactly why they cannot be a `TuningSystem` variant.
319    #[test]
320    fn the_carlos_scales_have_periods_that_are_not_intervals_at_all() {
321        for (name, period, divisions, step) in [
322            ("alpha", 1404.0, 9, 156.0),
323            ("beta", 1403.6, 11, 127.6),
324            ("gamma", 1228.465, 20, 61.42),
325        ] {
326            let division = EqualDivision::new(divisions, period).expect("a period");
327            assert!(!division.repeats_at_the_octave(), "{name}");
328            assert!((division.step_cents() - step).abs() < 0.01, "{name}");
329            assert_eq!(division.period_ratio(), None, "{name}");
330            // With no ratio to name it after, it says its period in cents.
331            assert!(division.to_string().ends_with('c'), "{name}");
332        }
333    }
334
335    #[test]
336    fn how_well_a_division_does_the_just_intervals_is_the_whole_question() {
337        let twelve = EqualDivision::octave(12).expect("an octave");
338        let fifth = Monzo::from_ratio(3, 2).expect("the fifth");
339        let third = Monzo::from_ratio(5, 4).expect("the third");
340        assert_eq!(twelve.approximation_of(&fifth).0, 7);
341        assert!((twelve.approximation_of(&fifth).1 + 1.955).abs() < 1e-3);
342        // The famously sharp third.
343        assert!((twelve.approximation_of(&third).1 - 13.686).abs() < 1e-3);
344
345        // 31edo was built to do the third better, and does.
346        let thirty_one = EqualDivision::octave(31).expect("an octave");
347        assert!(thirty_one.approximation_of(&third).1.abs() < 1.0);
348        // 53edo does nearly everything in the 5-limit.
349        let fifty_three = EqualDivision::octave(53).expect("an octave");
350        assert!(fifty_three.approximation_of(&fifth).1.abs() < 0.1);
351        assert!(fifty_three.approximation_of(&third).1.abs() < 1.5);
352    }
353
354    #[test]
355    fn a_division_hands_its_steps_to_the_moment_of_symmetry_machinery() {
356        let twenty_two = EqualDivision::octave(22).expect("an octave");
357        assert_eq!(
358            twenty_two
359                .mos(3, 7)
360                .expect("a generator")
361                .pattern()
362                .expect("a moment")
363                .to_string(),
364            "1L 6s"
365        );
366        let bohlen_pierce = EqualDivision::tritave(13).expect("a tritave");
367        assert_eq!(
368            bohlen_pierce
369                .mos(3, 9)
370                .expect("a generator")
371                .pattern()
372                .expect("a moment")
373                .to_string(),
374            "4L 5s"
375        );
376    }
377
378    #[test]
379    fn a_division_writes_and_reads_the_way_it_is_named() {
380        for name in ["12edo", "13edt", "9ed3/2", "5ed5"] {
381            let division: EqualDivision = name.parse().expect("a division");
382            assert_eq!(division.to_string(), name);
383        }
384        // `13ed3` is the tritave written the long way, and names itself back
385        // the short way.
386        assert_eq!(
387            "13ed3"
388                .parse::<EqualDivision>()
389                .expect("a division")
390                .to_string(),
391            "13edt"
392        );
393        assert!("edo".parse::<EqualDivision>().is_err());
394        assert!("12".parse::<EqualDivision>().is_err());
395        assert!("12edx".parse::<EqualDivision>().is_err());
396        assert!("0edo".parse::<EqualDivision>().is_err());
397    }
398
399    #[test]
400    fn a_period_that_is_not_an_interval_is_refused() {
401        assert!(EqualDivision::new(0, OCTAVE_CENTS).is_err());
402        assert!(EqualDivision::new(12, 0.0).is_err());
403        assert!(EqualDivision::new(12, -1200.0).is_err());
404        assert!(EqualDivision::of_ratio(12, 1, 1).is_err());
405        assert!(EqualDivision::of_ratio(12, -2, 1).is_err());
406        assert!(
407            EqualDivision::octave(12)
408                .expect("an octave")
409                .patent_val(9)
410                .is_err()
411        );
412    }
413}