Skip to main content

music21_rs/tuningsystem/
equal.rs

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