music21_rs/tuningsystem/
tuning.rs1use crate::defaults::FloatType;
2use crate::error::{Error, Result};
3use crate::pitch::Pitch;
4use crate::tuningsystem::{A4, EqualDivision, Monzo, Temperament, Val};
5
6pub trait Tuning {
14 fn cents_of(&self, interval: &Monzo) -> Result<FloatType>;
16}
17
18#[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 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#[derive(Clone, Debug, PartialEq)]
50#[must_use]
51pub struct Reference {
52 pitch: Pitch,
53 hertz: FloatType,
54}
55
56impl Reference {
57 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 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 pub fn pitch(&self) -> &Pitch {
78 &self.pitch
79 }
80
81 #[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 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}