Skip to main content

music21_rs/tuningsystem/
mod.rs

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