Skip to main content

music21_rs/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]
378pub enum TuningSystem {
379    /// Equal steps of one period. `12edo` is ordinary equal temperament,
380    /// `6edo` the whole-tone scale and `24edo` quarter tones, and the period
381    /// need not be an octave: `13edt` is Bohlen-Pierce.
382    Equal(EqualDivision),
383
384    /// Twelve-tone harmonic-series scale (Wendy Carlos's Harmonic).
385    ///
386    /// Not just intonation, despite its former name: the degrees are the
387    /// harmonic series 16:17:18:19:20:21:22:24:26:27:28:30, which reaches the
388    /// 19-limit. [`TuningSystem::FiveLimit`] is the classical 12-tone JI scale.
389    CarlosHarmonic,
390    /// Twenty-four-tone harmonic-series scale.
391    CarlosHarmonic24,
392    /// Twelve-tone Pythagorean tuning table.
393    PythagoreanTuning,
394
395    /// Twelve-tone five-limit table.
396    FiveLimit,
397    /// Twenty-nine-tone eleven-limit table.
398    ElevenLimit,
399
400    /// Forty-three-tone ratio table.
401    FortyThreeTone,
402
403    // Ethnic scales.
404    /// Five-tone Javanese equal-temperament approximation.
405    Javanese,
406    /// Seven-tone Thai equal-temperament approximation.
407    Thai,
408    /// Ptolemy's intense diatonic, also Zarlino's just major scale.
409    ///
410    /// Greek and Renaissance European, despite once being filed here as an
411    /// Indian scale — the archive names it `ptolemy.scl`.
412    PtolemyIntenseDiatonic,
413    /// Alternate seven-tone PtolemyIntenseDiatonic scale table.
414    IndianAlt,
415    /// Twenty-two-tone PtolemyIntenseDiatonic scale table.
416    Indian22,
417
418    // Historical keyboard temperaments, transcribed from the Scala archive.
419    /// Twelve-tone quarter-comma meantone temperament (Aaron, 1523).
420    QuarterCommaMeantone,
421    /// Twelve-tone Werckmeister III well temperament (1681).
422    WerckmeisterIII,
423    /// Twelve-tone Rameau modified meantone temperament (1725).
424    Rameau,
425    /// Twelve-tone Kirnberger III well temperament (1744).
426    KirnbergerIII,
427    /// Twelve-tone Vallotti well temperament (c. 1754).
428    Vallotti,
429    /// Twelve-tone Thomas Young well temperament no. 2 (1799).
430    YoungII,
431    /// A twelve-tone third-comma meantone temperament (Salinas, 1577).
432    ThirdCommaMeantone,
433    /// A twelve-tone sixth-comma meantone temperament (Salinas, 1577).
434    SixthCommaMeantone,
435    /// A twelve-tone Werckmeister IV well temperament (1681).
436    WerckmeisterIV,
437    /// A twelve-tone Werckmeister V well temperament (1681).
438    WerckmeisterV,
439    /// A twelve-tone Kirnberger I well temperament (1766).
440    KirnbergerI,
441    /// A twelve-tone Neidhardt I well temperament (1724).
442    NeidhardtI,
443    /// A twelve-tone Gottfried Silbermann temperament no. 1 (c. 1730).
444    Silbermann,
445    /// A twelve-tone Lehman-Bach temperament (2005).
446    LehmanBach,
447}
448
449impl TuningSystem {
450    /// Returns the canonical identifier used by [`FromStr`].
451    pub fn id(self) -> String {
452        let id = match self {
453            Self::Equal(division) => return division.to_string(),
454            Self::CarlosHarmonic => "CarlosHarmonic",
455            Self::CarlosHarmonic24 => "CarlosHarmonic24",
456            Self::PythagoreanTuning => "PythagoreanTuning",
457            Self::FiveLimit => "FiveLimit",
458            Self::ElevenLimit => "ElevenLimit",
459            Self::FortyThreeTone => "FortyThreeTone",
460            Self::Javanese => "Javanese",
461            Self::Thai => "Thai",
462            Self::PtolemyIntenseDiatonic => "PtolemyIntenseDiatonic",
463            Self::IndianAlt => "IndianAlt",
464            Self::Indian22 => "Indian22",
465            Self::QuarterCommaMeantone => "QuarterCommaMeantone",
466            Self::WerckmeisterIII => "WerckmeisterIII",
467            Self::Rameau => "Rameau",
468            Self::KirnbergerIII => "KirnbergerIII",
469            Self::Vallotti => "Vallotti",
470            Self::YoungII => "YoungII",
471            Self::ThirdCommaMeantone => "ThirdCommaMeantone",
472            Self::SixthCommaMeantone => "SixthCommaMeantone",
473            Self::WerckmeisterIV => "WerckmeisterIV",
474            Self::WerckmeisterV => "WerckmeisterV",
475            Self::KirnbergerI => "KirnbergerI",
476            Self::NeidhardtI => "NeidhardtI",
477            Self::Silbermann => "Silbermann",
478            Self::LehmanBach => "LehmanBach",
479        };
480        id.to_string()
481    }
482
483    /// Returns a compact display name for this tuning system.
484    pub fn display_name(self) -> String {
485        let name = match self {
486            Self::Equal(division) => return equal_display_name(division),
487            Self::CarlosHarmonic => "Carlos Harmonic",
488            Self::CarlosHarmonic24 => "Carlos Harmonic 24",
489            Self::PythagoreanTuning => "Pythagorean",
490            Self::FiveLimit => "Five-limit",
491            Self::ElevenLimit => "Partch 11-limit diamond",
492            Self::FortyThreeTone => "Partch 43-tone",
493            Self::Javanese => "Javanese",
494            Self::Thai => "Thai",
495            Self::PtolemyIntenseDiatonic => "Ptolemy intense diatonic",
496            Self::IndianAlt => "Indian Sa-grama",
497            Self::Indian22 => "Indian shruti",
498            Self::QuarterCommaMeantone => "Quarter-comma meantone",
499            Self::WerckmeisterIII => "Werckmeister III",
500            Self::Rameau => "Rameau",
501            Self::KirnbergerIII => "Kirnberger III",
502            Self::Vallotti => "Vallotti",
503            Self::YoungII => "Young II",
504            Self::ThirdCommaMeantone => "Third-comma meantone",
505            Self::SixthCommaMeantone => "Sixth-comma meantone",
506            Self::WerckmeisterIV => "Werckmeister IV",
507            Self::WerckmeisterV => "Werckmeister V",
508            Self::KirnbergerI => "Kirnberger I",
509            Self::NeidhardtI => "Neidhardt I",
510            Self::Silbermann => "Silbermann",
511            Self::LehmanBach => "Lehman-Bach",
512        };
513        name.to_string()
514    }
515
516    /// Returns a short description of this tuning system.
517    pub fn description(self) -> String {
518        let description = match self {
519            Self::Equal(division) => return equal_description(division),
520            Self::CarlosHarmonic => "A twelve-tone harmonic-series scale, reaching the 19-limit.",
521            Self::CarlosHarmonic24 => "A twenty-four-tone harmonic-series scale.",
522            Self::PythagoreanTuning => "A twelve-tone tuning table built from pure fifths.",
523            Self::FiveLimit => "A twelve-tone table using five-limit just ratios.",
524            Self::ElevenLimit => "Harry Partch's twenty-nine-tone 11-limit tonality diamond.",
525            Self::FortyThreeTone => "Harry Partch's forty-three-tone pure scale.",
526            Self::Javanese => "A five-tone Javanese equal-temperament approximation.",
527            Self::Thai => "A seven-tone Thai equal-temperament approximation.",
528            Self::PtolemyIntenseDiatonic => {
529                "Ptolemy's intense diatonic, also Zarlino's just major scale."
530            }
531            Self::IndianAlt => "The Indian Sa-grama mode, the inverse of Didymus' diatonic.",
532            Self::Indian22 => "The twenty-two-shruti Indian scale.",
533            Self::QuarterCommaMeantone => {
534                "A twelve-tone quarter-comma meantone temperament (Aaron, 1523)."
535            }
536            Self::WerckmeisterIII => "A twelve-tone Werckmeister III well temperament (1681).",
537            Self::Rameau => "A twelve-tone Rameau modified meantone temperament (1725).",
538            Self::KirnbergerIII => "A twelve-tone Kirnberger III well temperament (1744).",
539            Self::Vallotti => "A twelve-tone Vallotti well temperament (c. 1754).",
540            Self::YoungII => "A twelve-tone Thomas Young well temperament no. 2 (1799).",
541            Self::ThirdCommaMeantone => {
542                "A twelve-tone third-comma meantone temperament (Salinas, 1577)."
543            }
544            Self::SixthCommaMeantone => {
545                "A twelve-tone sixth-comma meantone temperament (Salinas, 1577)."
546            }
547            Self::WerckmeisterIV => "A twelve-tone Werckmeister IV well temperament (1681).",
548            Self::WerckmeisterV => "A twelve-tone Werckmeister V well temperament (1681).",
549            Self::KirnbergerI => "A twelve-tone Kirnberger I well temperament (1766).",
550            Self::NeidhardtI => "A twelve-tone Neidhardt I well temperament (1724).",
551            Self::Silbermann => "A twelve-tone Gottfried Silbermann temperament no. 1 (c. 1730).",
552            Self::LehmanBach => "A twelve-tone Lehman-Bach temperament (2005).",
553        };
554        description.to_string()
555    }
556
557    /// The frequency ratio of a degree above the system's starting pitch.
558    pub fn ratio(self, index: usize) -> FloatType {
559        self.ratio_at(index as FloatType)
560    }
561
562    /// The same for a fractional degree. A whole degree is the system's own
563    /// step exactly; between two, the pitch is interpolated as the equal
564    /// division of the period with as many steps would place it.
565    pub fn ratio_at(self, index: FloatType) -> FloatType {
566        assert!(index.is_finite(), "degree index must be finite");
567        let steps = FloatType::from(self.degrees_per_period());
568        let period = self.period_ratio();
569        let Some(table) = self.ratio_table() else {
570            return period.powf(index / steps);
571        };
572        let whole = index.floor() as IntegerType;
573        let len = IntegerType::try_from(table.len()).expect("ratio table length exceeds i32 range");
574        let fraction = index - FloatType::from(whole);
575        table[whole.rem_euclid(len) as usize].value()
576            * period.powi(whole.div_euclid(len))
577            * period.powf(fraction / steps)
578    }
579
580    /// The degree as an exact ratio, where it is one.
581    ///
582    /// Every ratio table has one for each degree, and so does an equal
583    /// division of a whole-number period, as a root of it: a step of `13edt`
584    /// is the thirteenth root of three. A division of an irrational period,
585    /// Carlos Alpha's 78 cents, has none.
586    pub fn exact_ratio(self, index: usize) -> Option<Ratio> {
587        let index = UnsignedIntegerType::try_from(index).expect("tone index exceeds u32 range");
588        match self {
589            Self::Equal(division) => match division.period_ratio() {
590                Some((base, 1)) => Some(Ratio::root(
591                    UnsignedIntegerType::try_from(base).ok()?,
592                    index,
593                    division.divisions(),
594                )),
595                _ => None,
596            },
597            _ => {
598                let table = self.ratio_table()?;
599                let len = table.len() as UnsignedIntegerType;
600                Some(table[(index % len) as usize].with_octaves(index / len))
601            }
602        }
603    }
604
605    /// A name for a degree, counted from `C-1`.
606    ///
607    /// A system of twelve degrees to the octave names each degree for its key,
608    /// `C♯/D♭4`, however it is tuned, and so does any other system on a degree
609    /// that lands on a key. Every other degree is the pitch it sounds, as
610    /// [`TuningSystem::pitch_at`] spells it, with its microtone: `G0(+2c)` is
611    /// the thirteenth step of Bohlen-Pierce. Ptolemy's intense diatonic and
612    /// the Sa-grama are named in sargam, `Re0`.
613    pub fn label(self, index: UnsignedIntegerType) -> String {
614        let steps = self.degrees_per_period();
615        if matches!(self, Self::PtolemyIntenseDiatonic | Self::IndianAlt) && steps == 7 {
616            let octave = IntegerType::try_from(index / steps).expect("an octave fits an i32") - 1;
617            return format!("{}{octave}", INDIAN_SCALE_NAMES[(index % steps) as usize]);
618        }
619        let index = IntegerType::try_from(index).expect("a degree index fits an i32");
620        if self.is_keyboard() {
621            return key_label(index);
622        }
623        let pitch = self
624            .pitch_at(index)
625            .expect("every degree of a tuning system is a pitch");
626        if pitch.is_twelve_tone() {
627            return key_label(pitch.pitch_space().round() as IntegerType);
628        }
629        let microtone = pitch.microtone().map(ToString::to_string);
630        format!(
631            "{}{}",
632            pitch.unicode_name_with_octave(),
633            microtone.unwrap_or_default()
634        )
635    }
636
637    /// Which period a degree falls in, counting from nought.
638    pub fn period_of(self, index: UnsignedIntegerType) -> UnsignedIntegerType {
639        index / self.degrees_per_period()
640    }
641
642    /// The frequency in hertz of a degree above `C-1`.
643    pub fn frequency(self, index: UnsignedIntegerType) -> FloatType {
644        self.frequency_at(FloatType::from(index))
645    }
646
647    /// The frequency in hertz of a fractional degree above `C-1`.
648    pub fn frequency_at(self, index: FloatType) -> FloatType {
649        CN1 * self.ratio_at(index)
650    }
651
652    /// The degree sounding nearest `hertz`, counted from `C-1` as
653    /// [`TuningSystem::frequency`] counts, and measured in cents. Errors on a
654    /// frequency that is not a finite number above nought.
655    ///
656    /// ```
657    /// use music21_rs::tuningsystem::TuningSystem;
658    ///
659    /// let nineteen = TuningSystem::edo(19);
660    /// // A4 at 440 Hz is nearest degree 5 of the fifth period above C-1.
661    /// assert_eq!(nineteen.nearest_degree(440.0)?, 5 * 19 + 14);
662    /// # Ok::<(), music21_rs::Error>(())
663    /// ```
664    pub fn nearest_degree(self, hertz: FloatType) -> Result<IntegerType> {
665        if !hertz.is_finite() || hertz <= 0.0 {
666            return Err(Error::TuningSystem(format!(
667                "a frequency must be a finite number above nought, got {hertz}"
668            )));
669        }
670        Ok(self.nearest_degree_to_cents(OCTAVE_CENTS * (hertz / CN1).log2()))
671    }
672
673    /// The degree nearest a distance above `C-1` in cents. A table may be
674    /// uneven enough that the nearest degree lies in the next period, so the
675    /// periods either side are searched too.
676    fn nearest_degree_to_cents(self, cents: FloatType) -> IntegerType {
677        let steps = IntegerType::try_from(self.degrees_per_period())
678            .expect("a period's degree count fits an i32");
679        let period = (cents / self.period_cents()).floor() as IntegerType;
680        let distance = |degree: IntegerType| {
681            (OCTAVE_CENTS * self.ratio_at(degree.into()).log2() - cents).abs()
682        };
683        ((period - 1) * steps..=(period + 2) * steps)
684            .min_by(|&a, &b| distance(a).total_cmp(&distance(b)))
685            .expect("the search range is never empty")
686    }
687
688    /// The pitch a degree sounds, counted from `C-1`: spelled as the nearest
689    /// pitch-space value is spelled, with any remainder as a microtone.
690    ///
691    /// ```
692    /// use music21_rs::tuningsystem::TuningSystem;
693    ///
694    /// // Degree 4 of the five-limit table is a just major third above C-1.
695    /// let e = TuningSystem::FiveLimit.pitch_at(64)?;
696    /// assert_eq!(e.name_with_octave(), "E4");
697    /// assert!((e.microtone().unwrap().cents() + 13.686).abs() < 1e-3);
698    /// # Ok::<(), music21_rs::Error>(())
699    /// ```
700    pub fn pitch_at(self, index: IntegerType) -> Result<Pitch> {
701        Pitch::from_pitch_space(OCTAVE_CENTS * self.ratio_at(index.into()).log2() / 100.0)
702    }
703
704    /// How far a degree lies from the same degree of the equal division of
705    /// the same period into as many steps, in cents. Nought all the way up
706    /// for an equal division, which is its own reference.
707    pub fn cents(self, index: UnsignedIntegerType) -> FloatType {
708        self.cents_at(FloatType::from(index))
709    }
710
711    /// The same for a fractional degree.
712    pub fn cents_at(self, index: FloatType) -> FloatType {
713        let equal = self
714            .period_ratio()
715            .powf(index / FloatType::from(self.degrees_per_period()));
716        1200.0 * (self.ratio_at(index) / equal).log2()
717    }
718
719    /// How many degrees the system has before it repeats: twelve for most of
720    /// the tables, and the number of divisions for an equal one.
721    pub fn degrees_per_period(self) -> UnsignedIntegerType {
722        match self {
723            Self::Equal(division) => division.divisions(),
724            _ => self
725                .ratio_table()
726                .map_or(OCTAVE_SIZE, |table| table.len() as UnsignedIntegerType),
727        }
728    }
729
730    /// The interval the system repeats at, in cents: an octave for every
731    /// table, and whatever an equal division divides.
732    pub fn period_cents(self) -> FloatType {
733        match self {
734            Self::Equal(division) => division.period_cents(),
735            _ => OCTAVE_CENTS,
736        }
737    }
738
739    /// Whether the system repeats at the octave.
740    pub fn repeats_at_the_octave(self) -> bool {
741        match self {
742            Self::Equal(division) => division.repeats_at_the_octave(),
743            _ => true,
744        }
745    }
746
747    /// Whether the system has twelve degrees to the octave, one for each key
748    /// of a keyboard.
749    pub fn is_keyboard(self) -> bool {
750        self.degrees_per_period() == 12 && self.repeats_at_the_octave()
751    }
752
753    fn period_ratio(self) -> FloatType {
754        (2.0 as FloatType).powf(self.period_cents() / OCTAVE_CENTS)
755    }
756
757    fn ratio_table(self) -> Option<&'static [Ratio]> {
758        match self {
759            Self::CarlosHarmonic => Some(&CARLOS_HARMONIC),
760            Self::CarlosHarmonic24 => Some(&CARLOS_HARMONIC_24),
761            Self::PythagoreanTuning => Some(&PYTHAGOREAN_TUNING),
762            Self::FiveLimit => Some(&FIVE_LIMIT),
763            Self::ElevenLimit => Some(&ELEVEN_LIMIT),
764            Self::FortyThreeTone => Some(&FORTY_THREE_TONE),
765            Self::Javanese => Some(&JAVANESE),
766            Self::Thai => Some(&THAI),
767            Self::PtolemyIntenseDiatonic => Some(&PTOLEMY_INTENSE_DIATONIC),
768            Self::IndianAlt => Some(&INDIA_SCALE_ALT),
769            Self::Indian22 => Some(&INDIAN_SCALE_22),
770            Self::QuarterCommaMeantone => Some(&QUARTER_COMMA_MEANTONE),
771            Self::WerckmeisterIII => Some(&WERCKMEISTER_III),
772            Self::Rameau => Some(&RAMEAU),
773            Self::KirnbergerIII => Some(&KIRNBERGER_III),
774            Self::Vallotti => Some(&VALLOTTI),
775            Self::YoungII => Some(&YOUNG_II),
776            Self::ThirdCommaMeantone => Some(&THIRD_COMMA_MEANTONE),
777            Self::SixthCommaMeantone => Some(&SIXTH_COMMA_MEANTONE),
778            Self::WerckmeisterIV => Some(&WERCKMEISTER_IV),
779            Self::WerckmeisterV => Some(&WERCKMEISTER_V),
780            Self::KirnbergerI => Some(&KIRNBERGER_I),
781            Self::NeidhardtI => Some(&NEIDHARDT_I),
782            Self::Silbermann => Some(&SILBERMANN),
783            Self::LehmanBach => Some(&LEHMAN_BACH),
784            Self::Equal(_) => None,
785        }
786    }
787
788    /// Twelve-tone equal temperament, `12edo`.
789    pub const EQUAL_TEMPERAMENT: Self = Self::edo(12);
790
791    /// The whole-tone scale, six equal steps to the octave.
792    pub const WHOLE_TONE: Self = Self::edo(6);
793
794    /// Quarter tones, twenty-four equal steps to the octave.
795    pub const QUARTER_TONE: Self = Self::edo(24);
796
797    /// An equal division of the octave into a number of steps fixed where it
798    /// is written, for naming one in a constant. It is a compile error in a
799    /// constant, and a panic otherwise, to ask for no steps at all; a count
800    /// read at runtime goes through [`EqualDivision::octave`], which is an
801    /// error instead.
802    pub const fn edo(divisions: UnsignedIntegerType) -> Self {
803        Self::Equal(EqualDivision::edo(nonzero(divisions)))
804    }
805}
806
807/// A step count that is not nought, for the constants above.
808const fn nonzero(divisions: UnsignedIntegerType) -> NonZeroU32 {
809    match NonZeroU32::new(divisions) {
810        Some(divisions) => divisions,
811        None => panic!("a period cannot be divided into no steps"),
812    }
813}
814
815fn equal_display_name(division: EqualDivision) -> String {
816    match (division.period_ratio(), division.divisions()) {
817        (Some((2, 1)), 12) => "Equal temperament".to_string(),
818        (Some((2, 1)), 6) => "Whole tone".to_string(),
819        (Some((2, 1)), 24) => "Quarter tone".to_string(),
820        (Some((2, 1)), divisions) => format!("{divisions}-tone equal temperament"),
821        _ => format!("{division}"),
822    }
823}
824
825fn equal_description(division: EqualDivision) -> String {
826    let divisions = division.divisions();
827    match division.period_ratio() {
828        Some((2, 1)) => match divisions {
829            12 => "Twelve equal divisions of the octave.".to_string(),
830            6 => "Six equal whole-tone steps per octave.".to_string(),
831            24 => "Twenty-four equal quarter-tone steps per octave.".to_string(),
832            _ => format!("{divisions} equal divisions of the octave."),
833        },
834        Some((numerator, denominator)) => format!(
835            "{divisions} equal divisions of {numerator}/{denominator}, repeating there rather than at the octave."
836        ),
837        None => format!(
838            "{divisions} equal divisions of {:.3} cents, repeating there rather than at the octave.",
839            division.period_cents()
840        ),
841    }
842}
843
844impl Display for TuningSystem {
845    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
846        f.write_str(&self.id())
847    }
848}
849
850impl FromStr for TuningSystem {
851    type Err = Error;
852
853    /// Reads a system's id: a table's name, or an equal division as it
854    /// writes itself (`19edo`, `13edt`, `9ed3/2`). `EqualTemperament`,
855    /// `WholeTone` and `QuarterTone` read as `12edo`, `6edo` and `24edo`.
856    fn from_str(s: &str) -> Result<Self, Self::Err> {
857        match s {
858            "EqualTemperament" => Ok(Self::EQUAL_TEMPERAMENT),
859            "WholeTone" => Ok(Self::WHOLE_TONE),
860            "QuarterTone" => Ok(Self::QUARTER_TONE),
861            "CarlosHarmonic" => Ok(Self::CarlosHarmonic),
862            "CarlosHarmonic24" => Ok(Self::CarlosHarmonic24),
863            "PythagoreanTuning" => Ok(Self::PythagoreanTuning),
864            "FiveLimit" => Ok(Self::FiveLimit),
865            "ElevenLimit" => Ok(Self::ElevenLimit),
866            "FortyThreeTone" => Ok(Self::FortyThreeTone),
867            "Javanese" => Ok(Self::Javanese),
868            "Thai" => Ok(Self::Thai),
869            "PtolemyIntenseDiatonic" => Ok(Self::PtolemyIntenseDiatonic),
870            "IndianAlt" => Ok(Self::IndianAlt),
871            "Indian22" => Ok(Self::Indian22),
872            "QuarterCommaMeantone" => Ok(Self::QuarterCommaMeantone),
873            "WerckmeisterIII" => Ok(Self::WerckmeisterIII),
874            "Rameau" => Ok(Self::Rameau),
875            "KirnbergerIII" => Ok(Self::KirnbergerIII),
876            "Vallotti" => Ok(Self::Vallotti),
877            "YoungII" => Ok(Self::YoungII),
878            "ThirdCommaMeantone" => Ok(Self::ThirdCommaMeantone),
879            "SixthCommaMeantone" => Ok(Self::SixthCommaMeantone),
880            "WerckmeisterIV" => Ok(Self::WerckmeisterIV),
881            "WerckmeisterV" => Ok(Self::WerckmeisterV),
882            "KirnbergerI" => Ok(Self::KirnbergerI),
883            "NeidhardtI" => Ok(Self::NeidhardtI),
884            "Silbermann" => Ok(Self::Silbermann),
885            "LehmanBach" => Ok(Self::LehmanBach),
886            _ if s.contains("ed") => s.parse().map(Self::Equal),
887            _ => Err(Error::TuningSystem(format!("unknown tuning system {s:?}"))),
888        }
889    }
890}
891
892/// A step of an equal division of the octave: `tone` steps of `octave_size`.
893pub const fn equal_temperament(
894    tone: UnsignedIntegerType,
895    octave_size: UnsignedIntegerType,
896) -> Ratio {
897    Ratio::root(2, tone, octave_size)
898}
899
900/// A key of a keyboard by its pitch-space number: a white key by its
901/// letter, a black one by both of its names, `C♯/D♭4`.
902fn key_label(pitch_space: IntegerType) -> String {
903    let pitch =
904        Pitch::from_pitch_space(pitch_space.into()).expect("a whole pitch-space value is a pitch");
905    let octave = pitch_space.div_euclid(12) - 1;
906    let alter = pitch.alter();
907    if alter == 0.0 {
908        return format!("{}{octave}", pitch.unicode_name());
909    }
910    let (sharp, flat) = if alter < 0.0 {
911        (pitch.get_lower_enharmonic(), Ok(pitch))
912    } else {
913        (Ok(pitch.clone()), pitch.get_higher_enharmonic())
914    };
915    let (sharp, flat) = (
916        sharp.expect("a black key has a sharp name"),
917        flat.expect("a black key has a flat name"),
918    );
919    format!("{}/{}{octave}", sharp.unicode_name(), flat.unicode_name())
920}
921
922/// Five-tone Javanese equal-temperament approximation.
923pub const JAVANESE: [Ratio; 5] = [
924    Ratio::root(2, 0, 5),
925    Ratio::root(2, 1, 5),
926    Ratio::root(2, 2, 5),
927    Ratio::root(2, 3, 5),
928    Ratio::root(2, 4, 5),
929];
930
931/// Seven-tone Thai equal-temperament approximation.
932pub const THAI: [Ratio; 7] = [
933    Ratio::root(2, 0, 7),
934    Ratio::root(2, 1, 7),
935    Ratio::root(2, 2, 7),
936    Ratio::root(2, 3, 7),
937    Ratio::root(2, 4, 7),
938    Ratio::root(2, 5, 7),
939    Ratio::root(2, 6, 7),
940];
941
942/// Degree labels for the seven-tone PtolemyIntenseDiatonic scale.
943pub const INDIAN_SCALE_NAMES: [&str; 7] = ["Sa", "Re", "Ga", "Ma", "Pa", "Dha", "Ni"];
944
945#[cfg(test)]
946mod tests {
947    use super::*;
948
949    #[test]
950    fn every_system_answers_cents_and_an_exact_ratio_for_its_degrees() {
951        for system in ALL_TUNING_SYSTEMS {
952            let unison = system.exact_ratio(0).expect("every listed system is exact");
953            assert_eq!(unison.value(), 1.0);
954            assert_eq!(system.cents(0), 0.0);
955            assert!(system.cents(system.degrees_per_period()).abs() < 1e-9);
956        }
957    }
958
959    /// Cents above the tonic for a degree of a twelve-tone table.
960    fn cents_at_degree(system: TuningSystem, degree: usize) -> FloatType {
961        1200.0 * system.ratio(degree).log2()
962    }
963
964    #[test]
965    fn meantone_fifths_match_their_comma_fractions() {
966        // A 1/n-comma meantone narrows the pure fifth by 1/n of the syntonic
967        // comma (21.506 cents). Checking the arithmetic rather than a quoted
968        // table catches a table pointed at the wrong Scala file.
969        const PURE_FIFTH: FloatType = 701.955;
970        const SYNTONIC_COMMA: FloatType = 21.506;
971        for (system, divisor) in [
972            (TuningSystem::ThirdCommaMeantone, 3.0),
973            (TuningSystem::QuarterCommaMeantone, 4.0),
974            (TuningSystem::SixthCommaMeantone, 6.0),
975        ] {
976            let expected = PURE_FIFTH - SYNTONIC_COMMA / divisor;
977            let actual = cents_at_degree(system, 7);
978            assert!(
979                (actual - expected).abs() < 0.01,
980                "{} fifth: expected {expected:.3}, got {actual:.3}",
981                system.display_name()
982            );
983        }
984    }
985
986    #[test]
987    fn kirnberger_i_keeps_its_pure_fifth_and_third() {
988        // Kirnberger I is the outlier of the well temperaments: it leaves the
989        // C-G fifth pure rather than tempering it.
990        assert!((cents_at_degree(TuningSystem::KirnbergerI, 7) - 701.955).abs() < 0.01);
991        assert!((cents_at_degree(TuningSystem::KirnbergerI, 4) - 386.314).abs() < 0.01);
992    }
993
994    #[test]
995    fn common_equal_temperaments_divide_the_octave_evenly() {
996        for system in COMMON_EQUAL_TEMPERAMENTS {
997            let size = system.degrees_per_period();
998            let step = 1200.0 / FloatType::from(size);
999            for degree in 0..size as usize {
1000                let expected = step * degree as FloatType;
1001                let actual = cents_at_degree(system, degree);
1002                assert!(
1003                    (actual - expected).abs() < 0.001,
1004                    "{size}-EDO degree {degree}: expected {expected:.3}, got {actual:.3}"
1005                );
1006            }
1007        }
1008    }
1009
1010    #[test]
1011    fn historical_temperaments_match_their_published_values() {
1012        // Major third and perfect fifth above C, in cents, as given in the
1013        // standard literature. Just values for reference: M3 386.314,
1014        // P5 701.955; equal temperament: 400.000 and 700.000.
1015        let cases = [
1016            (TuningSystem::QuarterCommaMeantone, 386.314, 696.578),
1017            (TuningSystem::WerckmeisterIII, 390.225, 696.090),
1018            (TuningSystem::Rameau, 386.314, 696.578),
1019            (TuningSystem::KirnbergerIII, 386.314, 696.578),
1020            (TuningSystem::Vallotti, 392.180, 698.045),
1021            (TuningSystem::YoungII, 392.180, 698.045),
1022        ];
1023
1024        for (system, major_third, fifth) in cases {
1025            let actual_third = cents_at_degree(system, 4);
1026            let actual_fifth = cents_at_degree(system, 7);
1027            assert!(
1028                (actual_third - major_third).abs() < 0.001,
1029                "{} major third: expected {major_third}, got {actual_third}",
1030                system.display_name()
1031            );
1032            assert!(
1033                (actual_fifth - fifth).abs() < 0.001,
1034                "{} fifth: expected {fifth}, got {actual_fifth}",
1035                system.display_name()
1036            );
1037        }
1038    }
1039
1040    #[test]
1041    fn quarter_comma_meantone_and_kirnberger_have_a_pure_major_third() {
1042        // The defining property of both: C-E is the just 5/4 (386.314 cents).
1043        //
1044        // Not exactly 5/4, though. The Scala archive writes these degrees in
1045        // cents to five decimal places rather than as an exact ratio, so the
1046        // table carries 386.31371 cents. That is 4e-6 cents shy of just - some
1047        // nine orders of magnitude below anything audible - but it is not the
1048        // rational 5/4, and a test asserting exact equality would be asserting
1049        // something the source data does not contain.
1050        for system in [
1051            TuningSystem::QuarterCommaMeantone,
1052            TuningSystem::KirnbergerIII,
1053        ] {
1054            let cents = cents_at_degree(system, 4);
1055            assert!(
1056                (cents - 386.313_714).abs() < 0.001,
1057                "{} major third should be the just 386.314 cents, got {cents}",
1058                system.display_name()
1059            );
1060        }
1061    }
1062
1063    #[test]
1064    fn every_historical_temperament_is_wired_up() {
1065        for system in HISTORICAL_TEMPERAMENTS {
1066            assert_eq!(system.degrees_per_period(), OCTAVE_SIZE, "{system:?}");
1067            assert!(system.ratio_table().is_some(), "{system:?} has no table");
1068            assert_eq!(system.ratio_table().unwrap().len(), 12, "{system:?}");
1069            assert!(!system.description().is_empty(), "{system:?}");
1070            assert_eq!(
1071                TuningSystem::from_str(&system.id()).unwrap(),
1072                system,
1073                "{system:?} does not round-trip through its id"
1074            );
1075            assert!(
1076                ALL_TUNING_SYSTEMS.contains(&system),
1077                "{system:?} missing from ALL_TUNING_SYSTEMS"
1078            );
1079        }
1080    }
1081
1082    #[test]
1083    fn equal_temperament_degree_helpers_work_without_tone_objects() {
1084        assert_eq!(TuningSystem::edo(12).label(0), "C-1");
1085        assert_eq!(TuningSystem::edo(12).period_of(0), 0);
1086        assert!((TuningSystem::edo(12).frequency(0) - CN1).abs() < 1e-12);
1087
1088        assert_eq!(TuningSystem::edo(12).label(69), "A4");
1089        assert_eq!(TuningSystem::edo(12).period_of(69), 5);
1090        assert!((TuningSystem::edo(12).frequency(69) - 440.0).abs() < 0.0001);
1091    }
1092
1093    #[test]
1094    fn fractional_frequency_helpers_support_pitch_space_values() {
1095        let equal = TuningSystem::EQUAL_TEMPERAMENT;
1096        assert!((equal.frequency_at(69.0) - A4).abs() < 0.0001);
1097        assert!((equal.frequency_at(60.0) - C4).abs() < 0.0001);
1098        assert!((TuningSystem::FiveLimit.frequency_at(64.0) - (C4 * 5.0 / 4.0)).abs() < 0.0001);
1099        assert!(
1100            (TuningSystem::PythagoreanTuning.frequency_at(67.0) - (C4 * 3.0 / 2.0)).abs() < 0.0001
1101        );
1102        assert!(TuningSystem::FiveLimit.cents_at(64.0) < -13.0);
1103    }
1104
1105    #[test]
1106    fn ratio_helpers_cover_octaves() {
1107        let two_one: FloatType = Ratio::new(2, 1).into();
1108        assert_eq!(TuningSystem::CarlosHarmonic.ratio(12), two_one);
1109        assert_eq!(TuningSystem::CarlosHarmonic24.ratio(24), two_one);
1110        assert_eq!(TuningSystem::EQUAL_TEMPERAMENT.ratio(12), two_one);
1111    }
1112
1113    #[test]
1114    fn a_ratio_is_rational_or_a_root() {
1115        let rational = Ratio::new(3, 2);
1116        assert_eq!(rational.value(), 1.5);
1117        assert_eq!(rational.to_string(), "3/2");
1118        assert_eq!(rational.with_octaves(2), Ratio::new(12, 2));
1119        assert_eq!(rational.to_fraction(), Some(FractionType::new(3, 2)));
1120        assert_eq!(rational.monzo().unwrap().to_string(), "[-1 1⟩");
1121        assert!((rational.cents() - 701.955).abs() < 1e-3);
1122
1123        let root = Ratio::root(2, 7, 12);
1124        assert_eq!(root.to_string(), "2^(7/12)");
1125        assert_eq!(root.with_octaves(1), Ratio::root(2, 19, 12));
1126        assert!((root.value() - 2.0_f64.powf(7.0 / 12.0)).abs() < 1e-12);
1127        assert_eq!(root.to_fraction(), None);
1128        assert!(root.monzo().is_err());
1129    }
1130
1131    #[test]
1132    fn an_equal_division_carries_its_own_step_count() {
1133        assert_eq!(equal_temperament(12, 12), Ratio::root(2, 12, 12));
1134        let quarter = TuningSystem::QUARTER_TONE;
1135        assert_eq!(quarter.exact_ratio(6), Some(Ratio::root(2, 6, 24)));
1136        assert_eq!(quarter.label(24), "C0");
1137        assert!((quarter.frequency(12) - CN1 * 2.0_f64.sqrt()).abs() < 1e-10);
1138        assert_eq!(quarter.cents(12), 0.0);
1139    }
1140
1141    /// Bohlen-Pierce: thirteen equal steps of a tritave, which repeats there
1142    /// and not at the octave, and is still exact, as roots of three.
1143    #[test]
1144    fn an_equal_division_of_the_tritave_climbs_by_tritaves() {
1145        let bohlen_pierce = TuningSystem::Equal(EqualDivision::tritave(13).unwrap());
1146        assert_eq!(bohlen_pierce.degrees_per_period(), 13);
1147        assert!(!bohlen_pierce.repeats_at_the_octave());
1148        assert!((bohlen_pierce.ratio(13) - 3.0).abs() < 1e-12);
1149        assert!((bohlen_pierce.ratio(26) - 9.0).abs() < 1e-12);
1150        assert_eq!(bohlen_pierce.period_of(26), 2);
1151        assert_eq!(bohlen_pierce.exact_ratio(13), Some(Ratio::root(3, 13, 13)));
1152        assert!(
1153            (bohlen_pierce.exact_ratio(4).unwrap().value() - bohlen_pierce.ratio(4)).abs() < 1e-12
1154        );
1155        assert_eq!(bohlen_pierce.cents(5), 0.0);
1156        assert_eq!(bohlen_pierce.label(13), "G0(+2c)");
1157        assert_eq!(bohlen_pierce.id(), "13edt");
1158        assert_eq!("13edt".parse::<TuningSystem>().unwrap(), bohlen_pierce);
1159    }
1160
1161    /// A period with no whole-number ratio has no exact degrees, and still
1162    /// sounds and names itself.
1163    #[test]
1164    fn an_equal_division_of_an_irrational_period_has_no_fractions() {
1165        let alpha = TuningSystem::Equal(EqualDivision::new(18, 1404.0).unwrap());
1166        assert_eq!(alpha.exact_ratio(1), None);
1167        assert!((1200.0 * alpha.ratio(1).log2() - 78.0).abs() < 1e-9);
1168        assert!((1200.0 * alpha.ratio(18).log2() - 1404.0).abs() < 1e-9);
1169        assert_eq!(alpha.id().parse::<TuningSystem>().unwrap(), alpha);
1170    }
1171
1172    /// The names the equal systems had as variants still read.
1173    #[test]
1174    fn the_old_equal_names_still_parse() {
1175        assert_eq!(
1176            "EqualTemperament".parse::<TuningSystem>().unwrap(),
1177            TuningSystem::EQUAL_TEMPERAMENT
1178        );
1179        assert_eq!(
1180            "WholeTone".parse::<TuningSystem>().unwrap(),
1181            TuningSystem::WHOLE_TONE
1182        );
1183        assert_eq!(
1184            "QuarterTone".parse::<TuningSystem>().unwrap(),
1185            TuningSystem::QUARTER_TONE
1186        );
1187        assert_eq!(
1188            "19edo".parse::<TuningSystem>().unwrap(),
1189            TuningSystem::edo(19)
1190        );
1191        assert_eq!(TuningSystem::EQUAL_TEMPERAMENT.id(), "12edo");
1192        assert_eq!(
1193            TuningSystem::EQUAL_TEMPERAMENT.display_name(),
1194            "Equal temperament"
1195        );
1196        assert_eq!(
1197            TuningSystem::edo(19).display_name(),
1198            "19-tone equal temperament"
1199        );
1200    }
1201
1202    #[test]
1203    fn current_tuning_system_variants_return_ratios() {
1204        assert_eq!(TuningSystem::WHOLE_TONE.ratio(6), 2.0);
1205        assert_eq!(TuningSystem::QUARTER_TONE.ratio(24), 2.0);
1206        assert_eq!(TuningSystem::PythagoreanTuning.ratio(7), 1.5);
1207        assert_eq!(TuningSystem::Indian22.ratio(22), 2.0);
1208    }
1209
1210    #[test]
1211    fn a_keyboard_system_names_its_degrees_for_their_keys() {
1212        let five_limit = TuningSystem::FiveLimit;
1213        assert_eq!(five_limit.label(60), "C4");
1214        assert_eq!(five_limit.label(61), "C♯/D♭4");
1215        assert_eq!(five_limit.label(63), "D♯/E♭4");
1216        assert_eq!(five_limit.label(70), "A♯/B♭4");
1217        assert_eq!(TuningSystem::WHOLE_TONE.label(34), "G♯/A♭4");
1218        // Nineteen steps do not land on keys, so each is the pitch it sounds.
1219        assert_eq!(TuningSystem::edo(19).label(95), "C4");
1220        assert_eq!(TuningSystem::edo(19).label(96), "C𝄲4(+13c)");
1221    }
1222
1223    #[test]
1224    fn the_nearest_degree_to_a_degree_is_that_degree() {
1225        for system in ALL_TUNING_SYSTEMS
1226            .into_iter()
1227            .chain([TuningSystem::Equal(EqualDivision::tritave(13).unwrap())])
1228        {
1229            let steps = system.degrees_per_period() as IntegerType;
1230            for degree in (2 * steps)..(6 * steps) {
1231                let hertz = system.frequency_at(degree.into());
1232                assert_eq!(system.nearest_degree(hertz).unwrap(), degree, "{system:?}");
1233            }
1234        }
1235        assert!(TuningSystem::FiveLimit.nearest_degree(0.0).is_err());
1236        assert!(
1237            TuningSystem::FiveLimit
1238                .nearest_degree(FloatType::INFINITY)
1239                .is_err()
1240        );
1241    }
1242
1243    #[test]
1244    fn a_pitch_sounds_the_nearest_degree_of_a_system_without_its_keys() {
1245        let nineteen = TuningSystem::edo(19);
1246        let a4 = Pitch::from_name("A4").unwrap();
1247        // 440 Hz lies between two steps of nineteen; the nearer is 436.0 Hz.
1248        let sounded = a4.frequency_hz_in(nineteen);
1249        assert!((sounded - nineteen.frequency(109)).abs() < 1e-9);
1250        assert!((sounded - 440.0).abs() < 1200.0 / 19.0);
1251        // A keyboard system still sounds each key's own degree.
1252        let e4 = Pitch::from_name("E4").unwrap();
1253        assert!((e4.frequency_hz_in(TuningSystem::FiveLimit) - 327.032).abs() < 0.001);
1254
1255        // Spelled back to within the six significant digits a pitch-space
1256        // value is kept to.
1257        let back = Pitch::from_frequency_in(sounded, nineteen).unwrap();
1258        assert!((1200.0 * (back.frequency_hz() / sounded).log2()).abs() < 0.01);
1259    }
1260
1261    #[test]
1262    fn table_ratios_shift_by_real_octaves() {
1263        assert_eq!(TuningSystem::CarlosHarmonic.ratio(19), 3.0);
1264        assert_eq!(TuningSystem::FortyThreeTone.ratio(68), 3.0);
1265        assert_eq!(TuningSystem::PtolemyIntenseDiatonic.ratio(8), 2.25);
1266    }
1267
1268    #[test]
1269    fn non_twelve_tone_systems_keep_system_octaves_and_labels() {
1270        assert_eq!(TuningSystem::WHOLE_TONE.label(1), "D-1");
1271        assert_eq!(TuningSystem::WHOLE_TONE.degrees_per_period(), 6);
1272        assert!(
1273            (TuningSystem::WHOLE_TONE.ratio(1) - (2.0 as FloatType).powf(1.0 / 6.0)).abs() < 1e-12
1274        );
1275
1276        assert_eq!(TuningSystem::QUARTER_TONE.label(13), "F♯𝄲-1");
1277        assert_eq!(TuningSystem::QUARTER_TONE.degrees_per_period(), 24);
1278        assert!(
1279            (TuningSystem::QUARTER_TONE.ratio(13) - (2.0 as FloatType).powf(13.0 / 24.0)).abs()
1280                < 1e-12
1281        );
1282
1283        assert_eq!(TuningSystem::Thai.label(7), "C0");
1284        assert_eq!(TuningSystem::Thai.degrees_per_period(), 7);
1285        assert_eq!(TuningSystem::Thai.ratio(7), 2.0);
1286
1287        assert_eq!(TuningSystem::PtolemyIntenseDiatonic.label(8), "Re0");
1288        assert_eq!(TuningSystem::PtolemyIntenseDiatonic.degrees_per_period(), 7);
1289        assert_eq!(TuningSystem::PtolemyIntenseDiatonic.ratio(8), 2.25);
1290
1291        assert_eq!(TuningSystem::FortyThreeTone.label(68), "G0(+2c)");
1292        assert_eq!(TuningSystem::FortyThreeTone.degrees_per_period(), 43);
1293        assert_eq!(TuningSystem::FortyThreeTone.ratio(68), 3.0);
1294    }
1295
1296    #[test]
1297    fn tuning_system_display_and_parse_are_canonical() {
1298        let system = TuningSystem::FiveLimit;
1299        assert_eq!(system.id(), "FiveLimit");
1300        assert_eq!(system.to_string(), "FiveLimit");
1301        assert_eq!("FiveLimit".parse::<TuningSystem>().unwrap(), system);
1302
1303        let err = "not-a-system".parse::<TuningSystem>().unwrap_err();
1304        assert_eq!(
1305            err,
1306            Error::TuningSystem("unknown tuning system \"not-a-system\"".to_string())
1307        );
1308    }
1309
1310    #[test]
1311    fn tuning_system_display_names_cover_variants() {
1312        for system in ALL_TUNING_SYSTEMS {
1313            assert!(!system.id().is_empty());
1314            assert!(!system.display_name().is_empty());
1315            assert!(!system.description().is_empty());
1316            assert!(system.degrees_per_period() > 0);
1317            assert_eq!(system.to_string(), system.id());
1318        }
1319    }
1320
1321    #[test]
1322    fn twelve_tone_systems_keep_chromatic_ratios_ascending() {
1323        for system in ALL_TUNING_SYSTEMS
1324            .into_iter()
1325            .filter(|system| system.degrees_per_period() == OCTAVE_SIZE)
1326        {
1327            let mut previous = system.ratio(0);
1328            for degree in 1..=OCTAVE_SIZE {
1329                let ratio = system.ratio(degree as usize);
1330                assert!(
1331                    ratio > previous,
1332                    "{} degree {degree} ratio {ratio} should be higher than {previous}",
1333                    system.id()
1334                );
1335                previous = ratio;
1336            }
1337        }
1338    }
1339
1340    #[test]
1341    fn all_ratio_tables_keep_degrees_strictly_ascending_within_the_octave() {
1342        for system in ALL_TUNING_SYSTEMS {
1343            let octave_size = system.degrees_per_period();
1344            let mut previous = system.ratio(0);
1345            for degree in 1..=octave_size {
1346                let ratio = system.ratio(degree as usize);
1347                assert!(
1348                    ratio > previous,
1349                    "{} degree {degree} ratio {ratio} should be higher than {previous} \
1350                     (a table entry is likely mistranscribed)",
1351                    system.id()
1352                );
1353                previous = ratio;
1354            }
1355        }
1356    }
1357}