1pub mod adaptive;
4#[cfg(test)]
9mod duplicates;
10pub mod equal;
12mod generated;
13pub mod monzo;
15pub mod mos;
17pub mod scala;
19#[cfg(feature = "scala-archive")]
20pub mod scala_bundled;
21pub mod temperament;
23mod temperaments_generated;
24pub mod tuning;
26
27pub use equal::{EqualDivision, TRITAVE_CENTS};
28
29pub use generated::*;
30pub use monzo::{Monzo, PRIMES, Val};
31pub use mos::{Mos, MosScale, OCTAVE_CENTS, moment_of_symmetry_sizes};
32use std::num::NonZeroU32;
33pub use temperament::Temperament;
34pub use temperaments_generated::*;
35pub use tuning::{Just, Reference, Tuning};
36
37use crate::defaults::{FloatType, FractionType, IntegerType, UnsignedIntegerType};
38use crate::error::{Error, Result};
39use crate::pitch::Pitch;
40use crate::tuningsystem::adaptive::AdaptiveTuningSystem;
41
42use std::fmt::{Display, Formatter};
43use std::str::FromStr;
44
45pub const OCTAVE_SIZE: UnsignedIntegerType = 12;
47
48pub const C4: FloatType = 261.625_565_300_598_6;
50pub const C0: FloatType = C4 / 16.0;
52pub const CN1: FloatType = C4 / 32.0;
54
55pub const A4: FloatType = 440.0;
57pub const A0: FloatType = A4 / 16.0;
59pub const AN1: FloatType = A4 / 32.0;
61
62pub const COMMON_TWELVE_TONE_TUNING_SYSTEMS: [TuningSystem; 4] = [
64 TuningSystem::EQUAL_TEMPERAMENT,
65 TuningSystem::CarlosHarmonic,
66 TuningSystem::PythagoreanTuning,
67 TuningSystem::FiveLimit,
68];
69
70pub const COMMON_EQUAL_TEMPERAMENTS: [TuningSystem; 8] = [
77 TuningSystem::edo(12),
78 TuningSystem::edo(19),
79 TuningSystem::edo(22),
80 TuningSystem::edo(24),
81 TuningSystem::edo(31),
82 TuningSystem::edo(41),
83 TuningSystem::edo(53),
84 TuningSystem::edo(72),
85];
86
87pub const HISTORICAL_TEMPERAMENTS: [TuningSystem; 14] = [
93 TuningSystem::QuarterCommaMeantone,
94 TuningSystem::WerckmeisterIII,
95 TuningSystem::Rameau,
96 TuningSystem::KirnbergerIII,
97 TuningSystem::Vallotti,
98 TuningSystem::YoungII,
99 TuningSystem::ThirdCommaMeantone,
100 TuningSystem::SixthCommaMeantone,
101 TuningSystem::WerckmeisterIV,
102 TuningSystem::WerckmeisterV,
103 TuningSystem::KirnbergerI,
104 TuningSystem::NeidhardtI,
105 TuningSystem::Silbermann,
106 TuningSystem::LehmanBach,
107];
108
109#[derive(Clone, Copy, Debug, PartialEq)]
111#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
112#[must_use]
113pub enum AnyTuningSystem {
114 Fixed(TuningSystem),
116 Adaptive(AdaptiveTuningSystem),
118}
119
120impl AnyTuningSystem {
121 pub fn frequency_at(self, context: FloatType, index: FloatType) -> FloatType {
126 match self {
127 Self::Fixed(tuning_system) => {
128 let _ = context;
129 tuning_system.frequency_at(index)
130 }
131 Self::Adaptive(adaptive_tuning_system) => {
132 adaptive_tuning_system.frequency_at(context, index)
133 }
134 }
135 }
136
137 pub fn cents_at(self, context: FloatType, index: FloatType) -> FloatType {
142 match self {
143 Self::Fixed(tuning_system) => {
144 let _ = context;
145 tuning_system.cents_at(index)
146 }
147 Self::Adaptive(adaptive_tuning_system) => {
148 adaptive_tuning_system.cents_at(context, index)
149 }
150 }
151 }
152
153 pub fn is_adaptive(self) -> bool {
155 matches!(self, Self::Adaptive(_))
156 }
157}
158
159impl From<TuningSystem> for AnyTuningSystem {
160 fn from(tuning_system: TuningSystem) -> Self {
161 Self::Fixed(tuning_system)
162 }
163}
164
165impl From<AdaptiveTuningSystem> for AnyTuningSystem {
166 fn from(adaptive_tuning_system: AdaptiveTuningSystem) -> Self {
167 Self::Adaptive(adaptive_tuning_system)
168 }
169}
170
171pub const ALL_TUNING_SYSTEMS: [TuningSystem; 28] = [
173 TuningSystem::EQUAL_TEMPERAMENT,
174 TuningSystem::WHOLE_TONE,
175 TuningSystem::QUARTER_TONE,
176 TuningSystem::CarlosHarmonic,
177 TuningSystem::CarlosHarmonic24,
178 TuningSystem::PythagoreanTuning,
179 TuningSystem::FiveLimit,
180 TuningSystem::ElevenLimit,
181 TuningSystem::FortyThreeTone,
182 TuningSystem::Javanese,
183 TuningSystem::Thai,
184 TuningSystem::PtolemyIntenseDiatonic,
185 TuningSystem::IndianAlt,
186 TuningSystem::Indian22,
187 TuningSystem::QuarterCommaMeantone,
188 TuningSystem::WerckmeisterIII,
189 TuningSystem::Rameau,
190 TuningSystem::KirnbergerIII,
191 TuningSystem::Vallotti,
192 TuningSystem::YoungII,
193 TuningSystem::ThirdCommaMeantone,
194 TuningSystem::SixthCommaMeantone,
195 TuningSystem::WerckmeisterIV,
196 TuningSystem::WerckmeisterV,
197 TuningSystem::KirnbergerI,
198 TuningSystem::NeidhardtI,
199 TuningSystem::Silbermann,
200 TuningSystem::LehmanBach,
201];
202
203#[derive(Clone, Copy, Debug, Eq, PartialEq)]
212#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
213#[must_use]
214pub enum Ratio {
215 Rational {
217 numerator: UnsignedIntegerType,
219 denominator: UnsignedIntegerType,
221 },
222 Root {
224 base: UnsignedIntegerType,
226 numerator: UnsignedIntegerType,
228 denominator: UnsignedIntegerType,
230 },
231}
232
233impl Ratio {
234 pub const fn new(numerator: UnsignedIntegerType, denominator: UnsignedIntegerType) -> Self {
236 Self::Rational {
237 numerator,
238 denominator,
239 }
240 }
241
242 pub const fn root(
244 base: UnsignedIntegerType,
245 numerator: UnsignedIntegerType,
246 denominator: UnsignedIntegerType,
247 ) -> Self {
248 Self::Root {
249 base,
250 numerator,
251 denominator,
252 }
253 }
254
255 #[must_use]
257 pub fn value(self) -> FloatType {
258 match self {
259 Self::Rational {
260 numerator,
261 denominator,
262 } => FloatType::from(numerator) / FloatType::from(denominator),
263 Self::Root {
264 base,
265 numerator,
266 denominator,
267 } => FloatType::from(base)
268 .powf(FloatType::from(numerator) / FloatType::from(denominator)),
269 }
270 }
271
272 #[must_use]
274 pub fn cents(self) -> FloatType {
275 OCTAVE_CENTS * self.value().log2()
276 }
277
278 #[must_use]
281 pub fn to_fraction(self) -> Option<FractionType> {
282 match self {
283 Self::Rational {
284 numerator,
285 denominator,
286 } => Some(FractionType::new(
287 IntegerType::try_from(numerator).ok()?,
288 IntegerType::try_from(denominator).ok()?,
289 )),
290 Self::Root { .. } => None,
291 }
292 }
293
294 pub fn monzo(self) -> Result<Monzo> {
297 let fraction = self.to_fraction().ok_or_else(|| {
298 Error::TuningSystem(format!(
299 "{self} is not a ratio of two i32s, so has no monzo"
300 ))
301 })?;
302 Monzo::from_fraction(fraction)
303 }
304
305 pub fn with_octaves(self, octaves: UnsignedIntegerType) -> Self {
308 match self {
309 Self::Rational {
310 numerator,
311 denominator,
312 } => {
313 let multiplier = (2 as UnsignedIntegerType)
314 .checked_pow(octaves)
315 .expect("octave multiplier exceeds u32 range");
316 Self::new(
317 numerator
318 .checked_mul(multiplier)
319 .expect("ratio numerator exceeds u32 range"),
320 denominator,
321 )
322 }
323 Self::Root {
324 base,
325 numerator,
326 denominator,
327 } => Self::root(
328 base,
329 numerator
330 .checked_add(
331 denominator
332 .checked_mul(octaves)
333 .expect("root octave offset exceeds u32 range"),
334 )
335 .expect("root numerator exceeds u32 range"),
336 denominator,
337 ),
338 }
339 }
340}
341
342impl From<Ratio> for FloatType {
343 fn from(ratio: Ratio) -> Self {
344 ratio.value()
345 }
346}
347
348impl Display for Ratio {
349 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
350 match *self {
351 Self::Rational {
352 numerator,
353 denominator: 1,
354 } => write!(f, "{numerator}"),
355 Self::Rational {
356 numerator,
357 denominator,
358 } => write!(f, "{numerator}/{denominator}"),
359 Self::Root { numerator: 0, .. } => write!(f, "1"),
360 Self::Root {
361 base,
362 numerator,
363 denominator,
364 } => write!(f, "{base}^({numerator}/{denominator})"),
365 }
366 }
367}
368
369#[derive(Clone, Copy, Debug, PartialEq)]
370#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
371#[must_use]
378#[non_exhaustive]
379pub enum TuningSystem {
380 Equal(EqualDivision),
384
385 CarlosHarmonic,
391 CarlosHarmonic24,
393 PythagoreanTuning,
395
396 FiveLimit,
398 ElevenLimit,
400
401 FortyThreeTone,
403
404 Javanese,
407 Thai,
409 PtolemyIntenseDiatonic,
414 IndianAlt,
416 Indian22,
418
419 QuarterCommaMeantone,
422 WerckmeisterIII,
424 Rameau,
426 KirnbergerIII,
428 Vallotti,
430 YoungII,
432 ThirdCommaMeantone,
434 SixthCommaMeantone,
436 WerckmeisterIV,
438 WerckmeisterV,
440 KirnbergerI,
442 NeidhardtI,
444 Silbermann,
446 LehmanBach,
448}
449
450impl TuningSystem {
451 pub fn id(self) -> String {
453 let id = match self {
454 Self::Equal(division) => return division.to_string(),
455 Self::CarlosHarmonic => "CarlosHarmonic",
456 Self::CarlosHarmonic24 => "CarlosHarmonic24",
457 Self::PythagoreanTuning => "PythagoreanTuning",
458 Self::FiveLimit => "FiveLimit",
459 Self::ElevenLimit => "ElevenLimit",
460 Self::FortyThreeTone => "FortyThreeTone",
461 Self::Javanese => "Javanese",
462 Self::Thai => "Thai",
463 Self::PtolemyIntenseDiatonic => "PtolemyIntenseDiatonic",
464 Self::IndianAlt => "IndianAlt",
465 Self::Indian22 => "Indian22",
466 Self::QuarterCommaMeantone => "QuarterCommaMeantone",
467 Self::WerckmeisterIII => "WerckmeisterIII",
468 Self::Rameau => "Rameau",
469 Self::KirnbergerIII => "KirnbergerIII",
470 Self::Vallotti => "Vallotti",
471 Self::YoungII => "YoungII",
472 Self::ThirdCommaMeantone => "ThirdCommaMeantone",
473 Self::SixthCommaMeantone => "SixthCommaMeantone",
474 Self::WerckmeisterIV => "WerckmeisterIV",
475 Self::WerckmeisterV => "WerckmeisterV",
476 Self::KirnbergerI => "KirnbergerI",
477 Self::NeidhardtI => "NeidhardtI",
478 Self::Silbermann => "Silbermann",
479 Self::LehmanBach => "LehmanBach",
480 };
481 id.to_string()
482 }
483
484 pub fn display_name(self) -> String {
486 let name = match self {
487 Self::Equal(division) => return equal_display_name(division),
488 Self::CarlosHarmonic => "Carlos Harmonic",
489 Self::CarlosHarmonic24 => "Carlos Harmonic 24",
490 Self::PythagoreanTuning => "Pythagorean",
491 Self::FiveLimit => "Five-limit",
492 Self::ElevenLimit => "Partch 11-limit diamond",
493 Self::FortyThreeTone => "Partch 43-tone",
494 Self::Javanese => "Javanese",
495 Self::Thai => "Thai",
496 Self::PtolemyIntenseDiatonic => "Ptolemy intense diatonic",
497 Self::IndianAlt => "Indian Sa-grama",
498 Self::Indian22 => "Indian shruti",
499 Self::QuarterCommaMeantone => "Quarter-comma meantone",
500 Self::WerckmeisterIII => "Werckmeister III",
501 Self::Rameau => "Rameau",
502 Self::KirnbergerIII => "Kirnberger III",
503 Self::Vallotti => "Vallotti",
504 Self::YoungII => "Young II",
505 Self::ThirdCommaMeantone => "Third-comma meantone",
506 Self::SixthCommaMeantone => "Sixth-comma meantone",
507 Self::WerckmeisterIV => "Werckmeister IV",
508 Self::WerckmeisterV => "Werckmeister V",
509 Self::KirnbergerI => "Kirnberger I",
510 Self::NeidhardtI => "Neidhardt I",
511 Self::Silbermann => "Silbermann",
512 Self::LehmanBach => "Lehman-Bach",
513 };
514 name.to_string()
515 }
516
517 pub fn description(self) -> String {
519 let description = match self {
520 Self::Equal(division) => return equal_description(division),
521 Self::CarlosHarmonic => "A twelve-tone harmonic-series scale, reaching the 19-limit.",
522 Self::CarlosHarmonic24 => "A twenty-four-tone harmonic-series scale.",
523 Self::PythagoreanTuning => "A twelve-tone tuning table built from pure fifths.",
524 Self::FiveLimit => "A twelve-tone table using five-limit just ratios.",
525 Self::ElevenLimit => "Harry Partch's twenty-nine-tone 11-limit tonality diamond.",
526 Self::FortyThreeTone => "Harry Partch's forty-three-tone pure scale.",
527 Self::Javanese => "A five-tone Javanese equal-temperament approximation.",
528 Self::Thai => "A seven-tone Thai equal-temperament approximation.",
529 Self::PtolemyIntenseDiatonic => {
530 "Ptolemy's intense diatonic, also Zarlino's just major scale."
531 }
532 Self::IndianAlt => "The Indian Sa-grama mode, the inverse of Didymus' diatonic.",
533 Self::Indian22 => "The twenty-two-shruti Indian scale.",
534 Self::QuarterCommaMeantone => {
535 "A twelve-tone quarter-comma meantone temperament (Aaron, 1523)."
536 }
537 Self::WerckmeisterIII => "A twelve-tone Werckmeister III well temperament (1681).",
538 Self::Rameau => "A twelve-tone Rameau modified meantone temperament (1725).",
539 Self::KirnbergerIII => "A twelve-tone Kirnberger III well temperament (1744).",
540 Self::Vallotti => "A twelve-tone Vallotti well temperament (c. 1754).",
541 Self::YoungII => "A twelve-tone Thomas Young well temperament no. 2 (1799).",
542 Self::ThirdCommaMeantone => {
543 "A twelve-tone third-comma meantone temperament (Salinas, 1577)."
544 }
545 Self::SixthCommaMeantone => {
546 "A twelve-tone sixth-comma meantone temperament (Salinas, 1577)."
547 }
548 Self::WerckmeisterIV => "A twelve-tone Werckmeister IV well temperament (1681).",
549 Self::WerckmeisterV => "A twelve-tone Werckmeister V well temperament (1681).",
550 Self::KirnbergerI => "A twelve-tone Kirnberger I well temperament (1766).",
551 Self::NeidhardtI => "A twelve-tone Neidhardt I well temperament (1724).",
552 Self::Silbermann => "A twelve-tone Gottfried Silbermann temperament no. 1 (c. 1730).",
553 Self::LehmanBach => "A twelve-tone Lehman-Bach temperament (2005).",
554 };
555 description.to_string()
556 }
557
558 pub fn ratio(self, index: usize) -> FloatType {
560 self.ratio_at(index as FloatType)
561 }
562
563 pub fn ratio_at(self, index: FloatType) -> FloatType {
567 assert!(index.is_finite(), "degree index must be finite");
568 let steps = FloatType::from(self.degrees_per_period());
569 let period = self.period_ratio();
570 let Some(table) = self.ratio_table() else {
571 return period.powf(index / steps);
572 };
573 let whole = index.floor() as IntegerType;
574 let len = IntegerType::try_from(table.len()).expect("ratio table length exceeds i32 range");
575 let fraction = index - FloatType::from(whole);
576 table[whole.rem_euclid(len) as usize].value()
577 * period.powi(whole.div_euclid(len))
578 * period.powf(fraction / steps)
579 }
580
581 pub fn exact_ratio(self, index: usize) -> Option<Ratio> {
588 let index = UnsignedIntegerType::try_from(index).expect("tone index exceeds u32 range");
589 match self {
590 Self::Equal(division) => match division.period_ratio() {
591 Some((base, 1)) => Some(Ratio::root(
592 UnsignedIntegerType::try_from(base).ok()?,
593 index,
594 division.divisions(),
595 )),
596 _ => None,
597 },
598 _ => {
599 let table = self.ratio_table()?;
600 let len = table.len() as UnsignedIntegerType;
601 Some(table[(index % len) as usize].with_octaves(index / len))
602 }
603 }
604 }
605
606 pub fn label(self, index: UnsignedIntegerType) -> String {
615 let steps = self.degrees_per_period();
616 if matches!(self, Self::PtolemyIntenseDiatonic | Self::IndianAlt) && steps == 7 {
617 let octave = IntegerType::try_from(index / steps).expect("an octave fits an i32") - 1;
618 return format!("{}{octave}", INDIAN_SCALE_NAMES[(index % steps) as usize]);
619 }
620 let index = IntegerType::try_from(index).expect("a degree index fits an i32");
621 if self.is_keyboard() {
622 return key_label(index);
623 }
624 let pitch = self
625 .pitch_at(index)
626 .expect("every degree of a tuning system is a pitch");
627 if pitch.is_twelve_tone() {
628 return key_label(pitch.pitch_space().round() as IntegerType);
629 }
630 let microtone = pitch.microtone().map(ToString::to_string);
631 format!(
632 "{}{}",
633 pitch.unicode_name_with_octave(),
634 microtone.unwrap_or_default()
635 )
636 }
637
638 pub fn period_of(self, index: UnsignedIntegerType) -> UnsignedIntegerType {
640 index / self.degrees_per_period()
641 }
642
643 pub fn frequency(self, index: UnsignedIntegerType) -> FloatType {
645 self.frequency_at(FloatType::from(index))
646 }
647
648 pub fn frequency_at(self, index: FloatType) -> FloatType {
650 CN1 * self.ratio_at(index)
651 }
652
653 pub fn nearest_degree(self, hertz: FloatType) -> Result<IntegerType> {
666 if !hertz.is_finite() || hertz <= 0.0 {
667 return Err(Error::TuningSystem(format!(
668 "a frequency must be a finite number above nought, got {hertz}"
669 )));
670 }
671 Ok(self.nearest_degree_to_cents(OCTAVE_CENTS * (hertz / CN1).log2()))
672 }
673
674 fn nearest_degree_to_cents(self, cents: FloatType) -> IntegerType {
678 let steps = IntegerType::try_from(self.degrees_per_period())
679 .expect("a period's degree count fits an i32");
680 let period = (cents / self.period_cents()).floor() as IntegerType;
681 let distance = |degree: IntegerType| {
682 (OCTAVE_CENTS * self.ratio_at(degree.into()).log2() - cents).abs()
683 };
684 ((period - 1) * steps..=(period + 2) * steps)
685 .min_by(|&a, &b| distance(a).total_cmp(&distance(b)))
686 .expect("the search range is never empty")
687 }
688
689 pub fn pitch_at(self, index: IntegerType) -> Result<Pitch> {
702 Pitch::from_pitch_space(OCTAVE_CENTS * self.ratio_at(index.into()).log2() / 100.0)
703 }
704
705 pub fn cents(self, index: UnsignedIntegerType) -> FloatType {
709 self.cents_at(FloatType::from(index))
710 }
711
712 pub fn cents_at(self, index: FloatType) -> FloatType {
714 let equal = self
715 .period_ratio()
716 .powf(index / FloatType::from(self.degrees_per_period()));
717 1200.0 * (self.ratio_at(index) / equal).log2()
718 }
719
720 pub fn degrees_per_period(self) -> UnsignedIntegerType {
723 match self {
724 Self::Equal(division) => division.divisions(),
725 _ => self
726 .ratio_table()
727 .map_or(OCTAVE_SIZE, |table| table.len() as UnsignedIntegerType),
728 }
729 }
730
731 pub fn period_cents(self) -> FloatType {
734 match self {
735 Self::Equal(division) => division.period_cents(),
736 _ => OCTAVE_CENTS,
737 }
738 }
739
740 pub fn repeats_at_the_octave(self) -> bool {
742 match self {
743 Self::Equal(division) => division.repeats_at_the_octave(),
744 _ => true,
745 }
746 }
747
748 pub fn is_keyboard(self) -> bool {
751 self.degrees_per_period() == 12 && self.repeats_at_the_octave()
752 }
753
754 fn period_ratio(self) -> FloatType {
755 (2.0 as FloatType).powf(self.period_cents() / OCTAVE_CENTS)
756 }
757
758 fn ratio_table(self) -> Option<&'static [Ratio]> {
759 match self {
760 Self::CarlosHarmonic => Some(&CARLOS_HARMONIC),
761 Self::CarlosHarmonic24 => Some(&CARLOS_HARMONIC_24),
762 Self::PythagoreanTuning => Some(&PYTHAGOREAN_TUNING),
763 Self::FiveLimit => Some(&FIVE_LIMIT),
764 Self::ElevenLimit => Some(&ELEVEN_LIMIT),
765 Self::FortyThreeTone => Some(&FORTY_THREE_TONE),
766 Self::Javanese => Some(&JAVANESE),
767 Self::Thai => Some(&THAI),
768 Self::PtolemyIntenseDiatonic => Some(&PTOLEMY_INTENSE_DIATONIC),
769 Self::IndianAlt => Some(&INDIA_SCALE_ALT),
770 Self::Indian22 => Some(&INDIAN_SCALE_22),
771 Self::QuarterCommaMeantone => Some(&QUARTER_COMMA_MEANTONE),
772 Self::WerckmeisterIII => Some(&WERCKMEISTER_III),
773 Self::Rameau => Some(&RAMEAU),
774 Self::KirnbergerIII => Some(&KIRNBERGER_III),
775 Self::Vallotti => Some(&VALLOTTI),
776 Self::YoungII => Some(&YOUNG_II),
777 Self::ThirdCommaMeantone => Some(&THIRD_COMMA_MEANTONE),
778 Self::SixthCommaMeantone => Some(&SIXTH_COMMA_MEANTONE),
779 Self::WerckmeisterIV => Some(&WERCKMEISTER_IV),
780 Self::WerckmeisterV => Some(&WERCKMEISTER_V),
781 Self::KirnbergerI => Some(&KIRNBERGER_I),
782 Self::NeidhardtI => Some(&NEIDHARDT_I),
783 Self::Silbermann => Some(&SILBERMANN),
784 Self::LehmanBach => Some(&LEHMAN_BACH),
785 Self::Equal(_) => None,
786 }
787 }
788
789 pub const EQUAL_TEMPERAMENT: Self = Self::edo(12);
791
792 pub const WHOLE_TONE: Self = Self::edo(6);
794
795 pub const QUARTER_TONE: Self = Self::edo(24);
797
798 pub const fn edo(divisions: UnsignedIntegerType) -> Self {
804 Self::Equal(EqualDivision::edo(nonzero(divisions)))
805 }
806}
807
808const fn nonzero(divisions: UnsignedIntegerType) -> NonZeroU32 {
810 match NonZeroU32::new(divisions) {
811 Some(divisions) => divisions,
812 None => panic!("a period cannot be divided into no steps"),
813 }
814}
815
816fn equal_display_name(division: EqualDivision) -> String {
817 match (division.period_ratio(), division.divisions()) {
818 (Some((2, 1)), 12) => "Equal temperament".to_string(),
819 (Some((2, 1)), 6) => "Whole tone".to_string(),
820 (Some((2, 1)), 24) => "Quarter tone".to_string(),
821 (Some((2, 1)), divisions) => format!("{divisions}-tone equal temperament"),
822 _ => format!("{division}"),
823 }
824}
825
826fn equal_description(division: EqualDivision) -> String {
827 let divisions = division.divisions();
828 match division.period_ratio() {
829 Some((2, 1)) => match divisions {
830 12 => "Twelve equal divisions of the octave.".to_string(),
831 6 => "Six equal whole-tone steps per octave.".to_string(),
832 24 => "Twenty-four equal quarter-tone steps per octave.".to_string(),
833 _ => format!("{divisions} equal divisions of the octave."),
834 },
835 Some((numerator, denominator)) => format!(
836 "{divisions} equal divisions of {numerator}/{denominator}, repeating there rather than at the octave."
837 ),
838 None => format!(
839 "{divisions} equal divisions of {:.3} cents, repeating there rather than at the octave.",
840 division.period_cents()
841 ),
842 }
843}
844
845impl Display for TuningSystem {
846 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
847 f.write_str(&self.id())
848 }
849}
850
851impl FromStr for TuningSystem {
852 type Err = Error;
853
854 fn from_str(s: &str) -> Result<Self, Self::Err> {
858 match s {
859 "EqualTemperament" => Ok(Self::EQUAL_TEMPERAMENT),
860 "WholeTone" => Ok(Self::WHOLE_TONE),
861 "QuarterTone" => Ok(Self::QUARTER_TONE),
862 "CarlosHarmonic" => Ok(Self::CarlosHarmonic),
863 "CarlosHarmonic24" => Ok(Self::CarlosHarmonic24),
864 "PythagoreanTuning" => Ok(Self::PythagoreanTuning),
865 "FiveLimit" => Ok(Self::FiveLimit),
866 "ElevenLimit" => Ok(Self::ElevenLimit),
867 "FortyThreeTone" => Ok(Self::FortyThreeTone),
868 "Javanese" => Ok(Self::Javanese),
869 "Thai" => Ok(Self::Thai),
870 "PtolemyIntenseDiatonic" => Ok(Self::PtolemyIntenseDiatonic),
871 "IndianAlt" => Ok(Self::IndianAlt),
872 "Indian22" => Ok(Self::Indian22),
873 "QuarterCommaMeantone" => Ok(Self::QuarterCommaMeantone),
874 "WerckmeisterIII" => Ok(Self::WerckmeisterIII),
875 "Rameau" => Ok(Self::Rameau),
876 "KirnbergerIII" => Ok(Self::KirnbergerIII),
877 "Vallotti" => Ok(Self::Vallotti),
878 "YoungII" => Ok(Self::YoungII),
879 "ThirdCommaMeantone" => Ok(Self::ThirdCommaMeantone),
880 "SixthCommaMeantone" => Ok(Self::SixthCommaMeantone),
881 "WerckmeisterIV" => Ok(Self::WerckmeisterIV),
882 "WerckmeisterV" => Ok(Self::WerckmeisterV),
883 "KirnbergerI" => Ok(Self::KirnbergerI),
884 "NeidhardtI" => Ok(Self::NeidhardtI),
885 "Silbermann" => Ok(Self::Silbermann),
886 "LehmanBach" => Ok(Self::LehmanBach),
887 _ if s.contains("ed") => s.parse().map(Self::Equal),
888 _ => Err(Error::TuningSystem(format!("unknown tuning system {s:?}"))),
889 }
890 }
891}
892
893pub const fn equal_temperament(
895 tone: UnsignedIntegerType,
896 octave_size: UnsignedIntegerType,
897) -> Ratio {
898 Ratio::root(2, tone, octave_size)
899}
900
901fn key_label(pitch_space: IntegerType) -> String {
904 let pitch =
905 Pitch::from_pitch_space(pitch_space.into()).expect("a whole pitch-space value is a pitch");
906 let octave = pitch_space.div_euclid(12) - 1;
907 let alter = pitch.alter();
908 if alter == 0.0 {
909 return format!("{}{octave}", pitch.unicode_name());
910 }
911 let (sharp, flat) = if alter < 0.0 {
912 (pitch.get_lower_enharmonic(), Ok(pitch))
913 } else {
914 (Ok(pitch.clone()), pitch.get_higher_enharmonic())
915 };
916 let (sharp, flat) = (
917 sharp.expect("a black key has a sharp name"),
918 flat.expect("a black key has a flat name"),
919 );
920 format!("{}/{}{octave}", sharp.unicode_name(), flat.unicode_name())
921}
922
923pub const JAVANESE: [Ratio; 5] = [
925 Ratio::root(2, 0, 5),
926 Ratio::root(2, 1, 5),
927 Ratio::root(2, 2, 5),
928 Ratio::root(2, 3, 5),
929 Ratio::root(2, 4, 5),
930];
931
932pub const THAI: [Ratio; 7] = [
934 Ratio::root(2, 0, 7),
935 Ratio::root(2, 1, 7),
936 Ratio::root(2, 2, 7),
937 Ratio::root(2, 3, 7),
938 Ratio::root(2, 4, 7),
939 Ratio::root(2, 5, 7),
940 Ratio::root(2, 6, 7),
941];
942
943pub const INDIAN_SCALE_NAMES: [&str; 7] = ["Sa", "Re", "Ga", "Ma", "Pa", "Dha", "Ni"];
945
946#[cfg(test)]
947mod tests {
948 use super::*;
949
950 #[test]
951 fn every_system_answers_cents_and_an_exact_ratio_for_its_degrees() {
952 for system in ALL_TUNING_SYSTEMS {
953 let unison = system.exact_ratio(0).expect("every listed system is exact");
954 assert_eq!(unison.value(), 1.0);
955 assert_eq!(system.cents(0), 0.0);
956 assert!(system.cents(system.degrees_per_period()).abs() < 1e-9);
957 }
958 }
959
960 fn cents_at_degree(system: TuningSystem, degree: usize) -> FloatType {
962 1200.0 * system.ratio(degree).log2()
963 }
964
965 #[test]
966 fn meantone_fifths_match_their_comma_fractions() {
967 const PURE_FIFTH: FloatType = 701.955;
971 const SYNTONIC_COMMA: FloatType = 21.506;
972 for (system, divisor) in [
973 (TuningSystem::ThirdCommaMeantone, 3.0),
974 (TuningSystem::QuarterCommaMeantone, 4.0),
975 (TuningSystem::SixthCommaMeantone, 6.0),
976 ] {
977 let expected = PURE_FIFTH - SYNTONIC_COMMA / divisor;
978 let actual = cents_at_degree(system, 7);
979 assert!(
980 (actual - expected).abs() < 0.01,
981 "{} fifth: expected {expected:.3}, got {actual:.3}",
982 system.display_name()
983 );
984 }
985 }
986
987 #[test]
988 fn kirnberger_i_keeps_its_pure_fifth_and_third() {
989 assert!((cents_at_degree(TuningSystem::KirnbergerI, 7) - 701.955).abs() < 0.01);
992 assert!((cents_at_degree(TuningSystem::KirnbergerI, 4) - 386.314).abs() < 0.01);
993 }
994
995 #[test]
996 fn common_equal_temperaments_divide_the_octave_evenly() {
997 for system in COMMON_EQUAL_TEMPERAMENTS {
998 let size = system.degrees_per_period();
999 let step = 1200.0 / FloatType::from(size);
1000 for degree in 0..size as usize {
1001 let expected = step * degree as FloatType;
1002 let actual = cents_at_degree(system, degree);
1003 assert!(
1004 (actual - expected).abs() < 0.001,
1005 "{size}-EDO degree {degree}: expected {expected:.3}, got {actual:.3}"
1006 );
1007 }
1008 }
1009 }
1010
1011 #[test]
1012 fn historical_temperaments_match_their_published_values() {
1013 let cases = [
1017 (TuningSystem::QuarterCommaMeantone, 386.314, 696.578),
1018 (TuningSystem::WerckmeisterIII, 390.225, 696.090),
1019 (TuningSystem::Rameau, 386.314, 696.578),
1020 (TuningSystem::KirnbergerIII, 386.314, 696.578),
1021 (TuningSystem::Vallotti, 392.180, 698.045),
1022 (TuningSystem::YoungII, 392.180, 698.045),
1023 ];
1024
1025 for (system, major_third, fifth) in cases {
1026 let actual_third = cents_at_degree(system, 4);
1027 let actual_fifth = cents_at_degree(system, 7);
1028 assert!(
1029 (actual_third - major_third).abs() < 0.001,
1030 "{} major third: expected {major_third}, got {actual_third}",
1031 system.display_name()
1032 );
1033 assert!(
1034 (actual_fifth - fifth).abs() < 0.001,
1035 "{} fifth: expected {fifth}, got {actual_fifth}",
1036 system.display_name()
1037 );
1038 }
1039 }
1040
1041 #[test]
1042 fn quarter_comma_meantone_and_kirnberger_have_a_pure_major_third() {
1043 for system in [
1052 TuningSystem::QuarterCommaMeantone,
1053 TuningSystem::KirnbergerIII,
1054 ] {
1055 let cents = cents_at_degree(system, 4);
1056 assert!(
1057 (cents - 386.313_714).abs() < 0.001,
1058 "{} major third should be the just 386.314 cents, got {cents}",
1059 system.display_name()
1060 );
1061 }
1062 }
1063
1064 #[test]
1065 fn every_historical_temperament_is_wired_up() {
1066 for system in HISTORICAL_TEMPERAMENTS {
1067 assert_eq!(system.degrees_per_period(), OCTAVE_SIZE, "{system:?}");
1068 assert!(system.ratio_table().is_some(), "{system:?} has no table");
1069 assert_eq!(system.ratio_table().unwrap().len(), 12, "{system:?}");
1070 assert!(!system.description().is_empty(), "{system:?}");
1071 assert_eq!(
1072 TuningSystem::from_str(&system.id()).unwrap(),
1073 system,
1074 "{system:?} does not round-trip through its id"
1075 );
1076 assert!(
1077 ALL_TUNING_SYSTEMS.contains(&system),
1078 "{system:?} missing from ALL_TUNING_SYSTEMS"
1079 );
1080 }
1081 }
1082
1083 #[test]
1084 fn equal_temperament_degree_helpers_work_without_tone_objects() {
1085 assert_eq!(TuningSystem::edo(12).label(0), "C-1");
1086 assert_eq!(TuningSystem::edo(12).period_of(0), 0);
1087 assert!((TuningSystem::edo(12).frequency(0) - CN1).abs() < 1e-12);
1088
1089 assert_eq!(TuningSystem::edo(12).label(69), "A4");
1090 assert_eq!(TuningSystem::edo(12).period_of(69), 5);
1091 assert!((TuningSystem::edo(12).frequency(69) - 440.0).abs() < 0.0001);
1092 }
1093
1094 #[test]
1095 fn fractional_frequency_helpers_support_pitch_space_values() {
1096 let equal = TuningSystem::EQUAL_TEMPERAMENT;
1097 assert!((equal.frequency_at(69.0) - A4).abs() < 0.0001);
1098 assert!((equal.frequency_at(60.0) - C4).abs() < 0.0001);
1099 assert!((TuningSystem::FiveLimit.frequency_at(64.0) - (C4 * 5.0 / 4.0)).abs() < 0.0001);
1100 assert!(
1101 (TuningSystem::PythagoreanTuning.frequency_at(67.0) - (C4 * 3.0 / 2.0)).abs() < 0.0001
1102 );
1103 assert!(TuningSystem::FiveLimit.cents_at(64.0) < -13.0);
1104 }
1105
1106 #[test]
1107 fn ratio_helpers_cover_octaves() {
1108 let two_one: FloatType = Ratio::new(2, 1).into();
1109 assert_eq!(TuningSystem::CarlosHarmonic.ratio(12), two_one);
1110 assert_eq!(TuningSystem::CarlosHarmonic24.ratio(24), two_one);
1111 assert_eq!(TuningSystem::EQUAL_TEMPERAMENT.ratio(12), two_one);
1112 }
1113
1114 #[test]
1115 fn a_ratio_is_rational_or_a_root() {
1116 let rational = Ratio::new(3, 2);
1117 assert_eq!(rational.value(), 1.5);
1118 assert_eq!(rational.to_string(), "3/2");
1119 assert_eq!(rational.with_octaves(2), Ratio::new(12, 2));
1120 assert_eq!(rational.to_fraction(), Some(FractionType::new(3, 2)));
1121 assert_eq!(rational.monzo().unwrap().to_string(), "[-1 1⟩");
1122 assert!((rational.cents() - 701.955).abs() < 1e-3);
1123
1124 let root = Ratio::root(2, 7, 12);
1125 assert_eq!(root.to_string(), "2^(7/12)");
1126 assert_eq!(root.with_octaves(1), Ratio::root(2, 19, 12));
1127 assert!((root.value() - 2.0_f64.powf(7.0 / 12.0)).abs() < 1e-12);
1128 assert_eq!(root.to_fraction(), None);
1129 assert!(root.monzo().is_err());
1130 }
1131
1132 #[test]
1133 fn an_equal_division_carries_its_own_step_count() {
1134 assert_eq!(equal_temperament(12, 12), Ratio::root(2, 12, 12));
1135 let quarter = TuningSystem::QUARTER_TONE;
1136 assert_eq!(quarter.exact_ratio(6), Some(Ratio::root(2, 6, 24)));
1137 assert_eq!(quarter.label(24), "C0");
1138 assert!((quarter.frequency(12) - CN1 * 2.0_f64.sqrt()).abs() < 1e-10);
1139 assert_eq!(quarter.cents(12), 0.0);
1140 }
1141
1142 #[test]
1145 fn an_equal_division_of_the_tritave_climbs_by_tritaves() {
1146 let bohlen_pierce = TuningSystem::Equal(EqualDivision::tritave(13).unwrap());
1147 assert_eq!(bohlen_pierce.degrees_per_period(), 13);
1148 assert!(!bohlen_pierce.repeats_at_the_octave());
1149 assert!((bohlen_pierce.ratio(13) - 3.0).abs() < 1e-12);
1150 assert!((bohlen_pierce.ratio(26) - 9.0).abs() < 1e-12);
1151 assert_eq!(bohlen_pierce.period_of(26), 2);
1152 assert_eq!(bohlen_pierce.exact_ratio(13), Some(Ratio::root(3, 13, 13)));
1153 assert!(
1154 (bohlen_pierce.exact_ratio(4).unwrap().value() - bohlen_pierce.ratio(4)).abs() < 1e-12
1155 );
1156 assert_eq!(bohlen_pierce.cents(5), 0.0);
1157 assert_eq!(bohlen_pierce.label(13), "G0(+2c)");
1158 assert_eq!(bohlen_pierce.id(), "13edt");
1159 assert_eq!("13edt".parse::<TuningSystem>().unwrap(), bohlen_pierce);
1160 }
1161
1162 #[test]
1165 fn an_equal_division_of_an_irrational_period_has_no_fractions() {
1166 let alpha = TuningSystem::Equal(EqualDivision::new(18, 1404.0).unwrap());
1167 assert_eq!(alpha.exact_ratio(1), None);
1168 assert!((1200.0 * alpha.ratio(1).log2() - 78.0).abs() < 1e-9);
1169 assert!((1200.0 * alpha.ratio(18).log2() - 1404.0).abs() < 1e-9);
1170 assert_eq!(alpha.id().parse::<TuningSystem>().unwrap(), alpha);
1171 }
1172
1173 #[test]
1175 fn the_old_equal_names_still_parse() {
1176 assert_eq!(
1177 "EqualTemperament".parse::<TuningSystem>().unwrap(),
1178 TuningSystem::EQUAL_TEMPERAMENT
1179 );
1180 assert_eq!(
1181 "WholeTone".parse::<TuningSystem>().unwrap(),
1182 TuningSystem::WHOLE_TONE
1183 );
1184 assert_eq!(
1185 "QuarterTone".parse::<TuningSystem>().unwrap(),
1186 TuningSystem::QUARTER_TONE
1187 );
1188 assert_eq!(
1189 "19edo".parse::<TuningSystem>().unwrap(),
1190 TuningSystem::edo(19)
1191 );
1192 assert_eq!(TuningSystem::EQUAL_TEMPERAMENT.id(), "12edo");
1193 assert_eq!(
1194 TuningSystem::EQUAL_TEMPERAMENT.display_name(),
1195 "Equal temperament"
1196 );
1197 assert_eq!(
1198 TuningSystem::edo(19).display_name(),
1199 "19-tone equal temperament"
1200 );
1201 }
1202
1203 #[test]
1204 fn current_tuning_system_variants_return_ratios() {
1205 assert_eq!(TuningSystem::WHOLE_TONE.ratio(6), 2.0);
1206 assert_eq!(TuningSystem::QUARTER_TONE.ratio(24), 2.0);
1207 assert_eq!(TuningSystem::PythagoreanTuning.ratio(7), 1.5);
1208 assert_eq!(TuningSystem::Indian22.ratio(22), 2.0);
1209 }
1210
1211 #[test]
1212 fn a_keyboard_system_names_its_degrees_for_their_keys() {
1213 let five_limit = TuningSystem::FiveLimit;
1214 assert_eq!(five_limit.label(60), "C4");
1215 assert_eq!(five_limit.label(61), "C♯/D♭4");
1216 assert_eq!(five_limit.label(63), "D♯/E♭4");
1217 assert_eq!(five_limit.label(70), "A♯/B♭4");
1218 assert_eq!(TuningSystem::WHOLE_TONE.label(34), "G♯/A♭4");
1219 assert_eq!(TuningSystem::edo(19).label(95), "C4");
1221 assert_eq!(TuningSystem::edo(19).label(96), "C𝄲4(+13c)");
1222 }
1223
1224 #[test]
1225 fn the_nearest_degree_to_a_degree_is_that_degree() {
1226 for system in ALL_TUNING_SYSTEMS
1227 .into_iter()
1228 .chain([TuningSystem::Equal(EqualDivision::tritave(13).unwrap())])
1229 {
1230 let steps = system.degrees_per_period() as IntegerType;
1231 for degree in (2 * steps)..(6 * steps) {
1232 let hertz = system.frequency_at(degree.into());
1233 assert_eq!(system.nearest_degree(hertz).unwrap(), degree, "{system:?}");
1234 }
1235 }
1236 assert!(TuningSystem::FiveLimit.nearest_degree(0.0).is_err());
1237 assert!(
1238 TuningSystem::FiveLimit
1239 .nearest_degree(FloatType::INFINITY)
1240 .is_err()
1241 );
1242 }
1243
1244 #[test]
1245 fn a_pitch_sounds_the_nearest_degree_of_a_system_without_its_keys() {
1246 let nineteen = TuningSystem::edo(19);
1247 let a4 = Pitch::from_name("A4").unwrap();
1248 let sounded = a4.frequency_hz_in(nineteen);
1250 assert!((sounded - nineteen.frequency(109)).abs() < 1e-9);
1251 assert!((sounded - 440.0).abs() < 1200.0 / 19.0);
1252 let e4 = Pitch::from_name("E4").unwrap();
1254 assert!((e4.frequency_hz_in(TuningSystem::FiveLimit) - 327.032).abs() < 0.001);
1255
1256 let back = Pitch::from_frequency_in(sounded, nineteen).unwrap();
1259 assert!((1200.0 * (back.frequency_hz() / sounded).log2()).abs() < 0.01);
1260 }
1261
1262 #[test]
1263 fn table_ratios_shift_by_real_octaves() {
1264 assert_eq!(TuningSystem::CarlosHarmonic.ratio(19), 3.0);
1265 assert_eq!(TuningSystem::FortyThreeTone.ratio(68), 3.0);
1266 assert_eq!(TuningSystem::PtolemyIntenseDiatonic.ratio(8), 2.25);
1267 }
1268
1269 #[test]
1270 fn non_twelve_tone_systems_keep_system_octaves_and_labels() {
1271 assert_eq!(TuningSystem::WHOLE_TONE.label(1), "D-1");
1272 assert_eq!(TuningSystem::WHOLE_TONE.degrees_per_period(), 6);
1273 assert!(
1274 (TuningSystem::WHOLE_TONE.ratio(1) - (2.0 as FloatType).powf(1.0 / 6.0)).abs() < 1e-12
1275 );
1276
1277 assert_eq!(TuningSystem::QUARTER_TONE.label(13), "F♯𝄲-1");
1278 assert_eq!(TuningSystem::QUARTER_TONE.degrees_per_period(), 24);
1279 assert!(
1280 (TuningSystem::QUARTER_TONE.ratio(13) - (2.0 as FloatType).powf(13.0 / 24.0)).abs()
1281 < 1e-12
1282 );
1283
1284 assert_eq!(TuningSystem::Thai.label(7), "C0");
1285 assert_eq!(TuningSystem::Thai.degrees_per_period(), 7);
1286 assert_eq!(TuningSystem::Thai.ratio(7), 2.0);
1287
1288 assert_eq!(TuningSystem::PtolemyIntenseDiatonic.label(8), "Re0");
1289 assert_eq!(TuningSystem::PtolemyIntenseDiatonic.degrees_per_period(), 7);
1290 assert_eq!(TuningSystem::PtolemyIntenseDiatonic.ratio(8), 2.25);
1291
1292 assert_eq!(TuningSystem::FortyThreeTone.label(68), "G0(+2c)");
1293 assert_eq!(TuningSystem::FortyThreeTone.degrees_per_period(), 43);
1294 assert_eq!(TuningSystem::FortyThreeTone.ratio(68), 3.0);
1295 }
1296
1297 #[test]
1298 fn tuning_system_display_and_parse_are_canonical() {
1299 let system = TuningSystem::FiveLimit;
1300 assert_eq!(system.id(), "FiveLimit");
1301 assert_eq!(system.to_string(), "FiveLimit");
1302 assert_eq!("FiveLimit".parse::<TuningSystem>().unwrap(), system);
1303
1304 let err = "not-a-system".parse::<TuningSystem>().unwrap_err();
1305 assert_eq!(
1306 err,
1307 Error::TuningSystem("unknown tuning system \"not-a-system\"".to_string())
1308 );
1309 }
1310
1311 #[test]
1312 fn tuning_system_display_names_cover_variants() {
1313 for system in ALL_TUNING_SYSTEMS {
1314 assert!(!system.id().is_empty());
1315 assert!(!system.display_name().is_empty());
1316 assert!(!system.description().is_empty());
1317 assert!(system.degrees_per_period() > 0);
1318 assert_eq!(system.to_string(), system.id());
1319 }
1320 }
1321
1322 #[test]
1323 fn twelve_tone_systems_keep_chromatic_ratios_ascending() {
1324 for system in ALL_TUNING_SYSTEMS
1325 .into_iter()
1326 .filter(|system| system.degrees_per_period() == OCTAVE_SIZE)
1327 {
1328 let mut previous = system.ratio(0);
1329 for degree in 1..=OCTAVE_SIZE {
1330 let ratio = system.ratio(degree as usize);
1331 assert!(
1332 ratio > previous,
1333 "{} degree {degree} ratio {ratio} should be higher than {previous}",
1334 system.id()
1335 );
1336 previous = ratio;
1337 }
1338 }
1339 }
1340
1341 #[test]
1342 fn all_ratio_tables_keep_degrees_strictly_ascending_within_the_octave() {
1343 for system in ALL_TUNING_SYSTEMS {
1344 let octave_size = system.degrees_per_period();
1345 let mut previous = system.ratio(0);
1346 for degree in 1..=octave_size {
1347 let ratio = system.ratio(degree as usize);
1348 assert!(
1349 ratio > previous,
1350 "{} degree {degree} ratio {ratio} should be higher than {previous} \
1351 (a table entry is likely mistranscribed)",
1352 system.id()
1353 );
1354 previous = ratio;
1355 }
1356 }
1357 }
1358}