Skip to main content

music21_rs/tuningsystem/
tuning.rs

1use crate::defaults::FloatType;
2use crate::error::{Error, Result};
3use crate::pitch::Pitch;
4use crate::tuningsystem::{A4, EqualDivision, Monzo, Temperament, Val};
5
6/// A way of sizing an interval by what it is, not by which keys it spans.
7///
8/// A spelled interval is a [`Monzo`] (see
9/// [`Interval::pythagorean_monzo`](crate::Interval::pythagorean_monzo)), so a
10/// tuning here says how wide each monzo sounds. `C#` and `Db` are then two
11/// notes wherever the tuning keeps them apart, which a keyboard table indexed
12/// by semitone cannot do.
13pub trait Tuning {
14    /// How wide `interval` sounds, in cents.
15    fn cents_of(&self, interval: &Monzo) -> Result<FloatType>;
16}
17
18/// Just intonation: every interval exactly as wide as its ratio.
19///
20/// For a spelled interval, which is octaves and fifths, this is Pythagorean
21/// tuning.
22#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
23pub struct Just;
24
25impl Tuning for Just {
26    fn cents_of(&self, interval: &Monzo) -> Result<FloatType> {
27        Ok(interval.cents())
28    }
29}
30
31impl Tuning for Temperament {
32    fn cents_of(&self, interval: &Monzo) -> Result<FloatType> {
33        self.cents(interval)
34    }
35}
36
37impl Tuning for EqualDivision {
38    /// Each prime takes its nearest number of steps (the patent val).
39    fn cents_of(&self, interval: &Monzo) -> Result<FloatType> {
40        let Some(limit) = interval.limit() else {
41            return Ok(0.0);
42        };
43        let val: Val = self.patent_val(limit)?;
44        Ok(FloatType::from(val.map(interval)) * self.step_cents())
45    }
46}
47
48/// The pitch a tuning is measured from, and the frequency it sounds at.
49#[derive(Clone, Debug, PartialEq)]
50#[must_use]
51pub struct Reference {
52    pitch: Pitch,
53    hertz: FloatType,
54}
55
56impl Reference {
57    /// `pitch` sounding at `hertz`. Errors on a frequency that is not a
58    /// finite number above nought.
59    pub fn new(pitch: Pitch, hertz: FloatType) -> Result<Self> {
60        if !hertz.is_finite() || hertz <= 0.0 {
61            return Err(Error::TuningSystem(format!(
62                "a reference frequency must be a finite number above nought, got {hertz}"
63            )));
64        }
65        Ok(Self { pitch, hertz })
66    }
67
68    /// `A4` at 440 Hz.
69    pub fn a440() -> Self {
70        Self {
71            pitch: Pitch::from_name("A4").expect("A4 is a pitch name"),
72            hertz: A4,
73        }
74    }
75
76    /// The pitch the tuning is measured from.
77    pub fn pitch(&self) -> &Pitch {
78        &self.pitch
79    }
80
81    /// The frequency the reference pitch sounds at, in hertz.
82    #[must_use]
83    pub fn hertz(&self) -> FloatType {
84        self.hertz
85    }
86}
87
88impl Default for Reference {
89    fn default() -> Self {
90        Self::a440()
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::tuningsystem::{C4, MEANTONE};
98
99    fn hz(name: &str, tuning: &impl Tuning) -> FloatType {
100        Pitch::from_name(name)
101            .unwrap()
102            .frequency_hz_tuned(tuning, &Reference::a440())
103            .unwrap()
104    }
105
106    #[test]
107    fn twelve_edo_agrees_with_the_plain_frequency() {
108        let twelve = EqualDivision::octave(12).unwrap();
109        for name in ["C4", "C#4", "Db4", "B#3", "F##5", "Ebb2", "A4"] {
110            let plain = Pitch::from_name(name).unwrap().frequency_hz();
111            assert!((hz(name, &twelve) - plain).abs() < 1e-9, "{name}");
112        }
113        assert!((hz("C4", &twelve) - C4).abs() < 1e-9);
114    }
115
116    #[test]
117    fn a_tuning_keeps_enharmonics_apart_where_it_should() {
118        let nineteen = EqualDivision::octave(19).unwrap();
119        assert!(hz("C#4", &nineteen) < hz("Db4", &nineteen));
120        // A Pythagorean sharp is higher than the flat above it.
121        assert!(hz("C#4", &Just) > hz("Db4", &Just));
122        let meantone = MEANTONE.temperament().unwrap();
123        assert!(hz("C#4", &meantone) < hz("Db4", &meantone));
124    }
125
126    #[test]
127    fn just_is_pythagorean_from_the_reference() {
128        let e5 = hz("E5", &Just);
129        assert!((e5 - 660.0).abs() < 1e-9);
130        let c4 = hz("C4", &Just);
131        assert!((c4 - 440.0 * 16.0 / 27.0).abs() < 1e-9);
132    }
133
134    #[test]
135    fn a_microtone_is_added_on_top() {
136        let mut pitch = Pitch::from_name("A4").unwrap();
137        pitch.set_microtone_cents(-14.0).unwrap();
138        let tuned = pitch.frequency_hz_tuned(&Just, &Reference::a440()).unwrap();
139        assert!((tuned - 440.0 * (2.0 as FloatType).powf(-14.0 / 1200.0)).abs() < 1e-9);
140    }
141
142    #[test]
143    fn a_reference_needs_a_real_frequency() {
144        let a4 = Pitch::from_name("A4").unwrap();
145        assert!(Reference::new(a4.clone(), 0.0).is_err());
146        assert!(Reference::new(a4, FloatType::NAN).is_err());
147    }
148}