Skip to main content

music21_rs/tuningsystem/
mod.rs

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