Skip to main content

music21/tuningsystem/
mod.rs

1/// Tuning systems whose frequencies depend on the harmonic context they
2/// sound in, rather than on a fixed table.
3pub mod adaptive;
4// Finding the same tuning under two different names. Every caller is one of
5// this crate's own tests -- nothing in the library asks the question, and a
6// caller who wants to ask it wants a scale, not a fingerprint -- so the module
7// is compiled for tests alone rather than published as API.
8#[cfg(test)]
9mod duplicates;
10/// Equal divisions of any interval, not only of the octave.
11pub mod equal;
12mod generated;
13/// Monzos and vals, the vectors regular temperament theory is written in.
14pub mod monzo;
15/// Moments of symmetry, the scales a single generator makes.
16pub mod mos;
17/// Runtime parsing of Scala `.scl` scale files.
18pub mod scala;
19#[cfg(feature = "scala-archive")]
20pub mod scala_bundled;
21/// Rank-2 regular temperaments, a period and a generator over a mapping.
22pub mod temperament;
23mod temperaments_generated;
24/// Tuning spelled pitches: a way of sizing intervals, and where it starts.
25pub mod tuning;
26
27pub use equal::{EqualDivision, TRITAVE_CENTS};
28
29pub use generated::*;
30pub use monzo::{Monzo, PRIMES, Val};
31pub use mos::{Mos, MosScale, OCTAVE_CENTS, moment_of_symmetry_sizes};
32use std::num::NonZeroU32;
33pub use temperament::Temperament;
34pub use temperaments_generated::*;
35pub use tuning::{Just, Reference, Tuning};
36
37use crate::defaults::{FloatType, FractionType, IntegerType, UnsignedIntegerType};
38use crate::error::{Error, Result};
39use crate::pitch::Pitch;
40use crate::tuningsystem::adaptive::AdaptiveTuningSystem;
41
42use std::fmt::{Display, Formatter};
43use std::str::FromStr;
44
45/// Default octave size for twelve-tone systems.
46pub const OCTAVE_SIZE: UnsignedIntegerType = 12;
47
48/// Frequency of middle C in hertz.
49pub const C4: FloatType = 261.625_565_300_598_6;
50/// Frequency of C0 in hertz.
51pub const C0: FloatType = C4 / 16.0;
52/// Frequency of C-1 in hertz.
53pub const CN1: FloatType = C4 / 32.0;
54
55/// Frequency of A4 in hertz.
56pub const A4: FloatType = 440.0;
57/// Frequency of A0 in hertz.
58pub const A0: FloatType = A4 / 16.0;
59/// Frequency of A-1 in hertz.
60pub const AN1: FloatType = A4 / 32.0;
61
62/// The common twelve-tone tuning systems useful for comparing pitch frequencies.
63pub const COMMON_TWELVE_TONE_TUNING_SYSTEMS: [TuningSystem; 4] = [
64    TuningSystem::EQUAL_TEMPERAMENT,
65    TuningSystem::CarlosHarmonic,
66    TuningSystem::PythagoreanTuning,
67    TuningSystem::FiveLimit,
68];
69
70/// The equal divisions of the octave that xenharmonic practice actually uses.
71///
72/// Any equal division is already expressible as a [`TuningSystem::Equal`];
73/// this names the ones worth reaching for. 19 and 31 support meantone, 22 deliberately does not,
74/// 53 gets 5-limit harmony almost exact, and 72 is the usual choice for
75/// notating 11-limit music.
76pub const COMMON_EQUAL_TEMPERAMENTS: [TuningSystem; 8] = [
77    TuningSystem::edo(12),
78    TuningSystem::edo(19),
79    TuningSystem::edo(22),
80    TuningSystem::edo(24),
81    TuningSystem::edo(31),
82    TuningSystem::edo(41),
83    TuningSystem::edo(53),
84    TuningSystem::edo(72),
85];
86
87/// Historical keyboard temperaments, oldest first.
88///
89/// These are the well temperaments and meantone tunings that Western keyboard
90/// music was actually written for, transcribed from the Scala archive in the
91/// `music21` reference submodule. All are twelve-tone.
92pub const HISTORICAL_TEMPERAMENTS: [TuningSystem; 14] = [
93    TuningSystem::QuarterCommaMeantone,
94    TuningSystem::WerckmeisterIII,
95    TuningSystem::Rameau,
96    TuningSystem::KirnbergerIII,
97    TuningSystem::Vallotti,
98    TuningSystem::YoungII,
99    TuningSystem::ThirdCommaMeantone,
100    TuningSystem::SixthCommaMeantone,
101    TuningSystem::WerckmeisterIV,
102    TuningSystem::WerckmeisterV,
103    TuningSystem::KirnbergerI,
104    TuningSystem::NeidhardtI,
105    TuningSystem::Silbermann,
106    TuningSystem::LehmanBach,
107];
108
109/// Either a normal tuning system or a context-sensitive adaptive tuning system.
110#[derive(Clone, Copy, Debug, PartialEq)]
111#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
112#[must_use]
113pub enum AnyTuningSystem {
114    /// A fixed system, which answers the same way whatever it sounds against.
115    Fixed(TuningSystem),
116    /// An adaptive system, whose answer depends on the context it is given.
117    Adaptive(AdaptiveTuningSystem),
118}
119
120impl AnyTuningSystem {
121    /// The frequency of a degree, in Hz.
122    ///
123    /// `context` is the frequency an adaptive system tunes against; a fixed
124    /// system ignores it.
125    pub fn frequency_at(self, context: FloatType, index: FloatType) -> FloatType {
126        match self {
127            Self::Fixed(tuning_system) => {
128                let _ = context;
129                tuning_system.frequency_at(index)
130            }
131            Self::Adaptive(adaptive_tuning_system) => {
132                adaptive_tuning_system.frequency_at(context, index)
133            }
134        }
135    }
136
137    /// The degree's distance above the tonic, in cents.
138    ///
139    /// `context` is the frequency an adaptive system tunes against; a fixed
140    /// system ignores it.
141    pub fn cents_at(self, context: FloatType, index: FloatType) -> FloatType {
142        match self {
143            Self::Fixed(tuning_system) => {
144                let _ = context;
145                tuning_system.cents_at(index)
146            }
147            Self::Adaptive(adaptive_tuning_system) => {
148                adaptive_tuning_system.cents_at(context, index)
149            }
150        }
151    }
152
153    /// Whether this system's answers depend on the context they are asked in.
154    pub fn is_adaptive(self) -> bool {
155        matches!(self, Self::Adaptive(_))
156    }
157}
158
159impl From<TuningSystem> for AnyTuningSystem {
160    fn from(tuning_system: TuningSystem) -> Self {
161        Self::Fixed(tuning_system)
162    }
163}
164
165impl From<AdaptiveTuningSystem> for AnyTuningSystem {
166    fn from(adaptive_tuning_system: AdaptiveTuningSystem) -> Self {
167        Self::Adaptive(adaptive_tuning_system)
168    }
169}
170
171/// All built-in tuning systems in canonical display order.
172pub const ALL_TUNING_SYSTEMS: [TuningSystem; 28] = [
173    TuningSystem::EQUAL_TEMPERAMENT,
174    TuningSystem::WHOLE_TONE,
175    TuningSystem::QUARTER_TONE,
176    TuningSystem::CarlosHarmonic,
177    TuningSystem::CarlosHarmonic24,
178    TuningSystem::PythagoreanTuning,
179    TuningSystem::FiveLimit,
180    TuningSystem::ElevenLimit,
181    TuningSystem::FortyThreeTone,
182    TuningSystem::Javanese,
183    TuningSystem::Thai,
184    TuningSystem::PtolemyIntenseDiatonic,
185    TuningSystem::IndianAlt,
186    TuningSystem::Indian22,
187    TuningSystem::QuarterCommaMeantone,
188    TuningSystem::WerckmeisterIII,
189    TuningSystem::Rameau,
190    TuningSystem::KirnbergerIII,
191    TuningSystem::Vallotti,
192    TuningSystem::YoungII,
193    TuningSystem::ThirdCommaMeantone,
194    TuningSystem::SixthCommaMeantone,
195    TuningSystem::WerckmeisterIV,
196    TuningSystem::WerckmeisterV,
197    TuningSystem::KirnbergerI,
198    TuningSystem::NeidhardtI,
199    TuningSystem::Silbermann,
200    TuningSystem::LehmanBach,
201];
202
203/// An exact interval as a tuning table writes one: a ratio of whole numbers,
204/// or a root of one.
205///
206/// A root is how an equal division is exact: a step of twelve-tone equal
207/// temperament is `2^(1/12)`, and one of Bohlen-Pierce `3^(1/13)`. Neither is
208/// a fraction, which is why this is not a [`FractionType`]; a rational one
209/// converts to that with [`Ratio::to_fraction`], and to a [`Monzo`] with
210/// [`Ratio::monzo`].
211#[derive(Clone, Copy, Debug, Eq, PartialEq)]
212#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
213#[must_use]
214pub enum Ratio {
215    /// `numerator / denominator`.
216    Rational {
217        /// The number on top.
218        numerator: UnsignedIntegerType,
219        /// The number below.
220        denominator: UnsignedIntegerType,
221    },
222    /// `base^(numerator / denominator)`.
223    Root {
224        /// The number a root is taken of.
225        base: UnsignedIntegerType,
226        /// The power it is raised to.
227        numerator: UnsignedIntegerType,
228        /// Which root is taken.
229        denominator: UnsignedIntegerType,
230    },
231}
232
233impl Ratio {
234    /// `numerator / denominator`.
235    pub const fn new(numerator: UnsignedIntegerType, denominator: UnsignedIntegerType) -> Self {
236        Self::Rational {
237            numerator,
238            denominator,
239        }
240    }
241
242    /// `base^(numerator / denominator)`.
243    pub const fn root(
244        base: UnsignedIntegerType,
245        numerator: UnsignedIntegerType,
246        denominator: UnsignedIntegerType,
247    ) -> Self {
248        Self::Root {
249            base,
250            numerator,
251            denominator,
252        }
253    }
254
255    /// How many times wider than a unison the interval is.
256    #[must_use]
257    pub fn value(self) -> FloatType {
258        match self {
259            Self::Rational {
260                numerator,
261                denominator,
262            } => FloatType::from(numerator) / FloatType::from(denominator),
263            Self::Root {
264                base,
265                numerator,
266                denominator,
267            } => FloatType::from(base)
268                .powf(FloatType::from(numerator) / FloatType::from(denominator)),
269        }
270    }
271
272    /// How wide the interval is, in cents.
273    #[must_use]
274    pub fn cents(self) -> FloatType {
275        OCTAVE_CENTS * self.value().log2()
276    }
277
278    /// The ratio as a [`FractionType`]: `None` for a root, and for a ratio
279    /// too wide for one.
280    #[must_use]
281    pub fn to_fraction(self) -> Option<FractionType> {
282        match self {
283            Self::Rational {
284                numerator,
285                denominator,
286            } => Some(FractionType::new(
287                IntegerType::try_from(numerator).ok()?,
288                IntegerType::try_from(denominator).ok()?,
289            )),
290            Self::Root { .. } => None,
291        }
292    }
293
294    /// The ratio's prime exponents. Errors on a root, whose exponents are not
295    /// whole, and on a ratio with a prime past the ones [`Monzo`] carries.
296    pub fn monzo(self) -> Result<Monzo> {
297        let fraction = self.to_fraction().ok_or_else(|| {
298            Error::TuningSystem(format!(
299                "{self} is not a ratio of two i32s, so has no monzo"
300            ))
301        })?;
302        Monzo::from_fraction(fraction)
303    }
304
305    /// The interval raised by `octaves` periods: octaves for a ratio, and
306    /// whole powers of its base for a root.
307    pub fn with_octaves(self, octaves: UnsignedIntegerType) -> Self {
308        match self {
309            Self::Rational {
310                numerator,
311                denominator,
312            } => {
313                let multiplier = (2 as UnsignedIntegerType)
314                    .checked_pow(octaves)
315                    .expect("octave multiplier exceeds u32 range");
316                Self::new(
317                    numerator
318                        .checked_mul(multiplier)
319                        .expect("ratio numerator exceeds u32 range"),
320                    denominator,
321                )
322            }
323            Self::Root {
324                base,
325                numerator,
326                denominator,
327            } => Self::root(
328                base,
329                numerator
330                    .checked_add(
331                        denominator
332                            .checked_mul(octaves)
333                            .expect("root octave offset exceeds u32 range"),
334                    )
335                    .expect("root numerator exceeds u32 range"),
336                denominator,
337            ),
338        }
339    }
340}
341
342impl From<Ratio> for FloatType {
343    fn from(ratio: Ratio) -> Self {
344        ratio.value()
345    }
346}
347
348impl Display for Ratio {
349    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
350        match *self {
351            Self::Rational {
352                numerator,
353                denominator: 1,
354            } => write!(f, "{numerator}"),
355            Self::Rational {
356                numerator,
357                denominator,
358            } => write!(f, "{numerator}/{denominator}"),
359            Self::Root { numerator: 0, .. } => write!(f, "1"),
360            Self::Root {
361                base,
362                numerator,
363                denominator,
364            } => write!(f, "{base}^({numerator}/{denominator})"),
365        }
366    }
367}
368
369#[derive(Clone, Copy, Debug, PartialEq)]
370#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
371/// Supported tuning systems and ratio tables.
372///
373/// Every system repeats at a period, and a degree past the end of one is the
374/// same degree a period up. The ratio tables repeat at the octave; an equal
375/// division repeats wherever its period is, so Bohlen-Pierce climbs by
376/// tritaves.
377#[must_use]
378#[non_exhaustive]
379pub enum TuningSystem {
380    /// Equal steps of one period. `12edo` is ordinary equal temperament,
381    /// `6edo` the whole-tone scale and `24edo` quarter tones, and the period
382    /// need not be an octave: `13edt` is Bohlen-Pierce.
383    Equal(EqualDivision),
384
385    /// Twelve-tone harmonic-series scale (Wendy Carlos's Harmonic).
386    ///
387    /// Not just intonation, despite its former name: the degrees are the
388    /// harmonic series 16:17:18:19:20:21:22:24:26:27:28:30, which reaches the
389    /// 19-limit. [`TuningSystem::FiveLimit`] is the classical 12-tone JI scale.
390    CarlosHarmonic,
391    /// Twenty-four-tone harmonic-series scale.
392    CarlosHarmonic24,
393    /// Twelve-tone Pythagorean tuning table.
394    PythagoreanTuning,
395
396    /// Twelve-tone five-limit table.
397    FiveLimit,
398    /// Twenty-nine-tone eleven-limit table.
399    ElevenLimit,
400
401    /// Forty-three-tone ratio table.
402    FortyThreeTone,
403
404    // Ethnic scales.
405    /// Five-tone Javanese equal-temperament approximation.
406    Javanese,
407    /// Seven-tone Thai equal-temperament approximation.
408    Thai,
409    /// Ptolemy's intense diatonic, also Zarlino's just major scale.
410    ///
411    /// Greek and Renaissance European, despite once being filed here as an
412    /// Indian scale — the archive names it `ptolemy.scl`.
413    PtolemyIntenseDiatonic,
414    /// Alternate seven-tone PtolemyIntenseDiatonic scale table.
415    IndianAlt,
416    /// Twenty-two-tone PtolemyIntenseDiatonic scale table.
417    Indian22,
418
419    // Historical keyboard temperaments, transcribed from the Scala archive.
420    /// Twelve-tone quarter-comma meantone temperament (Aaron, 1523).
421    QuarterCommaMeantone,
422    /// Twelve-tone Werckmeister III well temperament (1681).
423    WerckmeisterIII,
424    /// Twelve-tone Rameau modified meantone temperament (1725).
425    Rameau,
426    /// Twelve-tone Kirnberger III well temperament (1744).
427    KirnbergerIII,
428    /// Twelve-tone Vallotti well temperament (c. 1754).
429    Vallotti,
430    /// Twelve-tone Thomas Young well temperament no. 2 (1799).
431    YoungII,
432    /// A twelve-tone third-comma meantone temperament (Salinas, 1577).
433    ThirdCommaMeantone,
434    /// A twelve-tone sixth-comma meantone temperament (Salinas, 1577).
435    SixthCommaMeantone,
436    /// A twelve-tone Werckmeister IV well temperament (1681).
437    WerckmeisterIV,
438    /// A twelve-tone Werckmeister V well temperament (1681).
439    WerckmeisterV,
440    /// A twelve-tone Kirnberger I well temperament (1766).
441    KirnbergerI,
442    /// A twelve-tone Neidhardt I well temperament (1724).
443    NeidhardtI,
444    /// A twelve-tone Gottfried Silbermann temperament no. 1 (c. 1730).
445    Silbermann,
446    /// A twelve-tone Lehman-Bach temperament (2005).
447    LehmanBach,
448}
449
450impl TuningSystem {
451    /// Returns the canonical identifier used by [`FromStr`].
452    pub fn id(self) -> String {
453        let id = match self {
454            Self::Equal(division) => return division.to_string(),
455            Self::CarlosHarmonic => "CarlosHarmonic",
456            Self::CarlosHarmonic24 => "CarlosHarmonic24",
457            Self::PythagoreanTuning => "PythagoreanTuning",
458            Self::FiveLimit => "FiveLimit",
459            Self::ElevenLimit => "ElevenLimit",
460            Self::FortyThreeTone => "FortyThreeTone",
461            Self::Javanese => "Javanese",
462            Self::Thai => "Thai",
463            Self::PtolemyIntenseDiatonic => "PtolemyIntenseDiatonic",
464            Self::IndianAlt => "IndianAlt",
465            Self::Indian22 => "Indian22",
466            Self::QuarterCommaMeantone => "QuarterCommaMeantone",
467            Self::WerckmeisterIII => "WerckmeisterIII",
468            Self::Rameau => "Rameau",
469            Self::KirnbergerIII => "KirnbergerIII",
470            Self::Vallotti => "Vallotti",
471            Self::YoungII => "YoungII",
472            Self::ThirdCommaMeantone => "ThirdCommaMeantone",
473            Self::SixthCommaMeantone => "SixthCommaMeantone",
474            Self::WerckmeisterIV => "WerckmeisterIV",
475            Self::WerckmeisterV => "WerckmeisterV",
476            Self::KirnbergerI => "KirnbergerI",
477            Self::NeidhardtI => "NeidhardtI",
478            Self::Silbermann => "Silbermann",
479            Self::LehmanBach => "LehmanBach",
480        };
481        id.to_string()
482    }
483
484    /// Returns a compact display name for this tuning system.
485    pub fn display_name(self) -> String {
486        let name = match self {
487            Self::Equal(division) => return equal_display_name(division),
488            Self::CarlosHarmonic => "Carlos Harmonic",
489            Self::CarlosHarmonic24 => "Carlos Harmonic 24",
490            Self::PythagoreanTuning => "Pythagorean",
491            Self::FiveLimit => "Five-limit",
492            Self::ElevenLimit => "Partch 11-limit diamond",
493            Self::FortyThreeTone => "Partch 43-tone",
494            Self::Javanese => "Javanese",
495            Self::Thai => "Thai",
496            Self::PtolemyIntenseDiatonic => "Ptolemy intense diatonic",
497            Self::IndianAlt => "Indian Sa-grama",
498            Self::Indian22 => "Indian shruti",
499            Self::QuarterCommaMeantone => "Quarter-comma meantone",
500            Self::WerckmeisterIII => "Werckmeister III",
501            Self::Rameau => "Rameau",
502            Self::KirnbergerIII => "Kirnberger III",
503            Self::Vallotti => "Vallotti",
504            Self::YoungII => "Young II",
505            Self::ThirdCommaMeantone => "Third-comma meantone",
506            Self::SixthCommaMeantone => "Sixth-comma meantone",
507            Self::WerckmeisterIV => "Werckmeister IV",
508            Self::WerckmeisterV => "Werckmeister V",
509            Self::KirnbergerI => "Kirnberger I",
510            Self::NeidhardtI => "Neidhardt I",
511            Self::Silbermann => "Silbermann",
512            Self::LehmanBach => "Lehman-Bach",
513        };
514        name.to_string()
515    }
516
517    /// Returns a short description of this tuning system.
518    pub fn description(self) -> String {
519        let description = match self {
520            Self::Equal(division) => return equal_description(division),
521            Self::CarlosHarmonic => "A twelve-tone harmonic-series scale, reaching the 19-limit.",
522            Self::CarlosHarmonic24 => "A twenty-four-tone harmonic-series scale.",
523            Self::PythagoreanTuning => "A twelve-tone tuning table built from pure fifths.",
524            Self::FiveLimit => "A twelve-tone table using five-limit just ratios.",
525            Self::ElevenLimit => "Harry Partch's twenty-nine-tone 11-limit tonality diamond.",
526            Self::FortyThreeTone => "Harry Partch's forty-three-tone pure scale.",
527            Self::Javanese => "A five-tone Javanese equal-temperament approximation.",
528            Self::Thai => "A seven-tone Thai equal-temperament approximation.",
529            Self::PtolemyIntenseDiatonic => {
530                "Ptolemy's intense diatonic, also Zarlino's just major scale."
531            }
532            Self::IndianAlt => "The Indian Sa-grama mode, the inverse of Didymus' diatonic.",
533            Self::Indian22 => "The twenty-two-shruti Indian scale.",
534            Self::QuarterCommaMeantone => {
535                "A twelve-tone quarter-comma meantone temperament (Aaron, 1523)."
536            }
537            Self::WerckmeisterIII => "A twelve-tone Werckmeister III well temperament (1681).",
538            Self::Rameau => "A twelve-tone Rameau modified meantone temperament (1725).",
539            Self::KirnbergerIII => "A twelve-tone Kirnberger III well temperament (1744).",
540            Self::Vallotti => "A twelve-tone Vallotti well temperament (c. 1754).",
541            Self::YoungII => "A twelve-tone Thomas Young well temperament no. 2 (1799).",
542            Self::ThirdCommaMeantone => {
543                "A twelve-tone third-comma meantone temperament (Salinas, 1577)."
544            }
545            Self::SixthCommaMeantone => {
546                "A twelve-tone sixth-comma meantone temperament (Salinas, 1577)."
547            }
548            Self::WerckmeisterIV => "A twelve-tone Werckmeister IV well temperament (1681).",
549            Self::WerckmeisterV => "A twelve-tone Werckmeister V well temperament (1681).",
550            Self::KirnbergerI => "A twelve-tone Kirnberger I well temperament (1766).",
551            Self::NeidhardtI => "A twelve-tone Neidhardt I well temperament (1724).",
552            Self::Silbermann => "A twelve-tone Gottfried Silbermann temperament no. 1 (c. 1730).",
553            Self::LehmanBach => "A twelve-tone Lehman-Bach temperament (2005).",
554        };
555        description.to_string()
556    }
557
558    /// The frequency ratio of a degree above the system's starting pitch.
559    pub fn ratio(self, index: usize) -> FloatType {
560        self.ratio_at(index as FloatType)
561    }
562
563    /// The same for a fractional degree. A whole degree is the system's own
564    /// step exactly; between two, the pitch is interpolated as the equal
565    /// division of the period with as many steps would place it.
566    pub fn ratio_at(self, index: FloatType) -> FloatType {
567        assert!(index.is_finite(), "degree index must be finite");
568        let steps = FloatType::from(self.degrees_per_period());
569        let period = self.period_ratio();
570        let Some(table) = self.ratio_table() else {
571            return period.powf(index / steps);
572        };
573        let whole = index.floor() as IntegerType;
574        let len = IntegerType::try_from(table.len()).expect("ratio table length exceeds i32 range");
575        let fraction = index - FloatType::from(whole);
576        table[whole.rem_euclid(len) as usize].value()
577            * period.powi(whole.div_euclid(len))
578            * period.powf(fraction / steps)
579    }
580
581    /// The degree as an exact ratio, where it is one.
582    ///
583    /// Every ratio table has one for each degree, and so does an equal
584    /// division of a whole-number period, as a root of it: a step of `13edt`
585    /// is the thirteenth root of three. A division of an irrational period,
586    /// Carlos Alpha's 78 cents, has none.
587    pub fn exact_ratio(self, index: usize) -> Option<Ratio> {
588        let index = UnsignedIntegerType::try_from(index).expect("tone index exceeds u32 range");
589        match self {
590            Self::Equal(division) => match division.period_ratio() {
591                Some((base, 1)) => Some(Ratio::root(
592                    UnsignedIntegerType::try_from(base).ok()?,
593                    index,
594                    division.divisions(),
595                )),
596                _ => None,
597            },
598            _ => {
599                let table = self.ratio_table()?;
600                let len = table.len() as UnsignedIntegerType;
601                Some(table[(index % len) as usize].with_octaves(index / len))
602            }
603        }
604    }
605
606    /// A name for a degree, counted from `C-1`.
607    ///
608    /// A system of twelve degrees to the octave names each degree for its key,
609    /// `C♯/D♭4`, however it is tuned, and so does any other system on a degree
610    /// that lands on a key. Every other degree is the pitch it sounds, as
611    /// [`TuningSystem::pitch_at`] spells it, with its microtone: `G0(+2c)` is
612    /// the thirteenth step of Bohlen-Pierce. Ptolemy's intense diatonic and
613    /// the Sa-grama are named in sargam, `Re0`.
614    pub fn label(self, index: UnsignedIntegerType) -> String {
615        let steps = self.degrees_per_period();
616        if matches!(self, Self::PtolemyIntenseDiatonic | Self::IndianAlt) && steps == 7 {
617            let octave = IntegerType::try_from(index / steps).expect("an octave fits an i32") - 1;
618            return format!("{}{octave}", INDIAN_SCALE_NAMES[(index % steps) as usize]);
619        }
620        let index = IntegerType::try_from(index).expect("a degree index fits an i32");
621        if self.is_keyboard() {
622            return key_label(index);
623        }
624        let pitch = self
625            .pitch_at(index)
626            .expect("every degree of a tuning system is a pitch");
627        if pitch.is_twelve_tone() {
628            return key_label(pitch.pitch_space().round() as IntegerType);
629        }
630        let microtone = pitch.microtone().map(ToString::to_string);
631        format!(
632            "{}{}",
633            pitch.unicode_name_with_octave(),
634            microtone.unwrap_or_default()
635        )
636    }
637
638    /// Which period a degree falls in, counting from nought.
639    pub fn period_of(self, index: UnsignedIntegerType) -> UnsignedIntegerType {
640        index / self.degrees_per_period()
641    }
642
643    /// The frequency in hertz of a degree above `C-1`.
644    pub fn frequency(self, index: UnsignedIntegerType) -> FloatType {
645        self.frequency_at(FloatType::from(index))
646    }
647
648    /// The frequency in hertz of a fractional degree above `C-1`.
649    pub fn frequency_at(self, index: FloatType) -> FloatType {
650        CN1 * self.ratio_at(index)
651    }
652
653    /// The degree sounding nearest `hertz`, counted from `C-1` as
654    /// [`TuningSystem::frequency`] counts, and measured in cents. Errors on a
655    /// frequency that is not a finite number above nought.
656    ///
657    /// ```
658    /// use music21::tuningsystem::TuningSystem;
659    ///
660    /// let nineteen = TuningSystem::edo(19);
661    /// // A4 at 440 Hz is nearest degree 5 of the fifth period above C-1.
662    /// assert_eq!(nineteen.nearest_degree(440.0)?, 5 * 19 + 14);
663    /// # Ok::<(), music21::Error>(())
664    /// ```
665    pub fn nearest_degree(self, hertz: FloatType) -> Result<IntegerType> {
666        if !hertz.is_finite() || hertz <= 0.0 {
667            return Err(Error::TuningSystem(format!(
668                "a frequency must be a finite number above nought, got {hertz}"
669            )));
670        }
671        Ok(self.nearest_degree_to_cents(OCTAVE_CENTS * (hertz / CN1).log2()))
672    }
673
674    /// The degree nearest a distance above `C-1` in cents. A table may be
675    /// uneven enough that the nearest degree lies in the next period, so the
676    /// periods either side are searched too.
677    fn nearest_degree_to_cents(self, cents: FloatType) -> IntegerType {
678        let steps = IntegerType::try_from(self.degrees_per_period())
679            .expect("a period's degree count fits an i32");
680        let period = (cents / self.period_cents()).floor() as IntegerType;
681        let distance = |degree: IntegerType| {
682            (OCTAVE_CENTS * self.ratio_at(degree.into()).log2() - cents).abs()
683        };
684        ((period - 1) * steps..=(period + 2) * steps)
685            .min_by(|&a, &b| distance(a).total_cmp(&distance(b)))
686            .expect("the search range is never empty")
687    }
688
689    /// The pitch a degree sounds, counted from `C-1`: spelled as the nearest
690    /// pitch-space value is spelled, with any remainder as a microtone.
691    ///
692    /// ```
693    /// use music21::tuningsystem::TuningSystem;
694    ///
695    /// // Degree 4 of the five-limit table is a just major third above C-1.
696    /// let e = TuningSystem::FiveLimit.pitch_at(64)?;
697    /// assert_eq!(e.name_with_octave(), "E4");
698    /// assert!((e.microtone().unwrap().cents() + 13.686).abs() < 1e-3);
699    /// # Ok::<(), music21::Error>(())
700    /// ```
701    pub fn pitch_at(self, index: IntegerType) -> Result<Pitch> {
702        Pitch::from_pitch_space(OCTAVE_CENTS * self.ratio_at(index.into()).log2() / 100.0)
703    }
704
705    /// How far a degree lies from the same degree of the equal division of
706    /// the same period into as many steps, in cents. Nought all the way up
707    /// for an equal division, which is its own reference.
708    pub fn cents(self, index: UnsignedIntegerType) -> FloatType {
709        self.cents_at(FloatType::from(index))
710    }
711
712    /// The same for a fractional degree.
713    pub fn cents_at(self, index: FloatType) -> FloatType {
714        let equal = self
715            .period_ratio()
716            .powf(index / FloatType::from(self.degrees_per_period()));
717        1200.0 * (self.ratio_at(index) / equal).log2()
718    }
719
720    /// How many degrees the system has before it repeats: twelve for most of
721    /// the tables, and the number of divisions for an equal one.
722    pub fn degrees_per_period(self) -> UnsignedIntegerType {
723        match self {
724            Self::Equal(division) => division.divisions(),
725            _ => self
726                .ratio_table()
727                .map_or(OCTAVE_SIZE, |table| table.len() as UnsignedIntegerType),
728        }
729    }
730
731    /// The interval the system repeats at, in cents: an octave for every
732    /// table, and whatever an equal division divides.
733    pub fn period_cents(self) -> FloatType {
734        match self {
735            Self::Equal(division) => division.period_cents(),
736            _ => OCTAVE_CENTS,
737        }
738    }
739
740    /// Whether the system repeats at the octave.
741    pub fn repeats_at_the_octave(self) -> bool {
742        match self {
743            Self::Equal(division) => division.repeats_at_the_octave(),
744            _ => true,
745        }
746    }
747
748    /// Whether the system has twelve degrees to the octave, one for each key
749    /// of a keyboard.
750    pub fn is_keyboard(self) -> bool {
751        self.degrees_per_period() == 12 && self.repeats_at_the_octave()
752    }
753
754    fn period_ratio(self) -> FloatType {
755        (2.0 as FloatType).powf(self.period_cents() / OCTAVE_CENTS)
756    }
757
758    fn ratio_table(self) -> Option<&'static [Ratio]> {
759        match self {
760            Self::CarlosHarmonic => Some(&CARLOS_HARMONIC),
761            Self::CarlosHarmonic24 => Some(&CARLOS_HARMONIC_24),
762            Self::PythagoreanTuning => Some(&PYTHAGOREAN_TUNING),
763            Self::FiveLimit => Some(&FIVE_LIMIT),
764            Self::ElevenLimit => Some(&ELEVEN_LIMIT),
765            Self::FortyThreeTone => Some(&FORTY_THREE_TONE),
766            Self::Javanese => Some(&JAVANESE),
767            Self::Thai => Some(&THAI),
768            Self::PtolemyIntenseDiatonic => Some(&PTOLEMY_INTENSE_DIATONIC),
769            Self::IndianAlt => Some(&INDIA_SCALE_ALT),
770            Self::Indian22 => Some(&INDIAN_SCALE_22),
771            Self::QuarterCommaMeantone => Some(&QUARTER_COMMA_MEANTONE),
772            Self::WerckmeisterIII => Some(&WERCKMEISTER_III),
773            Self::Rameau => Some(&RAMEAU),
774            Self::KirnbergerIII => Some(&KIRNBERGER_III),
775            Self::Vallotti => Some(&VALLOTTI),
776            Self::YoungII => Some(&YOUNG_II),
777            Self::ThirdCommaMeantone => Some(&THIRD_COMMA_MEANTONE),
778            Self::SixthCommaMeantone => Some(&SIXTH_COMMA_MEANTONE),
779            Self::WerckmeisterIV => Some(&WERCKMEISTER_IV),
780            Self::WerckmeisterV => Some(&WERCKMEISTER_V),
781            Self::KirnbergerI => Some(&KIRNBERGER_I),
782            Self::NeidhardtI => Some(&NEIDHARDT_I),
783            Self::Silbermann => Some(&SILBERMANN),
784            Self::LehmanBach => Some(&LEHMAN_BACH),
785            Self::Equal(_) => None,
786        }
787    }
788
789    /// Twelve-tone equal temperament, `12edo`.
790    pub const EQUAL_TEMPERAMENT: Self = Self::edo(12);
791
792    /// The whole-tone scale, six equal steps to the octave.
793    pub const WHOLE_TONE: Self = Self::edo(6);
794
795    /// Quarter tones, twenty-four equal steps to the octave.
796    pub const QUARTER_TONE: Self = Self::edo(24);
797
798    /// An equal division of the octave into a number of steps fixed where it
799    /// is written, for naming one in a constant. It is a compile error in a
800    /// constant, and a panic otherwise, to ask for no steps at all; a count
801    /// read at runtime goes through [`EqualDivision::octave`], which is an
802    /// error instead.
803    pub const fn edo(divisions: UnsignedIntegerType) -> Self {
804        Self::Equal(EqualDivision::edo(nonzero(divisions)))
805    }
806}
807
808/// A step count that is not nought, for the constants above.
809const fn nonzero(divisions: UnsignedIntegerType) -> NonZeroU32 {
810    match NonZeroU32::new(divisions) {
811        Some(divisions) => divisions,
812        None => panic!("a period cannot be divided into no steps"),
813    }
814}
815
816fn equal_display_name(division: EqualDivision) -> String {
817    match (division.period_ratio(), division.divisions()) {
818        (Some((2, 1)), 12) => "Equal temperament".to_string(),
819        (Some((2, 1)), 6) => "Whole tone".to_string(),
820        (Some((2, 1)), 24) => "Quarter tone".to_string(),
821        (Some((2, 1)), divisions) => format!("{divisions}-tone equal temperament"),
822        _ => format!("{division}"),
823    }
824}
825
826fn equal_description(division: EqualDivision) -> String {
827    let divisions = division.divisions();
828    match division.period_ratio() {
829        Some((2, 1)) => match divisions {
830            12 => "Twelve equal divisions of the octave.".to_string(),
831            6 => "Six equal whole-tone steps per octave.".to_string(),
832            24 => "Twenty-four equal quarter-tone steps per octave.".to_string(),
833            _ => format!("{divisions} equal divisions of the octave."),
834        },
835        Some((numerator, denominator)) => format!(
836            "{divisions} equal divisions of {numerator}/{denominator}, repeating there rather than at the octave."
837        ),
838        None => format!(
839            "{divisions} equal divisions of {:.3} cents, repeating there rather than at the octave.",
840            division.period_cents()
841        ),
842    }
843}
844
845impl Display for TuningSystem {
846    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
847        f.write_str(&self.id())
848    }
849}
850
851impl FromStr for TuningSystem {
852    type Err = Error;
853
854    /// Reads a system's id: a table's name, or an equal division as it
855    /// writes itself (`19edo`, `13edt`, `9ed3/2`). `EqualTemperament`,
856    /// `WholeTone` and `QuarterTone` read as `12edo`, `6edo` and `24edo`.
857    fn from_str(s: &str) -> Result<Self, Self::Err> {
858        match s {
859            "EqualTemperament" => Ok(Self::EQUAL_TEMPERAMENT),
860            "WholeTone" => Ok(Self::WHOLE_TONE),
861            "QuarterTone" => Ok(Self::QUARTER_TONE),
862            "CarlosHarmonic" => Ok(Self::CarlosHarmonic),
863            "CarlosHarmonic24" => Ok(Self::CarlosHarmonic24),
864            "PythagoreanTuning" => Ok(Self::PythagoreanTuning),
865            "FiveLimit" => Ok(Self::FiveLimit),
866            "ElevenLimit" => Ok(Self::ElevenLimit),
867            "FortyThreeTone" => Ok(Self::FortyThreeTone),
868            "Javanese" => Ok(Self::Javanese),
869            "Thai" => Ok(Self::Thai),
870            "PtolemyIntenseDiatonic" => Ok(Self::PtolemyIntenseDiatonic),
871            "IndianAlt" => Ok(Self::IndianAlt),
872            "Indian22" => Ok(Self::Indian22),
873            "QuarterCommaMeantone" => Ok(Self::QuarterCommaMeantone),
874            "WerckmeisterIII" => Ok(Self::WerckmeisterIII),
875            "Rameau" => Ok(Self::Rameau),
876            "KirnbergerIII" => Ok(Self::KirnbergerIII),
877            "Vallotti" => Ok(Self::Vallotti),
878            "YoungII" => Ok(Self::YoungII),
879            "ThirdCommaMeantone" => Ok(Self::ThirdCommaMeantone),
880            "SixthCommaMeantone" => Ok(Self::SixthCommaMeantone),
881            "WerckmeisterIV" => Ok(Self::WerckmeisterIV),
882            "WerckmeisterV" => Ok(Self::WerckmeisterV),
883            "KirnbergerI" => Ok(Self::KirnbergerI),
884            "NeidhardtI" => Ok(Self::NeidhardtI),
885            "Silbermann" => Ok(Self::Silbermann),
886            "LehmanBach" => Ok(Self::LehmanBach),
887            _ if s.contains("ed") => s.parse().map(Self::Equal),
888            _ => Err(Error::TuningSystem(format!("unknown tuning system {s:?}"))),
889        }
890    }
891}
892
893/// A step of an equal division of the octave: `tone` steps of `octave_size`.
894pub const fn equal_temperament(
895    tone: UnsignedIntegerType,
896    octave_size: UnsignedIntegerType,
897) -> Ratio {
898    Ratio::root(2, tone, octave_size)
899}
900
901/// A key of a keyboard by its pitch-space number: a white key by its
902/// letter, a black one by both of its names, `C♯/D♭4`.
903fn key_label(pitch_space: IntegerType) -> String {
904    let pitch =
905        Pitch::from_pitch_space(pitch_space.into()).expect("a whole pitch-space value is a pitch");
906    let octave = pitch_space.div_euclid(12) - 1;
907    let alter = pitch.alter();
908    if alter == 0.0 {
909        return format!("{}{octave}", pitch.unicode_name());
910    }
911    let (sharp, flat) = if alter < 0.0 {
912        (pitch.get_lower_enharmonic(), Ok(pitch))
913    } else {
914        (Ok(pitch.clone()), pitch.get_higher_enharmonic())
915    };
916    let (sharp, flat) = (
917        sharp.expect("a black key has a sharp name"),
918        flat.expect("a black key has a flat name"),
919    );
920    format!("{}/{}{octave}", sharp.unicode_name(), flat.unicode_name())
921}
922
923/// Five-tone Javanese equal-temperament approximation.
924pub const JAVANESE: [Ratio; 5] = [
925    Ratio::root(2, 0, 5),
926    Ratio::root(2, 1, 5),
927    Ratio::root(2, 2, 5),
928    Ratio::root(2, 3, 5),
929    Ratio::root(2, 4, 5),
930];
931
932/// Seven-tone Thai equal-temperament approximation.
933pub const THAI: [Ratio; 7] = [
934    Ratio::root(2, 0, 7),
935    Ratio::root(2, 1, 7),
936    Ratio::root(2, 2, 7),
937    Ratio::root(2, 3, 7),
938    Ratio::root(2, 4, 7),
939    Ratio::root(2, 5, 7),
940    Ratio::root(2, 6, 7),
941];
942
943/// Degree labels for the seven-tone PtolemyIntenseDiatonic scale.
944pub const INDIAN_SCALE_NAMES: [&str; 7] = ["Sa", "Re", "Ga", "Ma", "Pa", "Dha", "Ni"];
945
946#[cfg(test)]
947mod tests {
948    use super::*;
949
950    #[test]
951    fn every_system_answers_cents_and_an_exact_ratio_for_its_degrees() {
952        for system in ALL_TUNING_SYSTEMS {
953            let unison = system.exact_ratio(0).expect("every listed system is exact");
954            assert_eq!(unison.value(), 1.0);
955            assert_eq!(system.cents(0), 0.0);
956            assert!(system.cents(system.degrees_per_period()).abs() < 1e-9);
957        }
958    }
959
960    /// Cents above the tonic for a degree of a twelve-tone table.
961    fn cents_at_degree(system: TuningSystem, degree: usize) -> FloatType {
962        1200.0 * system.ratio(degree).log2()
963    }
964
965    #[test]
966    fn meantone_fifths_match_their_comma_fractions() {
967        // A 1/n-comma meantone narrows the pure fifth by 1/n of the syntonic
968        // comma (21.506 cents). Checking the arithmetic rather than a quoted
969        // table catches a table pointed at the wrong Scala file.
970        const PURE_FIFTH: FloatType = 701.955;
971        const SYNTONIC_COMMA: FloatType = 21.506;
972        for (system, divisor) in [
973            (TuningSystem::ThirdCommaMeantone, 3.0),
974            (TuningSystem::QuarterCommaMeantone, 4.0),
975            (TuningSystem::SixthCommaMeantone, 6.0),
976        ] {
977            let expected = PURE_FIFTH - SYNTONIC_COMMA / divisor;
978            let actual = cents_at_degree(system, 7);
979            assert!(
980                (actual - expected).abs() < 0.01,
981                "{} fifth: expected {expected:.3}, got {actual:.3}",
982                system.display_name()
983            );
984        }
985    }
986
987    #[test]
988    fn kirnberger_i_keeps_its_pure_fifth_and_third() {
989        // Kirnberger I is the outlier of the well temperaments: it leaves the
990        // C-G fifth pure rather than tempering it.
991        assert!((cents_at_degree(TuningSystem::KirnbergerI, 7) - 701.955).abs() < 0.01);
992        assert!((cents_at_degree(TuningSystem::KirnbergerI, 4) - 386.314).abs() < 0.01);
993    }
994
995    #[test]
996    fn common_equal_temperaments_divide_the_octave_evenly() {
997        for system in COMMON_EQUAL_TEMPERAMENTS {
998            let size = system.degrees_per_period();
999            let step = 1200.0 / FloatType::from(size);
1000            for degree in 0..size as usize {
1001                let expected = step * degree as FloatType;
1002                let actual = cents_at_degree(system, degree);
1003                assert!(
1004                    (actual - expected).abs() < 0.001,
1005                    "{size}-EDO degree {degree}: expected {expected:.3}, got {actual:.3}"
1006                );
1007            }
1008        }
1009    }
1010
1011    #[test]
1012    fn historical_temperaments_match_their_published_values() {
1013        // Major third and perfect fifth above C, in cents, as given in the
1014        // standard literature. Just values for reference: M3 386.314,
1015        // P5 701.955; equal temperament: 400.000 and 700.000.
1016        let cases = [
1017            (TuningSystem::QuarterCommaMeantone, 386.314, 696.578),
1018            (TuningSystem::WerckmeisterIII, 390.225, 696.090),
1019            (TuningSystem::Rameau, 386.314, 696.578),
1020            (TuningSystem::KirnbergerIII, 386.314, 696.578),
1021            (TuningSystem::Vallotti, 392.180, 698.045),
1022            (TuningSystem::YoungII, 392.180, 698.045),
1023        ];
1024
1025        for (system, major_third, fifth) in cases {
1026            let actual_third = cents_at_degree(system, 4);
1027            let actual_fifth = cents_at_degree(system, 7);
1028            assert!(
1029                (actual_third - major_third).abs() < 0.001,
1030                "{} major third: expected {major_third}, got {actual_third}",
1031                system.display_name()
1032            );
1033            assert!(
1034                (actual_fifth - fifth).abs() < 0.001,
1035                "{} fifth: expected {fifth}, got {actual_fifth}",
1036                system.display_name()
1037            );
1038        }
1039    }
1040
1041    #[test]
1042    fn quarter_comma_meantone_and_kirnberger_have_a_pure_major_third() {
1043        // The defining property of both: C-E is the just 5/4 (386.314 cents).
1044        //
1045        // Not exactly 5/4, though. The Scala archive writes these degrees in
1046        // cents to five decimal places rather than as an exact ratio, so the
1047        // table carries 386.31371 cents. That is 4e-6 cents shy of just - some
1048        // nine orders of magnitude below anything audible - but it is not the
1049        // rational 5/4, and a test asserting exact equality would be asserting
1050        // something the source data does not contain.
1051        for system in [
1052            TuningSystem::QuarterCommaMeantone,
1053            TuningSystem::KirnbergerIII,
1054        ] {
1055            let cents = cents_at_degree(system, 4);
1056            assert!(
1057                (cents - 386.313_714).abs() < 0.001,
1058                "{} major third should be the just 386.314 cents, got {cents}",
1059                system.display_name()
1060            );
1061        }
1062    }
1063
1064    #[test]
1065    fn every_historical_temperament_is_wired_up() {
1066        for system in HISTORICAL_TEMPERAMENTS {
1067            assert_eq!(system.degrees_per_period(), OCTAVE_SIZE, "{system:?}");
1068            assert!(system.ratio_table().is_some(), "{system:?} has no table");
1069            assert_eq!(system.ratio_table().unwrap().len(), 12, "{system:?}");
1070            assert!(!system.description().is_empty(), "{system:?}");
1071            assert_eq!(
1072                TuningSystem::from_str(&system.id()).unwrap(),
1073                system,
1074                "{system:?} does not round-trip through its id"
1075            );
1076            assert!(
1077                ALL_TUNING_SYSTEMS.contains(&system),
1078                "{system:?} missing from ALL_TUNING_SYSTEMS"
1079            );
1080        }
1081    }
1082
1083    #[test]
1084    fn equal_temperament_degree_helpers_work_without_tone_objects() {
1085        assert_eq!(TuningSystem::edo(12).label(0), "C-1");
1086        assert_eq!(TuningSystem::edo(12).period_of(0), 0);
1087        assert!((TuningSystem::edo(12).frequency(0) - CN1).abs() < 1e-12);
1088
1089        assert_eq!(TuningSystem::edo(12).label(69), "A4");
1090        assert_eq!(TuningSystem::edo(12).period_of(69), 5);
1091        assert!((TuningSystem::edo(12).frequency(69) - 440.0).abs() < 0.0001);
1092    }
1093
1094    #[test]
1095    fn fractional_frequency_helpers_support_pitch_space_values() {
1096        let equal = TuningSystem::EQUAL_TEMPERAMENT;
1097        assert!((equal.frequency_at(69.0) - A4).abs() < 0.0001);
1098        assert!((equal.frequency_at(60.0) - C4).abs() < 0.0001);
1099        assert!((TuningSystem::FiveLimit.frequency_at(64.0) - (C4 * 5.0 / 4.0)).abs() < 0.0001);
1100        assert!(
1101            (TuningSystem::PythagoreanTuning.frequency_at(67.0) - (C4 * 3.0 / 2.0)).abs() < 0.0001
1102        );
1103        assert!(TuningSystem::FiveLimit.cents_at(64.0) < -13.0);
1104    }
1105
1106    #[test]
1107    fn ratio_helpers_cover_octaves() {
1108        let two_one: FloatType = Ratio::new(2, 1).into();
1109        assert_eq!(TuningSystem::CarlosHarmonic.ratio(12), two_one);
1110        assert_eq!(TuningSystem::CarlosHarmonic24.ratio(24), two_one);
1111        assert_eq!(TuningSystem::EQUAL_TEMPERAMENT.ratio(12), two_one);
1112    }
1113
1114    #[test]
1115    fn a_ratio_is_rational_or_a_root() {
1116        let rational = Ratio::new(3, 2);
1117        assert_eq!(rational.value(), 1.5);
1118        assert_eq!(rational.to_string(), "3/2");
1119        assert_eq!(rational.with_octaves(2), Ratio::new(12, 2));
1120        assert_eq!(rational.to_fraction(), Some(FractionType::new(3, 2)));
1121        assert_eq!(rational.monzo().unwrap().to_string(), "[-1 1⟩");
1122        assert!((rational.cents() - 701.955).abs() < 1e-3);
1123
1124        let root = Ratio::root(2, 7, 12);
1125        assert_eq!(root.to_string(), "2^(7/12)");
1126        assert_eq!(root.with_octaves(1), Ratio::root(2, 19, 12));
1127        assert!((root.value() - 2.0_f64.powf(7.0 / 12.0)).abs() < 1e-12);
1128        assert_eq!(root.to_fraction(), None);
1129        assert!(root.monzo().is_err());
1130    }
1131
1132    #[test]
1133    fn an_equal_division_carries_its_own_step_count() {
1134        assert_eq!(equal_temperament(12, 12), Ratio::root(2, 12, 12));
1135        let quarter = TuningSystem::QUARTER_TONE;
1136        assert_eq!(quarter.exact_ratio(6), Some(Ratio::root(2, 6, 24)));
1137        assert_eq!(quarter.label(24), "C0");
1138        assert!((quarter.frequency(12) - CN1 * 2.0_f64.sqrt()).abs() < 1e-10);
1139        assert_eq!(quarter.cents(12), 0.0);
1140    }
1141
1142    /// Bohlen-Pierce: thirteen equal steps of a tritave, which repeats there
1143    /// and not at the octave, and is still exact, as roots of three.
1144    #[test]
1145    fn an_equal_division_of_the_tritave_climbs_by_tritaves() {
1146        let bohlen_pierce = TuningSystem::Equal(EqualDivision::tritave(13).unwrap());
1147        assert_eq!(bohlen_pierce.degrees_per_period(), 13);
1148        assert!(!bohlen_pierce.repeats_at_the_octave());
1149        assert!((bohlen_pierce.ratio(13) - 3.0).abs() < 1e-12);
1150        assert!((bohlen_pierce.ratio(26) - 9.0).abs() < 1e-12);
1151        assert_eq!(bohlen_pierce.period_of(26), 2);
1152        assert_eq!(bohlen_pierce.exact_ratio(13), Some(Ratio::root(3, 13, 13)));
1153        assert!(
1154            (bohlen_pierce.exact_ratio(4).unwrap().value() - bohlen_pierce.ratio(4)).abs() < 1e-12
1155        );
1156        assert_eq!(bohlen_pierce.cents(5), 0.0);
1157        assert_eq!(bohlen_pierce.label(13), "G0(+2c)");
1158        assert_eq!(bohlen_pierce.id(), "13edt");
1159        assert_eq!("13edt".parse::<TuningSystem>().unwrap(), bohlen_pierce);
1160    }
1161
1162    /// A period with no whole-number ratio has no exact degrees, and still
1163    /// sounds and names itself.
1164    #[test]
1165    fn an_equal_division_of_an_irrational_period_has_no_fractions() {
1166        let alpha = TuningSystem::Equal(EqualDivision::new(18, 1404.0).unwrap());
1167        assert_eq!(alpha.exact_ratio(1), None);
1168        assert!((1200.0 * alpha.ratio(1).log2() - 78.0).abs() < 1e-9);
1169        assert!((1200.0 * alpha.ratio(18).log2() - 1404.0).abs() < 1e-9);
1170        assert_eq!(alpha.id().parse::<TuningSystem>().unwrap(), alpha);
1171    }
1172
1173    /// The names the equal systems had as variants still read.
1174    #[test]
1175    fn the_old_equal_names_still_parse() {
1176        assert_eq!(
1177            "EqualTemperament".parse::<TuningSystem>().unwrap(),
1178            TuningSystem::EQUAL_TEMPERAMENT
1179        );
1180        assert_eq!(
1181            "WholeTone".parse::<TuningSystem>().unwrap(),
1182            TuningSystem::WHOLE_TONE
1183        );
1184        assert_eq!(
1185            "QuarterTone".parse::<TuningSystem>().unwrap(),
1186            TuningSystem::QUARTER_TONE
1187        );
1188        assert_eq!(
1189            "19edo".parse::<TuningSystem>().unwrap(),
1190            TuningSystem::edo(19)
1191        );
1192        assert_eq!(TuningSystem::EQUAL_TEMPERAMENT.id(), "12edo");
1193        assert_eq!(
1194            TuningSystem::EQUAL_TEMPERAMENT.display_name(),
1195            "Equal temperament"
1196        );
1197        assert_eq!(
1198            TuningSystem::edo(19).display_name(),
1199            "19-tone equal temperament"
1200        );
1201    }
1202
1203    #[test]
1204    fn current_tuning_system_variants_return_ratios() {
1205        assert_eq!(TuningSystem::WHOLE_TONE.ratio(6), 2.0);
1206        assert_eq!(TuningSystem::QUARTER_TONE.ratio(24), 2.0);
1207        assert_eq!(TuningSystem::PythagoreanTuning.ratio(7), 1.5);
1208        assert_eq!(TuningSystem::Indian22.ratio(22), 2.0);
1209    }
1210
1211    #[test]
1212    fn a_keyboard_system_names_its_degrees_for_their_keys() {
1213        let five_limit = TuningSystem::FiveLimit;
1214        assert_eq!(five_limit.label(60), "C4");
1215        assert_eq!(five_limit.label(61), "C♯/D♭4");
1216        assert_eq!(five_limit.label(63), "D♯/E♭4");
1217        assert_eq!(five_limit.label(70), "A♯/B♭4");
1218        assert_eq!(TuningSystem::WHOLE_TONE.label(34), "G♯/A♭4");
1219        // Nineteen steps do not land on keys, so each is the pitch it sounds.
1220        assert_eq!(TuningSystem::edo(19).label(95), "C4");
1221        assert_eq!(TuningSystem::edo(19).label(96), "C𝄲4(+13c)");
1222    }
1223
1224    #[test]
1225    fn the_nearest_degree_to_a_degree_is_that_degree() {
1226        for system in ALL_TUNING_SYSTEMS
1227            .into_iter()
1228            .chain([TuningSystem::Equal(EqualDivision::tritave(13).unwrap())])
1229        {
1230            let steps = system.degrees_per_period() as IntegerType;
1231            for degree in (2 * steps)..(6 * steps) {
1232                let hertz = system.frequency_at(degree.into());
1233                assert_eq!(system.nearest_degree(hertz).unwrap(), degree, "{system:?}");
1234            }
1235        }
1236        assert!(TuningSystem::FiveLimit.nearest_degree(0.0).is_err());
1237        assert!(
1238            TuningSystem::FiveLimit
1239                .nearest_degree(FloatType::INFINITY)
1240                .is_err()
1241        );
1242    }
1243
1244    #[test]
1245    fn a_pitch_sounds_the_nearest_degree_of_a_system_without_its_keys() {
1246        let nineteen = TuningSystem::edo(19);
1247        let a4 = Pitch::from_name("A4").unwrap();
1248        // 440 Hz lies between two steps of nineteen; the nearer is 436.0 Hz.
1249        let sounded = a4.frequency_hz_in(nineteen);
1250        assert!((sounded - nineteen.frequency(109)).abs() < 1e-9);
1251        assert!((sounded - 440.0).abs() < 1200.0 / 19.0);
1252        // A keyboard system still sounds each key's own degree.
1253        let e4 = Pitch::from_name("E4").unwrap();
1254        assert!((e4.frequency_hz_in(TuningSystem::FiveLimit) - 327.032).abs() < 0.001);
1255
1256        // Spelled back to within the six significant digits a pitch-space
1257        // value is kept to.
1258        let back = Pitch::from_frequency_in(sounded, nineteen).unwrap();
1259        assert!((1200.0 * (back.frequency_hz() / sounded).log2()).abs() < 0.01);
1260    }
1261
1262    #[test]
1263    fn table_ratios_shift_by_real_octaves() {
1264        assert_eq!(TuningSystem::CarlosHarmonic.ratio(19), 3.0);
1265        assert_eq!(TuningSystem::FortyThreeTone.ratio(68), 3.0);
1266        assert_eq!(TuningSystem::PtolemyIntenseDiatonic.ratio(8), 2.25);
1267    }
1268
1269    #[test]
1270    fn non_twelve_tone_systems_keep_system_octaves_and_labels() {
1271        assert_eq!(TuningSystem::WHOLE_TONE.label(1), "D-1");
1272        assert_eq!(TuningSystem::WHOLE_TONE.degrees_per_period(), 6);
1273        assert!(
1274            (TuningSystem::WHOLE_TONE.ratio(1) - (2.0 as FloatType).powf(1.0 / 6.0)).abs() < 1e-12
1275        );
1276
1277        assert_eq!(TuningSystem::QUARTER_TONE.label(13), "F♯𝄲-1");
1278        assert_eq!(TuningSystem::QUARTER_TONE.degrees_per_period(), 24);
1279        assert!(
1280            (TuningSystem::QUARTER_TONE.ratio(13) - (2.0 as FloatType).powf(13.0 / 24.0)).abs()
1281                < 1e-12
1282        );
1283
1284        assert_eq!(TuningSystem::Thai.label(7), "C0");
1285        assert_eq!(TuningSystem::Thai.degrees_per_period(), 7);
1286        assert_eq!(TuningSystem::Thai.ratio(7), 2.0);
1287
1288        assert_eq!(TuningSystem::PtolemyIntenseDiatonic.label(8), "Re0");
1289        assert_eq!(TuningSystem::PtolemyIntenseDiatonic.degrees_per_period(), 7);
1290        assert_eq!(TuningSystem::PtolemyIntenseDiatonic.ratio(8), 2.25);
1291
1292        assert_eq!(TuningSystem::FortyThreeTone.label(68), "G0(+2c)");
1293        assert_eq!(TuningSystem::FortyThreeTone.degrees_per_period(), 43);
1294        assert_eq!(TuningSystem::FortyThreeTone.ratio(68), 3.0);
1295    }
1296
1297    #[test]
1298    fn tuning_system_display_and_parse_are_canonical() {
1299        let system = TuningSystem::FiveLimit;
1300        assert_eq!(system.id(), "FiveLimit");
1301        assert_eq!(system.to_string(), "FiveLimit");
1302        assert_eq!("FiveLimit".parse::<TuningSystem>().unwrap(), system);
1303
1304        let err = "not-a-system".parse::<TuningSystem>().unwrap_err();
1305        assert_eq!(
1306            err,
1307            Error::TuningSystem("unknown tuning system \"not-a-system\"".to_string())
1308        );
1309    }
1310
1311    #[test]
1312    fn tuning_system_display_names_cover_variants() {
1313        for system in ALL_TUNING_SYSTEMS {
1314            assert!(!system.id().is_empty());
1315            assert!(!system.display_name().is_empty());
1316            assert!(!system.description().is_empty());
1317            assert!(system.degrees_per_period() > 0);
1318            assert_eq!(system.to_string(), system.id());
1319        }
1320    }
1321
1322    #[test]
1323    fn twelve_tone_systems_keep_chromatic_ratios_ascending() {
1324        for system in ALL_TUNING_SYSTEMS
1325            .into_iter()
1326            .filter(|system| system.degrees_per_period() == OCTAVE_SIZE)
1327        {
1328            let mut previous = system.ratio(0);
1329            for degree in 1..=OCTAVE_SIZE {
1330                let ratio = system.ratio(degree as usize);
1331                assert!(
1332                    ratio > previous,
1333                    "{} degree {degree} ratio {ratio} should be higher than {previous}",
1334                    system.id()
1335                );
1336                previous = ratio;
1337            }
1338        }
1339    }
1340
1341    #[test]
1342    fn all_ratio_tables_keep_degrees_strictly_ascending_within_the_octave() {
1343        for system in ALL_TUNING_SYSTEMS {
1344            let octave_size = system.degrees_per_period();
1345            let mut previous = system.ratio(0);
1346            for degree in 1..=octave_size {
1347                let ratio = system.ratio(degree as usize);
1348                assert!(
1349                    ratio > previous,
1350                    "{} degree {degree} ratio {ratio} should be higher than {previous} \
1351                     (a table entry is likely mistranscribed)",
1352                    system.id()
1353                );
1354                previous = ratio;
1355            }
1356        }
1357    }
1358}