Skip to main content

music21_rs/tuningsystem/
mod.rs

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