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
25pub use equal::{EqualDivision, TRITAVE_CENTS};
26pub use generated::*;
27pub use monzo::{Monzo, PRIMES, Val};
28pub use mos::{Mos, MosScale, OCTAVE_CENTS, moment_of_symmetry_sizes};
29pub use temperament::Temperament;
30pub use temperaments_generated::*;
31
32use crate::defaults::{FloatType, IntegerType, UnsignedIntegerType};
33use crate::error::{Error, Result};
34use crate::tuningsystem::adaptive::AdaptiveTuningSystem;
35
36use std::fmt::{Display, Formatter};
37use std::str::FromStr;
38
39/// Default octave size for twelve-tone systems.
40pub const OCTAVE_SIZE: UnsignedIntegerType = 12;
41
42/// Frequency of middle C in hertz.
43pub const C4: FloatType = 261.625_565_300_598_6;
44/// Frequency of C0 in hertz.
45pub const C0: FloatType = C4 / 16.0;
46/// Frequency of C-1 in hertz.
47pub const CN1: FloatType = C4 / 32.0;
48
49/// Frequency of A4 in hertz.
50pub const A4: FloatType = 440.0;
51/// Frequency of A0 in hertz.
52pub const A0: FloatType = A4 / 16.0;
53/// Frequency of A-1 in hertz.
54pub const AN1: FloatType = A4 / 32.0;
55
56/// Degree labels for a twelve-tone chromatic octave.
57pub const TWELVE_TONE_NAMES: [&str; 12] = [
58    "C", "C#/Db", "D", "D#/Eb", "E", "F", "F#/Gb", "G", "G#/Ab", "A", "A#/Bb", "B",
59];
60
61/// Degree labels for a twelve-tone chromatic octave using sharps.
62pub const TWELVE_TONE_NAMES_SHARP: [&str; 12] = [
63    "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
64];
65
66/// Degree labels for a twelve-tone chromatic octave using flats.
67pub const TWELVE_TONE_NAMES_FLAT: [&str; 12] = [
68    "C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B",
69];
70
71/// Degree labels for a whole-tone octave.
72pub const WHOLE_TONE_NAMES: [&str; 6] = ["C", "D", "E", "F#/Gb", "G#/Ab", "A#/Bb"];
73
74/// The common twelve-tone tuning systems useful for comparing pitch frequencies.
75pub const COMMON_TWELVE_TONE_TUNING_SYSTEMS: [TuningSystem; 4] = [
76    TuningSystem::EqualTemperament {
77        octave_size: OCTAVE_SIZE,
78    },
79    TuningSystem::CarlosHarmonic,
80    TuningSystem::PythagoreanTuning,
81    TuningSystem::FiveLimit,
82];
83
84/// The equal divisions of the octave that xenharmonic practice actually uses.
85///
86/// Any EDO is already expressible as
87/// `TuningSystem::EqualTemperament { octave_size: n }`; this names the ones
88/// worth reaching for. 19 and 31 support meantone, 22 deliberately does not,
89/// 53 gets 5-limit harmony almost exact, and 72 is the usual choice for
90/// notating 11-limit music.
91pub const COMMON_EQUAL_TEMPERAMENTS: [TuningSystem; 8] = [
92    TuningSystem::EqualTemperament { octave_size: 12 },
93    TuningSystem::EqualTemperament { octave_size: 19 },
94    TuningSystem::EqualTemperament { octave_size: 22 },
95    TuningSystem::EqualTemperament { octave_size: 24 },
96    TuningSystem::EqualTemperament { octave_size: 31 },
97    TuningSystem::EqualTemperament { octave_size: 41 },
98    TuningSystem::EqualTemperament { octave_size: 53 },
99    TuningSystem::EqualTemperament { octave_size: 72 },
100];
101
102/// Historical keyboard temperaments, oldest first.
103///
104/// These are the well temperaments and meantone tunings that Western keyboard
105/// music was actually written for, transcribed from the Scala archive in the
106/// `music21` reference submodule. All are twelve-tone.
107pub const HISTORICAL_TEMPERAMENTS: [TuningSystem; 14] = [
108    TuningSystem::QuarterCommaMeantone,
109    TuningSystem::WerckmeisterIII,
110    TuningSystem::Rameau,
111    TuningSystem::KirnbergerIII,
112    TuningSystem::Vallotti,
113    TuningSystem::YoungII,
114    TuningSystem::ThirdCommaMeantone,
115    TuningSystem::SixthCommaMeantone,
116    TuningSystem::WerckmeisterIV,
117    TuningSystem::WerckmeisterV,
118    TuningSystem::KirnbergerI,
119    TuningSystem::NeidhardtI,
120    TuningSystem::Silbermann,
121    TuningSystem::LehmanBach,
122];
123
124/// Either a normal tuning system or a context-sensitive adaptive tuning system.
125#[derive(Clone, Copy, Debug, Eq, PartialEq)]
126#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
127#[must_use]
128pub enum AnyTuningSystem {
129    /// A fixed system, which answers the same way whatever it sounds against.
130    Fixed(TuningSystem),
131    /// An adaptive system, whose answer depends on the context it is given.
132    Adaptive(AdaptiveTuningSystem),
133}
134
135impl AnyTuningSystem {
136    /// The frequency of a degree, in Hz.
137    ///
138    /// `context` is the frequency an adaptive system tunes against; a fixed
139    /// system ignores it.
140    pub fn frequency_at(
141        self,
142        context: FloatType,
143        index: FloatType,
144        size: Option<UnsignedIntegerType>,
145    ) -> FloatType {
146        match self {
147            Self::Fixed(tuning_system) => {
148                let _ = context;
149                get_frequency_at(tuning_system, index, size)
150            }
151            Self::Adaptive(adaptive_tuning_system) => {
152                adaptive_tuning_system.frequency_at(context, index, size)
153            }
154        }
155    }
156
157    /// The degree's distance above the tonic, in cents.
158    ///
159    /// `context` is the frequency an adaptive system tunes against; a fixed
160    /// system ignores it.
161    pub fn cents_at(
162        self,
163        context: FloatType,
164        index: FloatType,
165        size: Option<UnsignedIntegerType>,
166    ) -> FloatType {
167        match self {
168            Self::Fixed(tuning_system) => {
169                let _ = context;
170                tuning_system.cents_at(index)
171            }
172            Self::Adaptive(adaptive_tuning_system) => {
173                adaptive_tuning_system.cents_at(context, index, size)
174            }
175        }
176    }
177
178    /// Whether this system's answers depend on the context they are asked in.
179    pub fn is_adaptive(self) -> bool {
180        matches!(self, Self::Adaptive(_))
181    }
182}
183
184impl From<TuningSystem> for AnyTuningSystem {
185    fn from(tuning_system: TuningSystem) -> Self {
186        Self::Fixed(tuning_system)
187    }
188}
189
190impl From<AdaptiveTuningSystem> for AnyTuningSystem {
191    fn from(adaptive_tuning_system: AdaptiveTuningSystem) -> Self {
192        Self::Adaptive(adaptive_tuning_system)
193    }
194}
195
196/// All built-in tuning systems in canonical display order.
197pub const ALL_TUNING_SYSTEMS: [TuningSystem; 28] = [
198    TuningSystem::EqualTemperament {
199        octave_size: OCTAVE_SIZE,
200    },
201    TuningSystem::WholeTone,
202    TuningSystem::QuarterTone,
203    TuningSystem::CarlosHarmonic,
204    TuningSystem::CarlosHarmonic24,
205    TuningSystem::PythagoreanTuning,
206    TuningSystem::FiveLimit,
207    TuningSystem::ElevenLimit,
208    TuningSystem::FortyThreeTone,
209    TuningSystem::Javanese,
210    TuningSystem::Thai,
211    TuningSystem::PtolemyIntenseDiatonic,
212    TuningSystem::IndianAlt,
213    TuningSystem::Indian22,
214    TuningSystem::QuarterCommaMeantone,
215    TuningSystem::WerckmeisterIII,
216    TuningSystem::Rameau,
217    TuningSystem::KirnbergerIII,
218    TuningSystem::Vallotti,
219    TuningSystem::YoungII,
220    TuningSystem::ThirdCommaMeantone,
221    TuningSystem::SixthCommaMeantone,
222    TuningSystem::WerckmeisterIV,
223    TuningSystem::WerckmeisterV,
224    TuningSystem::KirnbergerI,
225    TuningSystem::NeidhardtI,
226    TuningSystem::Silbermann,
227    TuningSystem::LehmanBach,
228];
229
230#[derive(Clone, Copy, Debug, Eq, PartialEq)]
231#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
232/// A ratio-like value used by tuning tables.
233#[must_use]
234pub struct Fraction {
235    /// Numerator for a rational ratio, or exponent numerator when `base` is set.
236    pub numerator: UnsignedIntegerType,
237    /// Denominator for a rational ratio, or exponent denominator when `base` is set.
238    pub denominator: UnsignedIntegerType,
239    /// Exponential base. A value of `0` means use `numerator / denominator`.
240    pub base: UnsignedIntegerType,
241}
242
243impl Fraction {
244    /// Creates a rational fraction.
245    pub const fn new(numerator: UnsignedIntegerType, denominator: UnsignedIntegerType) -> Self {
246        Self::new_with_base(numerator, denominator, 0)
247    }
248
249    /// Creates a fraction with an optional exponential base.
250    pub const fn new_with_base(
251        numerator: UnsignedIntegerType,
252        denominator: UnsignedIntegerType,
253        base: UnsignedIntegerType,
254    ) -> Self {
255        Self {
256            numerator,
257            denominator,
258            base,
259        }
260    }
261
262    /// Returns the numerator.
263    pub const fn numerator(&self) -> UnsignedIntegerType {
264        self.numerator
265    }
266
267    /// Returns the denominator.
268    pub const fn denominator(&self) -> UnsignedIntegerType {
269        self.denominator
270    }
271
272    /// Returns the exponential base, or `0` for rational ratios.
273    pub const fn base(&self) -> UnsignedIntegerType {
274        self.base
275    }
276
277    /// Converts this value into a floating-point ratio.
278    pub fn ratio(self) -> FloatType {
279        self.into()
280    }
281
282    /// Returns a compact music-friendly display label.
283    pub fn label(self) -> String {
284        self.to_string()
285    }
286
287    /// Returns this fraction shifted upward by `octaves`.
288    pub fn with_octaves(mut self, octaves: UnsignedIntegerType) -> Self {
289        if octaves == 0 {
290            return self;
291        }
292
293        if self.base == 0 {
294            let multiplier = (2 as UnsignedIntegerType)
295                .checked_pow(octaves)
296                .expect("octave multiplier exceeds u32 range");
297            self.numerator = self
298                .numerator
299                .checked_mul(multiplier)
300                .expect("fraction numerator exceeds u32 range");
301        } else {
302            let octave_offset = self
303                .denominator
304                .checked_mul(octaves)
305                .expect("fraction octave offset exceeds u32 range");
306            self.numerator = self
307                .numerator
308                .checked_add(octave_offset)
309                .expect("fraction numerator exceeds u32 range");
310        }
311
312        self
313    }
314}
315
316impl From<Fraction> for FloatType {
317    fn from(frac: Fraction) -> Self {
318        if frac.base == 0 {
319            frac.numerator as FloatType / frac.denominator as FloatType
320        } else {
321            (frac.base as FloatType)
322                .powf(frac.numerator as FloatType / frac.denominator as FloatType)
323        }
324    }
325}
326
327impl Display for Fraction {
328    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
329        if self.base == 0 {
330            if self.denominator == 1 {
331                write!(f, "{}", self.numerator)
332            } else {
333                write!(f, "{}/{}", self.numerator, self.denominator)
334            }
335        } else if self.numerator == 0 {
336            write!(f, "1")
337        } else {
338            write!(f, "{}^({}/{})", self.base, self.numerator, self.denominator)
339        }
340    }
341}
342
343impl From<(UnsignedIntegerType, UnsignedIntegerType)> for Fraction {
344    fn from(frac: (UnsignedIntegerType, UnsignedIntegerType)) -> Self {
345        Self::new(frac.0, frac.1)
346    }
347}
348
349impl
350    From<(
351        UnsignedIntegerType,
352        UnsignedIntegerType,
353        UnsignedIntegerType,
354    )> for Fraction
355{
356    fn from(
357        frac: (
358            UnsignedIntegerType,
359            UnsignedIntegerType,
360            UnsignedIntegerType,
361        ),
362    ) -> Self {
363        Self::new_with_base(frac.0, frac.1, frac.2)
364    }
365}
366
367#[derive(Clone, Copy, Debug, Eq, PartialEq)]
368#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
369/// Supported tuning systems and ratio tables.
370#[must_use]
371pub enum TuningSystem {
372    /// Equal temperament with a configurable octave size.
373    EqualTemperament {
374        /// Number of equal divisions in each octave.
375        octave_size: UnsignedIntegerType,
376    },
377    /// Six-tone equal temperament.
378    WholeTone,
379    /// Twenty-four-tone equal temperament.
380    QuarterTone,
381
382    /// Twelve-tone harmonic-series scale (Wendy Carlos's Harmonic).
383    ///
384    /// Not just intonation, despite its former name: the degrees are the
385    /// harmonic series 16:17:18:19:20:21:22:24:26:27:28:30, which reaches the
386    /// 19-limit. [`TuningSystem::FiveLimit`] is the classical 12-tone JI scale.
387    CarlosHarmonic,
388    /// Twenty-four-tone harmonic-series scale.
389    CarlosHarmonic24,
390    /// Twelve-tone Pythagorean tuning table.
391    PythagoreanTuning,
392
393    /// Twelve-tone five-limit table.
394    FiveLimit,
395    /// Twenty-nine-tone eleven-limit table.
396    ElevenLimit,
397
398    /// Forty-three-tone ratio table.
399    FortyThreeTone,
400
401    // Ethnic scales.
402    /// Five-tone Javanese equal-temperament approximation.
403    Javanese,
404    /// Seven-tone Thai equal-temperament approximation.
405    Thai,
406    /// Ptolemy's intense diatonic, also Zarlino's just major scale.
407    ///
408    /// Greek and Renaissance European, despite once being filed here as an
409    /// Indian scale — the archive names it `ptolemy.scl`.
410    PtolemyIntenseDiatonic,
411    /// Alternate seven-tone PtolemyIntenseDiatonic scale table.
412    IndianAlt,
413    /// Twenty-two-tone PtolemyIntenseDiatonic scale table.
414    Indian22,
415
416    // Historical keyboard temperaments, transcribed from the Scala archive.
417    /// Twelve-tone quarter-comma meantone temperament (Aaron, 1523).
418    QuarterCommaMeantone,
419    /// Twelve-tone Werckmeister III well temperament (1681).
420    WerckmeisterIII,
421    /// Twelve-tone Rameau modified meantone temperament (1725).
422    Rameau,
423    /// Twelve-tone Kirnberger III well temperament (1744).
424    KirnbergerIII,
425    /// Twelve-tone Vallotti well temperament (c. 1754).
426    Vallotti,
427    /// Twelve-tone Thomas Young well temperament no. 2 (1799).
428    YoungII,
429    /// A twelve-tone third-comma meantone temperament (Salinas, 1577).
430    ThirdCommaMeantone,
431    /// A twelve-tone sixth-comma meantone temperament (Salinas, 1577).
432    SixthCommaMeantone,
433    /// A twelve-tone Werckmeister IV well temperament (1681).
434    WerckmeisterIV,
435    /// A twelve-tone Werckmeister V well temperament (1681).
436    WerckmeisterV,
437    /// A twelve-tone Kirnberger I well temperament (1766).
438    KirnbergerI,
439    /// A twelve-tone Neidhardt I well temperament (1724).
440    NeidhardtI,
441    /// A twelve-tone Gottfried Silbermann temperament no. 1 (c. 1730).
442    Silbermann,
443    /// A twelve-tone Lehman-Bach temperament (2005).
444    LehmanBach,
445}
446
447impl TuningSystem {
448    /// Returns the canonical identifier used by [`FromStr`].
449    pub fn id(self) -> &'static str {
450        match self {
451            Self::EqualTemperament { .. } => "EqualTemperament",
452            Self::WholeTone => "WholeTone",
453            Self::QuarterTone => "QuarterTone",
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    }
481
482    /// Returns a compact display name for this tuning system.
483    pub fn display_name(self) -> &'static str {
484        match self {
485            Self::EqualTemperament { .. } => "Equal temperament",
486            Self::WholeTone => "Whole tone",
487            Self::QuarterTone => "Quarter tone",
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    }
515
516    /// Returns a short description of this tuning system.
517    pub fn description(self) -> &'static str {
518        match self {
519            Self::EqualTemperament { .. } => "Twelve equal divisions of the octave.",
520            Self::WholeTone => "Six equal whole-tone steps per octave.",
521            Self::QuarterTone => "Twenty-four equal quarter-tone steps per octave.",
522            Self::CarlosHarmonic => "A twelve-tone harmonic-series scale, reaching the 19-limit.",
523            Self::CarlosHarmonic24 => "A twenty-four-tone harmonic-series scale.",
524            Self::PythagoreanTuning => "A twelve-tone tuning table built from pure fifths.",
525            Self::FiveLimit => "A twelve-tone table using five-limit just ratios.",
526            Self::ElevenLimit => "Harry Partch's twenty-nine-tone 11-limit tonality diamond.",
527            Self::FortyThreeTone => "Harry Partch's forty-three-tone pure scale.",
528            Self::Javanese => "A five-tone Javanese equal-temperament approximation.",
529            Self::Thai => "A seven-tone Thai equal-temperament approximation.",
530            Self::PtolemyIntenseDiatonic => {
531                "Ptolemy's intense diatonic, also Zarlino's just major scale."
532            }
533            Self::IndianAlt => "The Indian Sa-grama mode, the inverse of Didymus' diatonic.",
534            Self::Indian22 => "The twenty-two-shruti Indian scale.",
535            Self::QuarterCommaMeantone => {
536                "A twelve-tone quarter-comma meantone temperament (Aaron, 1523)."
537            }
538            Self::WerckmeisterIII => "A twelve-tone Werckmeister III well temperament (1681).",
539            Self::Rameau => "A twelve-tone Rameau modified meantone temperament (1725).",
540            Self::KirnbergerIII => "A twelve-tone Kirnberger III well temperament (1744).",
541            Self::Vallotti => "A twelve-tone Vallotti well temperament (c. 1754).",
542            Self::YoungII => "A twelve-tone Thomas Young well temperament no. 2 (1799).",
543            Self::ThirdCommaMeantone => {
544                "A twelve-tone third-comma meantone temperament (Salinas, 1577)."
545            }
546            Self::SixthCommaMeantone => {
547                "A twelve-tone sixth-comma meantone temperament (Salinas, 1577)."
548            }
549            Self::WerckmeisterIV => "A twelve-tone Werckmeister IV well temperament (1681).",
550            Self::WerckmeisterV => "A twelve-tone Werckmeister V well temperament (1681).",
551            Self::KirnbergerI => "A twelve-tone Kirnberger I well temperament (1766).",
552            Self::NeidhardtI => "A twelve-tone Neidhardt I well temperament (1724).",
553            Self::Silbermann => "A twelve-tone Gottfried Silbermann temperament no. 1 (c. 1730).",
554            Self::LehmanBach => "A twelve-tone Lehman-Bach temperament (2005).",
555        }
556    }
557
558    /// Returns the frequency ratio for a degree index.
559    pub fn ratio(self, index: usize) -> FloatType {
560        get_ratio(self, index, None)
561    }
562
563    /// Returns the table fraction for a degree index.
564    pub fn fraction(self, index: usize) -> Fraction {
565        get_fraction(self, index, None)
566    }
567
568    /// Returns a display label for a degree index.
569    pub fn label(self, index: UnsignedIntegerType) -> String {
570        get_label(self, index, None)
571    }
572
573    /// Returns the octave number containing a degree index.
574    pub fn octave(self, index: UnsignedIntegerType) -> UnsignedIntegerType {
575        index / self.octave_size()
576    }
577
578    /// Returns the frequency in hertz for a degree index.
579    pub fn frequency(self, index: UnsignedIntegerType) -> FloatType {
580        get_frequency(self, index, None)
581    }
582
583    /// Returns the frequency in hertz for a fractional degree index.
584    pub fn frequency_at(self, index: FloatType) -> FloatType {
585        get_frequency_at(self, index, None)
586    }
587
588    /// Returns cents offset from equal temperament for a degree index.
589    pub fn cents(self, index: UnsignedIntegerType) -> FloatType {
590        get_cents(self, index, None)
591    }
592
593    /// Returns cents offset from equal temperament for a fractional degree index.
594    pub fn cents_at(self, index: FloatType) -> FloatType {
595        get_cents_at(self, index, None)
596    }
597
598    /// Returns the number of degrees in one octave for this tuning system.
599    pub fn octave_size(self) -> UnsignedIntegerType {
600        match self {
601            Self::EqualTemperament { octave_size } => octave_size,
602            Self::WholeTone => 6,
603            Self::QuarterTone | Self::CarlosHarmonic24 => 24,
604            Self::FortyThreeTone => 43,
605            Self::ElevenLimit => 29,
606            Self::Javanese => 5,
607            Self::Thai | Self::PtolemyIntenseDiatonic | Self::IndianAlt => 7,
608            Self::Indian22 => 22,
609            Self::QuarterCommaMeantone
610            | Self::WerckmeisterIII
611            | Self::Rameau
612            | Self::KirnbergerIII
613            | Self::Vallotti
614            | Self::YoungII
615            | Self::ThirdCommaMeantone
616            | Self::SixthCommaMeantone
617            | Self::WerckmeisterIV
618            | Self::WerckmeisterV
619            | Self::KirnbergerI
620            | Self::NeidhardtI
621            | Self::Silbermann
622            | Self::LehmanBach => OCTAVE_SIZE,
623            Self::CarlosHarmonic | Self::PythagoreanTuning | Self::FiveLimit => OCTAVE_SIZE,
624        }
625    }
626
627    fn ratio_table(self) -> Option<&'static [Fraction]> {
628        match self {
629            Self::CarlosHarmonic => Some(&CARLOS_HARMONIC),
630            Self::CarlosHarmonic24 => Some(&CARLOS_HARMONIC_24),
631            Self::PythagoreanTuning => Some(&PYTHAGOREAN_TUNING),
632            Self::FiveLimit => Some(&FIVE_LIMIT),
633            Self::ElevenLimit => Some(&ELEVEN_LIMIT),
634            Self::FortyThreeTone => Some(&FORTY_THREE_TONE),
635            Self::Javanese => Some(&JAVANESE),
636            Self::Thai => Some(&THAI),
637            Self::PtolemyIntenseDiatonic => Some(&PTOLEMY_INTENSE_DIATONIC),
638            Self::IndianAlt => Some(&INDIA_SCALE_ALT),
639            Self::Indian22 => Some(&INDIAN_SCALE_22),
640            Self::QuarterCommaMeantone => Some(&QUARTER_COMMA_MEANTONE),
641            Self::WerckmeisterIII => Some(&WERCKMEISTER_III),
642            Self::Rameau => Some(&RAMEAU),
643            Self::KirnbergerIII => Some(&KIRNBERGER_III),
644            Self::Vallotti => Some(&VALLOTTI),
645            Self::YoungII => Some(&YOUNG_II),
646            Self::ThirdCommaMeantone => Some(&THIRD_COMMA_MEANTONE),
647            Self::SixthCommaMeantone => Some(&SIXTH_COMMA_MEANTONE),
648            Self::WerckmeisterIV => Some(&WERCKMEISTER_IV),
649            Self::WerckmeisterV => Some(&WERCKMEISTER_V),
650            Self::KirnbergerI => Some(&KIRNBERGER_I),
651            Self::NeidhardtI => Some(&NEIDHARDT_I),
652            Self::Silbermann => Some(&SILBERMANN),
653            Self::LehmanBach => Some(&LEHMAN_BACH),
654            Self::EqualTemperament { .. } | Self::WholeTone | Self::QuarterTone => None,
655        }
656    }
657
658    fn degree_label(self, index: UnsignedIntegerType, octave_size: UnsignedIntegerType) -> String {
659        if octave_size == 0 {
660            return default_degree_label(OCTAVE_SIZE, index);
661        }
662
663        let degree = index % octave_size;
664        match self {
665            Self::WholeTone if octave_size == 6 => WHOLE_TONE_NAMES[degree as usize].to_string(),
666            Self::PtolemyIntenseDiatonic | Self::IndianAlt if octave_size == 7 => {
667                INDIAN_SCALE_NAMES[degree as usize].to_string()
668            }
669            _ => default_degree_label(octave_size, index),
670        }
671    }
672}
673
674impl Display for TuningSystem {
675    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
676        f.write_str(self.id())
677    }
678}
679
680impl FromStr for TuningSystem {
681    type Err = Error;
682
683    fn from_str(s: &str) -> Result<Self, Self::Err> {
684        match s {
685            "EqualTemperament" => Ok(Self::EqualTemperament {
686                octave_size: OCTAVE_SIZE,
687            }),
688            "WholeTone" => Ok(Self::WholeTone),
689            "QuarterTone" => Ok(Self::QuarterTone),
690            "CarlosHarmonic" => Ok(Self::CarlosHarmonic),
691            "CarlosHarmonic24" => Ok(Self::CarlosHarmonic24),
692            "PythagoreanTuning" => Ok(Self::PythagoreanTuning),
693            "FiveLimit" => Ok(Self::FiveLimit),
694            "ElevenLimit" => Ok(Self::ElevenLimit),
695            "FortyThreeTone" => Ok(Self::FortyThreeTone),
696            "Javanese" => Ok(Self::Javanese),
697            "Thai" => Ok(Self::Thai),
698            "PtolemyIntenseDiatonic" => Ok(Self::PtolemyIntenseDiatonic),
699            "IndianAlt" => Ok(Self::IndianAlt),
700            "Indian22" => Ok(Self::Indian22),
701            "QuarterCommaMeantone" => Ok(Self::QuarterCommaMeantone),
702            "WerckmeisterIII" => Ok(Self::WerckmeisterIII),
703            "Rameau" => Ok(Self::Rameau),
704            "KirnbergerIII" => Ok(Self::KirnbergerIII),
705            "Vallotti" => Ok(Self::Vallotti),
706            "YoungII" => Ok(Self::YoungII),
707            "ThirdCommaMeantone" => Ok(Self::ThirdCommaMeantone),
708            "SixthCommaMeantone" => Ok(Self::SixthCommaMeantone),
709            "WerckmeisterIV" => Ok(Self::WerckmeisterIV),
710            "WerckmeisterV" => Ok(Self::WerckmeisterV),
711            "KirnbergerI" => Ok(Self::KirnbergerI),
712            "NeidhardtI" => Ok(Self::NeidhardtI),
713            "Silbermann" => Ok(Self::Silbermann),
714            "LehmanBach" => Ok(Self::LehmanBach),
715            _ => Err(Error::TuningSystem(format!("unknown tuning system {s:?}"))),
716        }
717    }
718}
719
720/// Creates an equal-temperament fraction for `tone` within `octave_size`.
721pub fn equal_temperament(tone: UnsignedIntegerType, octave_size: UnsignedIntegerType) -> Fraction {
722    Fraction::new_with_base(tone, octave_size, 2)
723}
724
725/// Creates a twelve-tone equal-temperament fraction.
726pub fn equal_temperament_12(tone: UnsignedIntegerType) -> Fraction {
727    equal_temperament(tone, 12)
728}
729
730/// Creates an equal-temperament fraction using [`OCTAVE_SIZE`].
731pub fn equal_temperament_default(tone: UnsignedIntegerType) -> Fraction {
732    equal_temperament(tone, OCTAVE_SIZE)
733}
734
735/// Returns the frequency ratio for a tuning-system degree.
736pub fn get_ratio(
737    tuning_system: TuningSystem,
738    index: usize,
739    size: Option<UnsignedIntegerType>,
740) -> FloatType {
741    get_fraction(tuning_system, index, size).into()
742}
743
744/// Returns the fraction for a tuning-system degree.
745///
746/// The optional `size` overrides the tuning system's octave size for
747/// equal-temperament-style systems.
748pub fn get_fraction(
749    tuning_system: TuningSystem,
750    index: usize,
751    size: Option<UnsignedIntegerType>,
752) -> Fraction {
753    match tuning_system {
754        TuningSystem::EqualTemperament { octave_size } => equal_temperament(
755            index_to_unsigned_integer(index),
756            size.unwrap_or(octave_size),
757        ),
758        TuningSystem::WholeTone => {
759            equal_temperament(index_to_unsigned_integer(index), size.unwrap_or(6))
760        }
761        TuningSystem::QuarterTone => {
762            equal_temperament(index_to_unsigned_integer(index), size.unwrap_or(24))
763        }
764        _ => get_fraction_from_table(tuning_system, index),
765    }
766}
767
768/// Returns a display label for a tuning-system degree.
769///
770/// The optional `size` overrides the tuning system's octave size for label
771/// calculation.
772pub fn get_label(
773    tuning_system: TuningSystem,
774    index: UnsignedIntegerType,
775    size: Option<UnsignedIntegerType>,
776) -> String {
777    let octave_size = size.unwrap_or_else(|| tuning_system.octave_size());
778    assert!(octave_size > 0, "octave_size must be greater than zero");
779    degree_name_with_octave(
780        &tuning_system.degree_label(index, octave_size),
781        index / octave_size,
782    )
783}
784
785/// Returns the frequency in hertz for a tuning-system degree.
786///
787/// The optional `size` overrides the tuning system's octave size for
788/// equal-temperament-style systems.
789pub fn get_frequency(
790    tuning_system: TuningSystem,
791    index: UnsignedIntegerType,
792    size: Option<UnsignedIntegerType>,
793) -> FloatType {
794    get_frequency_at(tuning_system, FloatType::from(index), size)
795}
796
797/// Returns the frequency in hertz for a fractional tuning-system degree.
798///
799/// Integer degrees use the tuning system table exactly. Fractional degrees are
800/// interpolated by equal-temperament distance within the same octave.
801pub fn get_frequency_at(
802    tuning_system: TuningSystem,
803    index: FloatType,
804    size: Option<UnsignedIntegerType>,
805) -> FloatType {
806    CN1 * get_ratio_at(tuning_system, index, size)
807}
808
809fn get_ratio_at(
810    tuning_system: TuningSystem,
811    index: FloatType,
812    size: Option<UnsignedIntegerType>,
813) -> FloatType {
814    assert!(index.is_finite(), "degree index must be finite");
815    let octave_size = size.unwrap_or_else(|| tuning_system.octave_size());
816    assert!(octave_size > 0, "octave_size must be greater than zero");
817
818    if tuning_system.ratio_table().is_none() {
819        return (2.0 as FloatType).powf(index / FloatType::from(octave_size));
820    }
821
822    let base_index = index.floor() as IntegerType;
823    let fractional_degree = index - FloatType::from(base_index);
824    get_ratio_at_integer_index(tuning_system, base_index)
825        * (2.0 as FloatType).powf(fractional_degree / FloatType::from(octave_size))
826}
827
828/// Returns cents offset from equal temperament for a tuning-system degree.
829///
830/// The optional `size` overrides the tuning system's octave size for the
831/// equal-temperament comparison.
832pub fn get_cents(
833    tuning_system: TuningSystem,
834    index: UnsignedIntegerType,
835    size: Option<UnsignedIntegerType>,
836) -> FloatType {
837    get_cents_at(tuning_system, FloatType::from(index), size)
838}
839
840/// Returns cents offset from equal temperament for a fractional degree index.
841///
842/// The optional `size` overrides the octave size of the equal-temperament
843/// comparison.
844pub fn get_cents_at(
845    tuning_system: TuningSystem,
846    index: FloatType,
847    size: Option<UnsignedIntegerType>,
848) -> FloatType {
849    let octave_size = size.unwrap_or_else(|| tuning_system.octave_size());
850    assert!(octave_size > 0, "octave_size must be greater than zero");
851    let reference_freq = get_frequency_at(
852        TuningSystem::EqualTemperament { octave_size },
853        index,
854        Some(octave_size),
855    );
856    let comparison_freq = get_frequency_at(tuning_system, index, size);
857    1200.0 * (comparison_freq / reference_freq).log2()
858}
859
860fn get_fraction_from_table(tuning_system: TuningSystem, index: usize) -> Fraction {
861    let table = tuning_system
862        .ratio_table()
863        .expect("tuning system does not have a ratio table");
864    let len = table.len();
865    let octaves = (index / len) as UnsignedIntegerType;
866    table[index % len].with_octaves(octaves)
867}
868
869fn get_ratio_at_integer_index(tuning_system: TuningSystem, index: IntegerType) -> FloatType {
870    let table = tuning_system
871        .ratio_table()
872        .expect("tuning system does not have a ratio table");
873    let len = IntegerType::try_from(table.len()).expect("ratio table length exceeds i32 range");
874    let octave = index.div_euclid(len);
875    let degree = index.rem_euclid(len) as usize;
876    table[degree].ratio() * (2.0 as FloatType).powi(octave)
877}
878
879fn index_to_unsigned_integer(index: usize) -> UnsignedIntegerType {
880    UnsignedIntegerType::try_from(index).expect("tone index exceeds u32 range")
881}
882
883fn default_degree_label(octave_size: UnsignedIntegerType, index: UnsignedIntegerType) -> String {
884    if octave_size == OCTAVE_SIZE {
885        TWELVE_TONE_NAMES[(index % OCTAVE_SIZE) as usize].to_string()
886    } else {
887        format!("T{}", index % octave_size)
888    }
889}
890
891fn degree_name_with_octave(degree_label: &str, octave: UnsignedIntegerType) -> String {
892    let adjusted_octave = i64::from(octave) - 1;
893    let generic_degree_label = degree_label
894        .strip_prefix('T')
895        .is_some_and(|rest| !rest.is_empty() && rest.chars().all(|ch| ch.is_ascii_digit()));
896
897    if generic_degree_label {
898        return if adjusted_octave < 0 {
899            format!("{degree_label}ON{}", -adjusted_octave)
900        } else {
901            format!("{degree_label}O{adjusted_octave}")
902        };
903    }
904
905    if adjusted_octave < 0 {
906        format!("{degree_label}N{}", -adjusted_octave)
907    } else {
908        format!("{degree_label}{adjusted_octave}")
909    }
910}
911
912/// Backwards-compatible alias for [`FORTY_THREE_TONE`].
913pub const FORTYTHREE_TONE: [Fraction; 43] = FORTY_THREE_TONE;
914
915/// Five-tone Javanese equal-temperament approximation.
916pub const JAVANESE: [Fraction; 5] = [
917    Fraction::new_with_base(0, 5, 2),
918    Fraction::new_with_base(1, 5, 2),
919    Fraction::new_with_base(2, 5, 2),
920    Fraction::new_with_base(3, 5, 2),
921    Fraction::new_with_base(4, 5, 2),
922];
923
924/// Seven-tone Thai equal-temperament approximation.
925pub const THAI: [Fraction; 7] = [
926    Fraction::new_with_base(0, 7, 2),
927    Fraction::new_with_base(1, 7, 2),
928    Fraction::new_with_base(2, 7, 2),
929    Fraction::new_with_base(3, 7, 2),
930    Fraction::new_with_base(4, 7, 2),
931    Fraction::new_with_base(5, 7, 2),
932    Fraction::new_with_base(6, 7, 2),
933];
934
935/// Degree labels for the seven-tone PtolemyIntenseDiatonic scale.
936pub const INDIAN_SCALE_NAMES: [&str; 7] = ["Sa", "Re", "Ga", "Ma", "Pa", "Dha", "Ni"];
937
938#[cfg(test)]
939mod tests {
940    use super::*;
941
942    #[test]
943    fn every_system_answers_cents_and_a_fraction_for_its_degrees() {
944        for system in ALL_TUNING_SYSTEMS {
945            let unison = system.fraction(0);
946            assert!(unison.numerator == 0 || unison.numerator == unison.denominator);
947            assert_eq!(system.cents(0), 0.0);
948            assert!(system.cents(system.octave_size()).abs() < 1e-9);
949        }
950    }
951
952    /// Cents above the tonic for a degree of a twelve-tone table.
953    fn cents_at_degree(system: TuningSystem, degree: usize) -> FloatType {
954        1200.0 * system.ratio(degree).log2()
955    }
956
957    #[test]
958    fn meantone_fifths_match_their_comma_fractions() {
959        // A 1/n-comma meantone narrows the pure fifth by 1/n of the syntonic
960        // comma (21.506 cents). Checking the arithmetic rather than a quoted
961        // table catches a table pointed at the wrong Scala file.
962        const PURE_FIFTH: FloatType = 701.955;
963        const SYNTONIC_COMMA: FloatType = 21.506;
964        for (system, divisor) in [
965            (TuningSystem::ThirdCommaMeantone, 3.0),
966            (TuningSystem::QuarterCommaMeantone, 4.0),
967            (TuningSystem::SixthCommaMeantone, 6.0),
968        ] {
969            let expected = PURE_FIFTH - SYNTONIC_COMMA / divisor;
970            let actual = cents_at_degree(system, 7);
971            assert!(
972                (actual - expected).abs() < 0.01,
973                "{} fifth: expected {expected:.3}, got {actual:.3}",
974                system.display_name()
975            );
976        }
977    }
978
979    #[test]
980    fn kirnberger_i_keeps_its_pure_fifth_and_third() {
981        // Kirnberger I is the outlier of the well temperaments: it leaves the
982        // C-G fifth pure rather than tempering it.
983        assert!((cents_at_degree(TuningSystem::KirnbergerI, 7) - 701.955).abs() < 0.01);
984        assert!((cents_at_degree(TuningSystem::KirnbergerI, 4) - 386.314).abs() < 0.01);
985    }
986
987    #[test]
988    fn common_equal_temperaments_divide_the_octave_evenly() {
989        for system in COMMON_EQUAL_TEMPERAMENTS {
990            let size = system.octave_size();
991            let step = 1200.0 / FloatType::from(size);
992            for degree in 0..size as usize {
993                let expected = step * degree as FloatType;
994                let actual = cents_at_degree(system, degree);
995                assert!(
996                    (actual - expected).abs() < 0.001,
997                    "{size}-EDO degree {degree}: expected {expected:.3}, got {actual:.3}"
998                );
999            }
1000        }
1001    }
1002
1003    #[test]
1004    fn historical_temperaments_match_their_published_values() {
1005        // Major third and perfect fifth above C, in cents, as given in the
1006        // standard literature. Just values for reference: M3 386.314,
1007        // P5 701.955; equal temperament: 400.000 and 700.000.
1008        let cases = [
1009            (TuningSystem::QuarterCommaMeantone, 386.314, 696.578),
1010            (TuningSystem::WerckmeisterIII, 390.225, 696.090),
1011            (TuningSystem::Rameau, 386.314, 696.578),
1012            (TuningSystem::KirnbergerIII, 386.314, 696.578),
1013            (TuningSystem::Vallotti, 392.180, 698.045),
1014            (TuningSystem::YoungII, 392.180, 698.045),
1015        ];
1016
1017        for (system, major_third, fifth) in cases {
1018            let actual_third = cents_at_degree(system, 4);
1019            let actual_fifth = cents_at_degree(system, 7);
1020            assert!(
1021                (actual_third - major_third).abs() < 0.001,
1022                "{} major third: expected {major_third}, got {actual_third}",
1023                system.display_name()
1024            );
1025            assert!(
1026                (actual_fifth - fifth).abs() < 0.001,
1027                "{} fifth: expected {fifth}, got {actual_fifth}",
1028                system.display_name()
1029            );
1030        }
1031    }
1032
1033    #[test]
1034    fn quarter_comma_meantone_and_kirnberger_have_a_pure_major_third() {
1035        // The defining property of both: C-E is the just 5/4 (386.314 cents).
1036        //
1037        // Not exactly 5/4, though. The Scala archive writes these degrees in
1038        // cents to five decimal places rather than as an exact ratio, so the
1039        // table carries 386.31371 cents. That is 4e-6 cents shy of just - some
1040        // nine orders of magnitude below anything audible - but it is not the
1041        // rational 5/4, and a test asserting exact equality would be asserting
1042        // something the source data does not contain.
1043        for system in [
1044            TuningSystem::QuarterCommaMeantone,
1045            TuningSystem::KirnbergerIII,
1046        ] {
1047            let cents = cents_at_degree(system, 4);
1048            assert!(
1049                (cents - 386.313_714).abs() < 0.001,
1050                "{} major third should be the just 386.314 cents, got {cents}",
1051                system.display_name()
1052            );
1053        }
1054    }
1055
1056    #[test]
1057    fn every_historical_temperament_is_wired_up() {
1058        for system in HISTORICAL_TEMPERAMENTS {
1059            assert_eq!(system.octave_size(), OCTAVE_SIZE, "{system:?}");
1060            assert!(system.ratio_table().is_some(), "{system:?} has no table");
1061            assert_eq!(system.ratio_table().unwrap().len(), 12, "{system:?}");
1062            assert!(!system.description().is_empty(), "{system:?}");
1063            assert_eq!(
1064                TuningSystem::from_str(system.id()).unwrap(),
1065                system,
1066                "{system:?} does not round-trip through its id"
1067            );
1068            assert!(
1069                ALL_TUNING_SYSTEMS.contains(&system),
1070                "{system:?} missing from ALL_TUNING_SYSTEMS"
1071            );
1072        }
1073    }
1074
1075    #[test]
1076    fn equal_temperament_degree_helpers_work_without_tone_objects() {
1077        assert_eq!(
1078            TuningSystem::EqualTemperament { octave_size: 12 }.label(0),
1079            "CN1"
1080        );
1081        assert_eq!(
1082            TuningSystem::EqualTemperament { octave_size: 12 }.octave(0),
1083            0
1084        );
1085        assert!(
1086            (TuningSystem::EqualTemperament { octave_size: 12 }.frequency(0) - CN1).abs() < 1e-12
1087        );
1088
1089        assert_eq!(
1090            TuningSystem::EqualTemperament { octave_size: 12 }.label(69),
1091            "A4"
1092        );
1093        assert_eq!(
1094            TuningSystem::EqualTemperament { octave_size: 12 }.octave(69),
1095            5
1096        );
1097        assert!(
1098            (TuningSystem::EqualTemperament { octave_size: 12 }.frequency(69) - 440.0).abs()
1099                < 0.0001
1100        );
1101    }
1102
1103    #[test]
1104    fn fractional_frequency_helpers_support_pitch_space_values() {
1105        let equal = TuningSystem::EqualTemperament {
1106            octave_size: OCTAVE_SIZE,
1107        };
1108        assert!((equal.frequency_at(69.0) - A4).abs() < 0.0001);
1109        assert!((equal.frequency_at(60.0) - C4).abs() < 0.0001);
1110        assert!((TuningSystem::FiveLimit.frequency_at(64.0) - (C4 * 5.0 / 4.0)).abs() < 0.0001);
1111        assert!(
1112            (TuningSystem::PythagoreanTuning.frequency_at(67.0) - (C4 * 3.0 / 2.0)).abs() < 0.0001
1113        );
1114        assert!(TuningSystem::FiveLimit.cents_at(64.0) < -13.0);
1115    }
1116
1117    #[test]
1118    fn ratio_helpers_cover_octaves() {
1119        let two_one: FloatType = Fraction::new(2, 1).into();
1120        assert_eq!(get_ratio(TuningSystem::CarlosHarmonic, 12, None), two_one);
1121        assert_eq!(get_ratio(TuningSystem::CarlosHarmonic24, 24, None), two_one);
1122        assert_eq!(
1123            get_ratio(
1124                TuningSystem::EqualTemperament {
1125                    octave_size: OCTAVE_SIZE,
1126                },
1127                12,
1128                None,
1129            ),
1130            two_one
1131        );
1132    }
1133
1134    #[test]
1135    fn fraction_helpers_cover_rational_and_exponential_forms() {
1136        let rational = Fraction::from((3, 2));
1137        assert_eq!(rational.numerator(), 3);
1138        assert_eq!(rational.denominator(), 2);
1139        assert_eq!(rational.base(), 0);
1140        assert_eq!(rational.ratio(), 1.5);
1141        assert_eq!(rational.label(), "3/2");
1142        assert_eq!(rational.with_octaves(2), Fraction::new(12, 2));
1143
1144        let exponential = Fraction::from((7, 12, 2));
1145        assert_eq!(exponential.label(), "2^(7/12)");
1146        assert_eq!(
1147            exponential.with_octaves(1),
1148            Fraction::new_with_base(19, 12, 2)
1149        );
1150        assert!((exponential.ratio() - 2.0_f64.powf(7.0 / 12.0)).abs() < 1e-12);
1151    }
1152
1153    #[test]
1154    fn free_tuning_helpers_accept_size_overrides() {
1155        let system = TuningSystem::EqualTemperament { octave_size: 12 };
1156        assert_eq!(equal_temperament_12(12), Fraction::new_with_base(12, 12, 2));
1157        assert_eq!(
1158            equal_temperament_default(3),
1159            Fraction::new_with_base(3, OCTAVE_SIZE, 2)
1160        );
1161        assert_eq!(
1162            get_fraction(system, 6, Some(24)),
1163            Fraction::new_with_base(6, 24, 2)
1164        );
1165        assert_eq!(get_label(system, 24, Some(24)), "T0O0");
1166        assert!((get_frequency(system, 12, Some(24)) - CN1 * 2.0_f64.sqrt()).abs() < 1e-10);
1167        assert_eq!(get_cents(system, 12, Some(24)), 0.0);
1168    }
1169
1170    #[test]
1171    fn current_tuning_system_variants_return_ratios() {
1172        assert_eq!(TuningSystem::WholeTone.ratio(6), 2.0);
1173        assert_eq!(TuningSystem::QuarterTone.ratio(24), 2.0);
1174        assert_eq!(TuningSystem::PythagoreanTuning.ratio(7), 1.5);
1175        assert_eq!(TuningSystem::Indian22.ratio(22), 2.0);
1176    }
1177
1178    #[test]
1179    fn table_ratios_shift_by_real_octaves() {
1180        assert_eq!(TuningSystem::CarlosHarmonic.ratio(19), 3.0);
1181        assert_eq!(TuningSystem::FortyThreeTone.ratio(68), 3.0);
1182        assert_eq!(TuningSystem::PtolemyIntenseDiatonic.ratio(8), 2.25);
1183    }
1184
1185    #[test]
1186    fn non_twelve_tone_systems_keep_system_octaves_and_labels() {
1187        assert_eq!(TuningSystem::WholeTone.label(1), "DN1");
1188        assert_eq!(TuningSystem::WholeTone.octave_size(), 6);
1189        assert!(
1190            (TuningSystem::WholeTone.ratio(1) - (2.0 as FloatType).powf(1.0 / 6.0)).abs() < 1e-12
1191        );
1192
1193        assert_eq!(TuningSystem::QuarterTone.label(13), "T13ON1");
1194        assert_eq!(TuningSystem::QuarterTone.octave_size(), 24);
1195        assert!(
1196            (TuningSystem::QuarterTone.ratio(13) - (2.0 as FloatType).powf(13.0 / 24.0)).abs()
1197                < 1e-12
1198        );
1199
1200        assert_eq!(TuningSystem::Thai.label(7), "T0O0");
1201        assert_eq!(TuningSystem::Thai.octave_size(), 7);
1202        assert_eq!(TuningSystem::Thai.ratio(7), 2.0);
1203
1204        assert_eq!(TuningSystem::PtolemyIntenseDiatonic.label(8), "Re0");
1205        assert_eq!(TuningSystem::PtolemyIntenseDiatonic.octave_size(), 7);
1206        assert_eq!(TuningSystem::PtolemyIntenseDiatonic.ratio(8), 2.25);
1207
1208        assert_eq!(TuningSystem::FortyThreeTone.label(68), "T25O0");
1209        assert_eq!(TuningSystem::FortyThreeTone.octave_size(), 43);
1210        assert_eq!(TuningSystem::FortyThreeTone.ratio(68), 3.0);
1211    }
1212
1213    #[test]
1214    fn tuning_system_display_and_parse_are_canonical() {
1215        let system = TuningSystem::FiveLimit;
1216        assert_eq!(system.id(), "FiveLimit");
1217        assert_eq!(system.to_string(), "FiveLimit");
1218        assert_eq!("FiveLimit".parse::<TuningSystem>().unwrap(), system);
1219
1220        let err = "not-a-system".parse::<TuningSystem>().unwrap_err();
1221        assert_eq!(
1222            err,
1223            Error::TuningSystem("unknown tuning system \"not-a-system\"".to_string())
1224        );
1225    }
1226
1227    #[test]
1228    fn tuning_system_display_names_cover_variants() {
1229        for system in ALL_TUNING_SYSTEMS {
1230            assert!(!system.id().is_empty());
1231            assert!(!system.display_name().is_empty());
1232            assert!(!system.description().is_empty());
1233            assert!(system.octave_size() > 0);
1234            assert_eq!(system.to_string(), system.id());
1235        }
1236    }
1237
1238    #[test]
1239    fn twelve_tone_systems_keep_chromatic_ratios_ascending() {
1240        for system in ALL_TUNING_SYSTEMS
1241            .into_iter()
1242            .filter(|system| system.octave_size() == OCTAVE_SIZE)
1243        {
1244            let mut previous = system.ratio(0);
1245            for degree in 1..=OCTAVE_SIZE {
1246                let ratio = system.ratio(degree as usize);
1247                assert!(
1248                    ratio > previous,
1249                    "{} degree {degree} ratio {ratio} should be higher than {previous}",
1250                    system.id()
1251                );
1252                previous = ratio;
1253            }
1254        }
1255    }
1256
1257    #[test]
1258    fn all_ratio_tables_keep_degrees_strictly_ascending_within_the_octave() {
1259        for system in ALL_TUNING_SYSTEMS {
1260            let octave_size = system.octave_size();
1261            let mut previous = system.ratio(0);
1262            for degree in 1..=octave_size {
1263                let ratio = system.ratio(degree as usize);
1264                assert!(
1265                    ratio > previous,
1266                    "{} degree {degree} ratio {ratio} should be higher than {previous} \
1267                     (a table entry is likely mistranscribed)",
1268                    system.id()
1269                );
1270                previous = ratio;
1271            }
1272        }
1273    }
1274}