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]
378pub enum TuningSystem {
379 Equal(EqualDivision),
383
384 CarlosHarmonic,
390 CarlosHarmonic24,
392 PythagoreanTuning,
394
395 FiveLimit,
397 ElevenLimit,
399
400 FortyThreeTone,
402
403 Javanese,
406 Thai,
408 PtolemyIntenseDiatonic,
413 IndianAlt,
415 Indian22,
417
418 QuarterCommaMeantone,
421 WerckmeisterIII,
423 Rameau,
425 KirnbergerIII,
427 Vallotti,
429 YoungII,
431 ThirdCommaMeantone,
433 SixthCommaMeantone,
435 WerckmeisterIV,
437 WerckmeisterV,
439 KirnbergerI,
441 NeidhardtI,
443 Silbermann,
445 LehmanBach,
447}
448
449impl TuningSystem {
450 pub fn id(self) -> String {
452 let id = match self {
453 Self::Equal(division) => return division.to_string(),
454 Self::CarlosHarmonic => "CarlosHarmonic",
455 Self::CarlosHarmonic24 => "CarlosHarmonic24",
456 Self::PythagoreanTuning => "PythagoreanTuning",
457 Self::FiveLimit => "FiveLimit",
458 Self::ElevenLimit => "ElevenLimit",
459 Self::FortyThreeTone => "FortyThreeTone",
460 Self::Javanese => "Javanese",
461 Self::Thai => "Thai",
462 Self::PtolemyIntenseDiatonic => "PtolemyIntenseDiatonic",
463 Self::IndianAlt => "IndianAlt",
464 Self::Indian22 => "Indian22",
465 Self::QuarterCommaMeantone => "QuarterCommaMeantone",
466 Self::WerckmeisterIII => "WerckmeisterIII",
467 Self::Rameau => "Rameau",
468 Self::KirnbergerIII => "KirnbergerIII",
469 Self::Vallotti => "Vallotti",
470 Self::YoungII => "YoungII",
471 Self::ThirdCommaMeantone => "ThirdCommaMeantone",
472 Self::SixthCommaMeantone => "SixthCommaMeantone",
473 Self::WerckmeisterIV => "WerckmeisterIV",
474 Self::WerckmeisterV => "WerckmeisterV",
475 Self::KirnbergerI => "KirnbergerI",
476 Self::NeidhardtI => "NeidhardtI",
477 Self::Silbermann => "Silbermann",
478 Self::LehmanBach => "LehmanBach",
479 };
480 id.to_string()
481 }
482
483 pub fn display_name(self) -> String {
485 let name = match self {
486 Self::Equal(division) => return equal_display_name(division),
487 Self::CarlosHarmonic => "Carlos Harmonic",
488 Self::CarlosHarmonic24 => "Carlos Harmonic 24",
489 Self::PythagoreanTuning => "Pythagorean",
490 Self::FiveLimit => "Five-limit",
491 Self::ElevenLimit => "Partch 11-limit diamond",
492 Self::FortyThreeTone => "Partch 43-tone",
493 Self::Javanese => "Javanese",
494 Self::Thai => "Thai",
495 Self::PtolemyIntenseDiatonic => "Ptolemy intense diatonic",
496 Self::IndianAlt => "Indian Sa-grama",
497 Self::Indian22 => "Indian shruti",
498 Self::QuarterCommaMeantone => "Quarter-comma meantone",
499 Self::WerckmeisterIII => "Werckmeister III",
500 Self::Rameau => "Rameau",
501 Self::KirnbergerIII => "Kirnberger III",
502 Self::Vallotti => "Vallotti",
503 Self::YoungII => "Young II",
504 Self::ThirdCommaMeantone => "Third-comma meantone",
505 Self::SixthCommaMeantone => "Sixth-comma meantone",
506 Self::WerckmeisterIV => "Werckmeister IV",
507 Self::WerckmeisterV => "Werckmeister V",
508 Self::KirnbergerI => "Kirnberger I",
509 Self::NeidhardtI => "Neidhardt I",
510 Self::Silbermann => "Silbermann",
511 Self::LehmanBach => "Lehman-Bach",
512 };
513 name.to_string()
514 }
515
516 pub fn description(self) -> String {
518 let description = match self {
519 Self::Equal(division) => return equal_description(division),
520 Self::CarlosHarmonic => "A twelve-tone harmonic-series scale, reaching the 19-limit.",
521 Self::CarlosHarmonic24 => "A twenty-four-tone harmonic-series scale.",
522 Self::PythagoreanTuning => "A twelve-tone tuning table built from pure fifths.",
523 Self::FiveLimit => "A twelve-tone table using five-limit just ratios.",
524 Self::ElevenLimit => "Harry Partch's twenty-nine-tone 11-limit tonality diamond.",
525 Self::FortyThreeTone => "Harry Partch's forty-three-tone pure scale.",
526 Self::Javanese => "A five-tone Javanese equal-temperament approximation.",
527 Self::Thai => "A seven-tone Thai equal-temperament approximation.",
528 Self::PtolemyIntenseDiatonic => {
529 "Ptolemy's intense diatonic, also Zarlino's just major scale."
530 }
531 Self::IndianAlt => "The Indian Sa-grama mode, the inverse of Didymus' diatonic.",
532 Self::Indian22 => "The twenty-two-shruti Indian scale.",
533 Self::QuarterCommaMeantone => {
534 "A twelve-tone quarter-comma meantone temperament (Aaron, 1523)."
535 }
536 Self::WerckmeisterIII => "A twelve-tone Werckmeister III well temperament (1681).",
537 Self::Rameau => "A twelve-tone Rameau modified meantone temperament (1725).",
538 Self::KirnbergerIII => "A twelve-tone Kirnberger III well temperament (1744).",
539 Self::Vallotti => "A twelve-tone Vallotti well temperament (c. 1754).",
540 Self::YoungII => "A twelve-tone Thomas Young well temperament no. 2 (1799).",
541 Self::ThirdCommaMeantone => {
542 "A twelve-tone third-comma meantone temperament (Salinas, 1577)."
543 }
544 Self::SixthCommaMeantone => {
545 "A twelve-tone sixth-comma meantone temperament (Salinas, 1577)."
546 }
547 Self::WerckmeisterIV => "A twelve-tone Werckmeister IV well temperament (1681).",
548 Self::WerckmeisterV => "A twelve-tone Werckmeister V well temperament (1681).",
549 Self::KirnbergerI => "A twelve-tone Kirnberger I well temperament (1766).",
550 Self::NeidhardtI => "A twelve-tone Neidhardt I well temperament (1724).",
551 Self::Silbermann => "A twelve-tone Gottfried Silbermann temperament no. 1 (c. 1730).",
552 Self::LehmanBach => "A twelve-tone Lehman-Bach temperament (2005).",
553 };
554 description.to_string()
555 }
556
557 pub fn ratio(self, index: usize) -> FloatType {
559 self.ratio_at(index as FloatType)
560 }
561
562 pub fn ratio_at(self, index: FloatType) -> FloatType {
566 assert!(index.is_finite(), "degree index must be finite");
567 let steps = FloatType::from(self.degrees_per_period());
568 let period = self.period_ratio();
569 let Some(table) = self.ratio_table() else {
570 return period.powf(index / steps);
571 };
572 let whole = index.floor() as IntegerType;
573 let len = IntegerType::try_from(table.len()).expect("ratio table length exceeds i32 range");
574 let fraction = index - FloatType::from(whole);
575 table[whole.rem_euclid(len) as usize].value()
576 * period.powi(whole.div_euclid(len))
577 * period.powf(fraction / steps)
578 }
579
580 pub fn exact_ratio(self, index: usize) -> Option<Ratio> {
587 let index = UnsignedIntegerType::try_from(index).expect("tone index exceeds u32 range");
588 match self {
589 Self::Equal(division) => match division.period_ratio() {
590 Some((base, 1)) => Some(Ratio::root(
591 UnsignedIntegerType::try_from(base).ok()?,
592 index,
593 division.divisions(),
594 )),
595 _ => None,
596 },
597 _ => {
598 let table = self.ratio_table()?;
599 let len = table.len() as UnsignedIntegerType;
600 Some(table[(index % len) as usize].with_octaves(index / len))
601 }
602 }
603 }
604
605 pub fn label(self, index: UnsignedIntegerType) -> String {
614 let steps = self.degrees_per_period();
615 if matches!(self, Self::PtolemyIntenseDiatonic | Self::IndianAlt) && steps == 7 {
616 let octave = IntegerType::try_from(index / steps).expect("an octave fits an i32") - 1;
617 return format!("{}{octave}", INDIAN_SCALE_NAMES[(index % steps) as usize]);
618 }
619 let index = IntegerType::try_from(index).expect("a degree index fits an i32");
620 if self.is_keyboard() {
621 return key_label(index);
622 }
623 let pitch = self
624 .pitch_at(index)
625 .expect("every degree of a tuning system is a pitch");
626 if pitch.is_twelve_tone() {
627 return key_label(pitch.pitch_space().round() as IntegerType);
628 }
629 let microtone = pitch.microtone().map(ToString::to_string);
630 format!(
631 "{}{}",
632 pitch.unicode_name_with_octave(),
633 microtone.unwrap_or_default()
634 )
635 }
636
637 pub fn period_of(self, index: UnsignedIntegerType) -> UnsignedIntegerType {
639 index / self.degrees_per_period()
640 }
641
642 pub fn frequency(self, index: UnsignedIntegerType) -> FloatType {
644 self.frequency_at(FloatType::from(index))
645 }
646
647 pub fn frequency_at(self, index: FloatType) -> FloatType {
649 CN1 * self.ratio_at(index)
650 }
651
652 pub fn nearest_degree(self, hertz: FloatType) -> Result<IntegerType> {
665 if !hertz.is_finite() || hertz <= 0.0 {
666 return Err(Error::TuningSystem(format!(
667 "a frequency must be a finite number above nought, got {hertz}"
668 )));
669 }
670 Ok(self.nearest_degree_to_cents(OCTAVE_CENTS * (hertz / CN1).log2()))
671 }
672
673 fn nearest_degree_to_cents(self, cents: FloatType) -> IntegerType {
677 let steps = IntegerType::try_from(self.degrees_per_period())
678 .expect("a period's degree count fits an i32");
679 let period = (cents / self.period_cents()).floor() as IntegerType;
680 let distance = |degree: IntegerType| {
681 (OCTAVE_CENTS * self.ratio_at(degree.into()).log2() - cents).abs()
682 };
683 ((period - 1) * steps..=(period + 2) * steps)
684 .min_by(|&a, &b| distance(a).total_cmp(&distance(b)))
685 .expect("the search range is never empty")
686 }
687
688 pub fn pitch_at(self, index: IntegerType) -> Result<Pitch> {
701 Pitch::from_pitch_space(OCTAVE_CENTS * self.ratio_at(index.into()).log2() / 100.0)
702 }
703
704 pub fn cents(self, index: UnsignedIntegerType) -> FloatType {
708 self.cents_at(FloatType::from(index))
709 }
710
711 pub fn cents_at(self, index: FloatType) -> FloatType {
713 let equal = self
714 .period_ratio()
715 .powf(index / FloatType::from(self.degrees_per_period()));
716 1200.0 * (self.ratio_at(index) / equal).log2()
717 }
718
719 pub fn degrees_per_period(self) -> UnsignedIntegerType {
722 match self {
723 Self::Equal(division) => division.divisions(),
724 _ => self
725 .ratio_table()
726 .map_or(OCTAVE_SIZE, |table| table.len() as UnsignedIntegerType),
727 }
728 }
729
730 pub fn period_cents(self) -> FloatType {
733 match self {
734 Self::Equal(division) => division.period_cents(),
735 _ => OCTAVE_CENTS,
736 }
737 }
738
739 pub fn repeats_at_the_octave(self) -> bool {
741 match self {
742 Self::Equal(division) => division.repeats_at_the_octave(),
743 _ => true,
744 }
745 }
746
747 pub fn is_keyboard(self) -> bool {
750 self.degrees_per_period() == 12 && self.repeats_at_the_octave()
751 }
752
753 fn period_ratio(self) -> FloatType {
754 (2.0 as FloatType).powf(self.period_cents() / OCTAVE_CENTS)
755 }
756
757 fn ratio_table(self) -> Option<&'static [Ratio]> {
758 match self {
759 Self::CarlosHarmonic => Some(&CARLOS_HARMONIC),
760 Self::CarlosHarmonic24 => Some(&CARLOS_HARMONIC_24),
761 Self::PythagoreanTuning => Some(&PYTHAGOREAN_TUNING),
762 Self::FiveLimit => Some(&FIVE_LIMIT),
763 Self::ElevenLimit => Some(&ELEVEN_LIMIT),
764 Self::FortyThreeTone => Some(&FORTY_THREE_TONE),
765 Self::Javanese => Some(&JAVANESE),
766 Self::Thai => Some(&THAI),
767 Self::PtolemyIntenseDiatonic => Some(&PTOLEMY_INTENSE_DIATONIC),
768 Self::IndianAlt => Some(&INDIA_SCALE_ALT),
769 Self::Indian22 => Some(&INDIAN_SCALE_22),
770 Self::QuarterCommaMeantone => Some(&QUARTER_COMMA_MEANTONE),
771 Self::WerckmeisterIII => Some(&WERCKMEISTER_III),
772 Self::Rameau => Some(&RAMEAU),
773 Self::KirnbergerIII => Some(&KIRNBERGER_III),
774 Self::Vallotti => Some(&VALLOTTI),
775 Self::YoungII => Some(&YOUNG_II),
776 Self::ThirdCommaMeantone => Some(&THIRD_COMMA_MEANTONE),
777 Self::SixthCommaMeantone => Some(&SIXTH_COMMA_MEANTONE),
778 Self::WerckmeisterIV => Some(&WERCKMEISTER_IV),
779 Self::WerckmeisterV => Some(&WERCKMEISTER_V),
780 Self::KirnbergerI => Some(&KIRNBERGER_I),
781 Self::NeidhardtI => Some(&NEIDHARDT_I),
782 Self::Silbermann => Some(&SILBERMANN),
783 Self::LehmanBach => Some(&LEHMAN_BACH),
784 Self::Equal(_) => None,
785 }
786 }
787
788 pub const EQUAL_TEMPERAMENT: Self = Self::edo(12);
790
791 pub const WHOLE_TONE: Self = Self::edo(6);
793
794 pub const QUARTER_TONE: Self = Self::edo(24);
796
797 pub const fn edo(divisions: UnsignedIntegerType) -> Self {
803 Self::Equal(EqualDivision::edo(nonzero(divisions)))
804 }
805}
806
807const fn nonzero(divisions: UnsignedIntegerType) -> NonZeroU32 {
809 match NonZeroU32::new(divisions) {
810 Some(divisions) => divisions,
811 None => panic!("a period cannot be divided into no steps"),
812 }
813}
814
815fn equal_display_name(division: EqualDivision) -> String {
816 match (division.period_ratio(), division.divisions()) {
817 (Some((2, 1)), 12) => "Equal temperament".to_string(),
818 (Some((2, 1)), 6) => "Whole tone".to_string(),
819 (Some((2, 1)), 24) => "Quarter tone".to_string(),
820 (Some((2, 1)), divisions) => format!("{divisions}-tone equal temperament"),
821 _ => format!("{division}"),
822 }
823}
824
825fn equal_description(division: EqualDivision) -> String {
826 let divisions = division.divisions();
827 match division.period_ratio() {
828 Some((2, 1)) => match divisions {
829 12 => "Twelve equal divisions of the octave.".to_string(),
830 6 => "Six equal whole-tone steps per octave.".to_string(),
831 24 => "Twenty-four equal quarter-tone steps per octave.".to_string(),
832 _ => format!("{divisions} equal divisions of the octave."),
833 },
834 Some((numerator, denominator)) => format!(
835 "{divisions} equal divisions of {numerator}/{denominator}, repeating there rather than at the octave."
836 ),
837 None => format!(
838 "{divisions} equal divisions of {:.3} cents, repeating there rather than at the octave.",
839 division.period_cents()
840 ),
841 }
842}
843
844impl Display for TuningSystem {
845 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
846 f.write_str(&self.id())
847 }
848}
849
850impl FromStr for TuningSystem {
851 type Err = Error;
852
853 fn from_str(s: &str) -> Result<Self, Self::Err> {
857 match s {
858 "EqualTemperament" => Ok(Self::EQUAL_TEMPERAMENT),
859 "WholeTone" => Ok(Self::WHOLE_TONE),
860 "QuarterTone" => Ok(Self::QUARTER_TONE),
861 "CarlosHarmonic" => Ok(Self::CarlosHarmonic),
862 "CarlosHarmonic24" => Ok(Self::CarlosHarmonic24),
863 "PythagoreanTuning" => Ok(Self::PythagoreanTuning),
864 "FiveLimit" => Ok(Self::FiveLimit),
865 "ElevenLimit" => Ok(Self::ElevenLimit),
866 "FortyThreeTone" => Ok(Self::FortyThreeTone),
867 "Javanese" => Ok(Self::Javanese),
868 "Thai" => Ok(Self::Thai),
869 "PtolemyIntenseDiatonic" => Ok(Self::PtolemyIntenseDiatonic),
870 "IndianAlt" => Ok(Self::IndianAlt),
871 "Indian22" => Ok(Self::Indian22),
872 "QuarterCommaMeantone" => Ok(Self::QuarterCommaMeantone),
873 "WerckmeisterIII" => Ok(Self::WerckmeisterIII),
874 "Rameau" => Ok(Self::Rameau),
875 "KirnbergerIII" => Ok(Self::KirnbergerIII),
876 "Vallotti" => Ok(Self::Vallotti),
877 "YoungII" => Ok(Self::YoungII),
878 "ThirdCommaMeantone" => Ok(Self::ThirdCommaMeantone),
879 "SixthCommaMeantone" => Ok(Self::SixthCommaMeantone),
880 "WerckmeisterIV" => Ok(Self::WerckmeisterIV),
881 "WerckmeisterV" => Ok(Self::WerckmeisterV),
882 "KirnbergerI" => Ok(Self::KirnbergerI),
883 "NeidhardtI" => Ok(Self::NeidhardtI),
884 "Silbermann" => Ok(Self::Silbermann),
885 "LehmanBach" => Ok(Self::LehmanBach),
886 _ if s.contains("ed") => s.parse().map(Self::Equal),
887 _ => Err(Error::TuningSystem(format!("unknown tuning system {s:?}"))),
888 }
889 }
890}
891
892pub const fn equal_temperament(
894 tone: UnsignedIntegerType,
895 octave_size: UnsignedIntegerType,
896) -> Ratio {
897 Ratio::root(2, tone, octave_size)
898}
899
900fn key_label(pitch_space: IntegerType) -> String {
903 let pitch =
904 Pitch::from_pitch_space(pitch_space.into()).expect("a whole pitch-space value is a pitch");
905 let octave = pitch_space.div_euclid(12) - 1;
906 let alter = pitch.alter();
907 if alter == 0.0 {
908 return format!("{}{octave}", pitch.unicode_name());
909 }
910 let (sharp, flat) = if alter < 0.0 {
911 (pitch.get_lower_enharmonic(), Ok(pitch))
912 } else {
913 (Ok(pitch.clone()), pitch.get_higher_enharmonic())
914 };
915 let (sharp, flat) = (
916 sharp.expect("a black key has a sharp name"),
917 flat.expect("a black key has a flat name"),
918 );
919 format!("{}/{}{octave}", sharp.unicode_name(), flat.unicode_name())
920}
921
922pub const JAVANESE: [Ratio; 5] = [
924 Ratio::root(2, 0, 5),
925 Ratio::root(2, 1, 5),
926 Ratio::root(2, 2, 5),
927 Ratio::root(2, 3, 5),
928 Ratio::root(2, 4, 5),
929];
930
931pub const THAI: [Ratio; 7] = [
933 Ratio::root(2, 0, 7),
934 Ratio::root(2, 1, 7),
935 Ratio::root(2, 2, 7),
936 Ratio::root(2, 3, 7),
937 Ratio::root(2, 4, 7),
938 Ratio::root(2, 5, 7),
939 Ratio::root(2, 6, 7),
940];
941
942pub const INDIAN_SCALE_NAMES: [&str; 7] = ["Sa", "Re", "Ga", "Ma", "Pa", "Dha", "Ni"];
944
945#[cfg(test)]
946mod tests {
947 use super::*;
948
949 #[test]
950 fn every_system_answers_cents_and_an_exact_ratio_for_its_degrees() {
951 for system in ALL_TUNING_SYSTEMS {
952 let unison = system.exact_ratio(0).expect("every listed system is exact");
953 assert_eq!(unison.value(), 1.0);
954 assert_eq!(system.cents(0), 0.0);
955 assert!(system.cents(system.degrees_per_period()).abs() < 1e-9);
956 }
957 }
958
959 fn cents_at_degree(system: TuningSystem, degree: usize) -> FloatType {
961 1200.0 * system.ratio(degree).log2()
962 }
963
964 #[test]
965 fn meantone_fifths_match_their_comma_fractions() {
966 const PURE_FIFTH: FloatType = 701.955;
970 const SYNTONIC_COMMA: FloatType = 21.506;
971 for (system, divisor) in [
972 (TuningSystem::ThirdCommaMeantone, 3.0),
973 (TuningSystem::QuarterCommaMeantone, 4.0),
974 (TuningSystem::SixthCommaMeantone, 6.0),
975 ] {
976 let expected = PURE_FIFTH - SYNTONIC_COMMA / divisor;
977 let actual = cents_at_degree(system, 7);
978 assert!(
979 (actual - expected).abs() < 0.01,
980 "{} fifth: expected {expected:.3}, got {actual:.3}",
981 system.display_name()
982 );
983 }
984 }
985
986 #[test]
987 fn kirnberger_i_keeps_its_pure_fifth_and_third() {
988 assert!((cents_at_degree(TuningSystem::KirnbergerI, 7) - 701.955).abs() < 0.01);
991 assert!((cents_at_degree(TuningSystem::KirnbergerI, 4) - 386.314).abs() < 0.01);
992 }
993
994 #[test]
995 fn common_equal_temperaments_divide_the_octave_evenly() {
996 for system in COMMON_EQUAL_TEMPERAMENTS {
997 let size = system.degrees_per_period();
998 let step = 1200.0 / FloatType::from(size);
999 for degree in 0..size as usize {
1000 let expected = step * degree as FloatType;
1001 let actual = cents_at_degree(system, degree);
1002 assert!(
1003 (actual - expected).abs() < 0.001,
1004 "{size}-EDO degree {degree}: expected {expected:.3}, got {actual:.3}"
1005 );
1006 }
1007 }
1008 }
1009
1010 #[test]
1011 fn historical_temperaments_match_their_published_values() {
1012 let cases = [
1016 (TuningSystem::QuarterCommaMeantone, 386.314, 696.578),
1017 (TuningSystem::WerckmeisterIII, 390.225, 696.090),
1018 (TuningSystem::Rameau, 386.314, 696.578),
1019 (TuningSystem::KirnbergerIII, 386.314, 696.578),
1020 (TuningSystem::Vallotti, 392.180, 698.045),
1021 (TuningSystem::YoungII, 392.180, 698.045),
1022 ];
1023
1024 for (system, major_third, fifth) in cases {
1025 let actual_third = cents_at_degree(system, 4);
1026 let actual_fifth = cents_at_degree(system, 7);
1027 assert!(
1028 (actual_third - major_third).abs() < 0.001,
1029 "{} major third: expected {major_third}, got {actual_third}",
1030 system.display_name()
1031 );
1032 assert!(
1033 (actual_fifth - fifth).abs() < 0.001,
1034 "{} fifth: expected {fifth}, got {actual_fifth}",
1035 system.display_name()
1036 );
1037 }
1038 }
1039
1040 #[test]
1041 fn quarter_comma_meantone_and_kirnberger_have_a_pure_major_third() {
1042 for system in [
1051 TuningSystem::QuarterCommaMeantone,
1052 TuningSystem::KirnbergerIII,
1053 ] {
1054 let cents = cents_at_degree(system, 4);
1055 assert!(
1056 (cents - 386.313_714).abs() < 0.001,
1057 "{} major third should be the just 386.314 cents, got {cents}",
1058 system.display_name()
1059 );
1060 }
1061 }
1062
1063 #[test]
1064 fn every_historical_temperament_is_wired_up() {
1065 for system in HISTORICAL_TEMPERAMENTS {
1066 assert_eq!(system.degrees_per_period(), OCTAVE_SIZE, "{system:?}");
1067 assert!(system.ratio_table().is_some(), "{system:?} has no table");
1068 assert_eq!(system.ratio_table().unwrap().len(), 12, "{system:?}");
1069 assert!(!system.description().is_empty(), "{system:?}");
1070 assert_eq!(
1071 TuningSystem::from_str(&system.id()).unwrap(),
1072 system,
1073 "{system:?} does not round-trip through its id"
1074 );
1075 assert!(
1076 ALL_TUNING_SYSTEMS.contains(&system),
1077 "{system:?} missing from ALL_TUNING_SYSTEMS"
1078 );
1079 }
1080 }
1081
1082 #[test]
1083 fn equal_temperament_degree_helpers_work_without_tone_objects() {
1084 assert_eq!(TuningSystem::edo(12).label(0), "C-1");
1085 assert_eq!(TuningSystem::edo(12).period_of(0), 0);
1086 assert!((TuningSystem::edo(12).frequency(0) - CN1).abs() < 1e-12);
1087
1088 assert_eq!(TuningSystem::edo(12).label(69), "A4");
1089 assert_eq!(TuningSystem::edo(12).period_of(69), 5);
1090 assert!((TuningSystem::edo(12).frequency(69) - 440.0).abs() < 0.0001);
1091 }
1092
1093 #[test]
1094 fn fractional_frequency_helpers_support_pitch_space_values() {
1095 let equal = TuningSystem::EQUAL_TEMPERAMENT;
1096 assert!((equal.frequency_at(69.0) - A4).abs() < 0.0001);
1097 assert!((equal.frequency_at(60.0) - C4).abs() < 0.0001);
1098 assert!((TuningSystem::FiveLimit.frequency_at(64.0) - (C4 * 5.0 / 4.0)).abs() < 0.0001);
1099 assert!(
1100 (TuningSystem::PythagoreanTuning.frequency_at(67.0) - (C4 * 3.0 / 2.0)).abs() < 0.0001
1101 );
1102 assert!(TuningSystem::FiveLimit.cents_at(64.0) < -13.0);
1103 }
1104
1105 #[test]
1106 fn ratio_helpers_cover_octaves() {
1107 let two_one: FloatType = Ratio::new(2, 1).into();
1108 assert_eq!(TuningSystem::CarlosHarmonic.ratio(12), two_one);
1109 assert_eq!(TuningSystem::CarlosHarmonic24.ratio(24), two_one);
1110 assert_eq!(TuningSystem::EQUAL_TEMPERAMENT.ratio(12), two_one);
1111 }
1112
1113 #[test]
1114 fn a_ratio_is_rational_or_a_root() {
1115 let rational = Ratio::new(3, 2);
1116 assert_eq!(rational.value(), 1.5);
1117 assert_eq!(rational.to_string(), "3/2");
1118 assert_eq!(rational.with_octaves(2), Ratio::new(12, 2));
1119 assert_eq!(rational.to_fraction(), Some(FractionType::new(3, 2)));
1120 assert_eq!(rational.monzo().unwrap().to_string(), "[-1 1⟩");
1121 assert!((rational.cents() - 701.955).abs() < 1e-3);
1122
1123 let root = Ratio::root(2, 7, 12);
1124 assert_eq!(root.to_string(), "2^(7/12)");
1125 assert_eq!(root.with_octaves(1), Ratio::root(2, 19, 12));
1126 assert!((root.value() - 2.0_f64.powf(7.0 / 12.0)).abs() < 1e-12);
1127 assert_eq!(root.to_fraction(), None);
1128 assert!(root.monzo().is_err());
1129 }
1130
1131 #[test]
1132 fn an_equal_division_carries_its_own_step_count() {
1133 assert_eq!(equal_temperament(12, 12), Ratio::root(2, 12, 12));
1134 let quarter = TuningSystem::QUARTER_TONE;
1135 assert_eq!(quarter.exact_ratio(6), Some(Ratio::root(2, 6, 24)));
1136 assert_eq!(quarter.label(24), "C0");
1137 assert!((quarter.frequency(12) - CN1 * 2.0_f64.sqrt()).abs() < 1e-10);
1138 assert_eq!(quarter.cents(12), 0.0);
1139 }
1140
1141 #[test]
1144 fn an_equal_division_of_the_tritave_climbs_by_tritaves() {
1145 let bohlen_pierce = TuningSystem::Equal(EqualDivision::tritave(13).unwrap());
1146 assert_eq!(bohlen_pierce.degrees_per_period(), 13);
1147 assert!(!bohlen_pierce.repeats_at_the_octave());
1148 assert!((bohlen_pierce.ratio(13) - 3.0).abs() < 1e-12);
1149 assert!((bohlen_pierce.ratio(26) - 9.0).abs() < 1e-12);
1150 assert_eq!(bohlen_pierce.period_of(26), 2);
1151 assert_eq!(bohlen_pierce.exact_ratio(13), Some(Ratio::root(3, 13, 13)));
1152 assert!(
1153 (bohlen_pierce.exact_ratio(4).unwrap().value() - bohlen_pierce.ratio(4)).abs() < 1e-12
1154 );
1155 assert_eq!(bohlen_pierce.cents(5), 0.0);
1156 assert_eq!(bohlen_pierce.label(13), "G0(+2c)");
1157 assert_eq!(bohlen_pierce.id(), "13edt");
1158 assert_eq!("13edt".parse::<TuningSystem>().unwrap(), bohlen_pierce);
1159 }
1160
1161 #[test]
1164 fn an_equal_division_of_an_irrational_period_has_no_fractions() {
1165 let alpha = TuningSystem::Equal(EqualDivision::new(18, 1404.0).unwrap());
1166 assert_eq!(alpha.exact_ratio(1), None);
1167 assert!((1200.0 * alpha.ratio(1).log2() - 78.0).abs() < 1e-9);
1168 assert!((1200.0 * alpha.ratio(18).log2() - 1404.0).abs() < 1e-9);
1169 assert_eq!(alpha.id().parse::<TuningSystem>().unwrap(), alpha);
1170 }
1171
1172 #[test]
1174 fn the_old_equal_names_still_parse() {
1175 assert_eq!(
1176 "EqualTemperament".parse::<TuningSystem>().unwrap(),
1177 TuningSystem::EQUAL_TEMPERAMENT
1178 );
1179 assert_eq!(
1180 "WholeTone".parse::<TuningSystem>().unwrap(),
1181 TuningSystem::WHOLE_TONE
1182 );
1183 assert_eq!(
1184 "QuarterTone".parse::<TuningSystem>().unwrap(),
1185 TuningSystem::QUARTER_TONE
1186 );
1187 assert_eq!(
1188 "19edo".parse::<TuningSystem>().unwrap(),
1189 TuningSystem::edo(19)
1190 );
1191 assert_eq!(TuningSystem::EQUAL_TEMPERAMENT.id(), "12edo");
1192 assert_eq!(
1193 TuningSystem::EQUAL_TEMPERAMENT.display_name(),
1194 "Equal temperament"
1195 );
1196 assert_eq!(
1197 TuningSystem::edo(19).display_name(),
1198 "19-tone equal temperament"
1199 );
1200 }
1201
1202 #[test]
1203 fn current_tuning_system_variants_return_ratios() {
1204 assert_eq!(TuningSystem::WHOLE_TONE.ratio(6), 2.0);
1205 assert_eq!(TuningSystem::QUARTER_TONE.ratio(24), 2.0);
1206 assert_eq!(TuningSystem::PythagoreanTuning.ratio(7), 1.5);
1207 assert_eq!(TuningSystem::Indian22.ratio(22), 2.0);
1208 }
1209
1210 #[test]
1211 fn a_keyboard_system_names_its_degrees_for_their_keys() {
1212 let five_limit = TuningSystem::FiveLimit;
1213 assert_eq!(five_limit.label(60), "C4");
1214 assert_eq!(five_limit.label(61), "C♯/D♭4");
1215 assert_eq!(five_limit.label(63), "D♯/E♭4");
1216 assert_eq!(five_limit.label(70), "A♯/B♭4");
1217 assert_eq!(TuningSystem::WHOLE_TONE.label(34), "G♯/A♭4");
1218 assert_eq!(TuningSystem::edo(19).label(95), "C4");
1220 assert_eq!(TuningSystem::edo(19).label(96), "C𝄲4(+13c)");
1221 }
1222
1223 #[test]
1224 fn the_nearest_degree_to_a_degree_is_that_degree() {
1225 for system in ALL_TUNING_SYSTEMS
1226 .into_iter()
1227 .chain([TuningSystem::Equal(EqualDivision::tritave(13).unwrap())])
1228 {
1229 let steps = system.degrees_per_period() as IntegerType;
1230 for degree in (2 * steps)..(6 * steps) {
1231 let hertz = system.frequency_at(degree.into());
1232 assert_eq!(system.nearest_degree(hertz).unwrap(), degree, "{system:?}");
1233 }
1234 }
1235 assert!(TuningSystem::FiveLimit.nearest_degree(0.0).is_err());
1236 assert!(
1237 TuningSystem::FiveLimit
1238 .nearest_degree(FloatType::INFINITY)
1239 .is_err()
1240 );
1241 }
1242
1243 #[test]
1244 fn a_pitch_sounds_the_nearest_degree_of_a_system_without_its_keys() {
1245 let nineteen = TuningSystem::edo(19);
1246 let a4 = Pitch::from_name("A4").unwrap();
1247 let sounded = a4.frequency_hz_in(nineteen);
1249 assert!((sounded - nineteen.frequency(109)).abs() < 1e-9);
1250 assert!((sounded - 440.0).abs() < 1200.0 / 19.0);
1251 let e4 = Pitch::from_name("E4").unwrap();
1253 assert!((e4.frequency_hz_in(TuningSystem::FiveLimit) - 327.032).abs() < 0.001);
1254
1255 let back = Pitch::from_frequency_in(sounded, nineteen).unwrap();
1258 assert!((1200.0 * (back.frequency_hz() / sounded).log2()).abs() < 0.01);
1259 }
1260
1261 #[test]
1262 fn table_ratios_shift_by_real_octaves() {
1263 assert_eq!(TuningSystem::CarlosHarmonic.ratio(19), 3.0);
1264 assert_eq!(TuningSystem::FortyThreeTone.ratio(68), 3.0);
1265 assert_eq!(TuningSystem::PtolemyIntenseDiatonic.ratio(8), 2.25);
1266 }
1267
1268 #[test]
1269 fn non_twelve_tone_systems_keep_system_octaves_and_labels() {
1270 assert_eq!(TuningSystem::WHOLE_TONE.label(1), "D-1");
1271 assert_eq!(TuningSystem::WHOLE_TONE.degrees_per_period(), 6);
1272 assert!(
1273 (TuningSystem::WHOLE_TONE.ratio(1) - (2.0 as FloatType).powf(1.0 / 6.0)).abs() < 1e-12
1274 );
1275
1276 assert_eq!(TuningSystem::QUARTER_TONE.label(13), "F♯𝄲-1");
1277 assert_eq!(TuningSystem::QUARTER_TONE.degrees_per_period(), 24);
1278 assert!(
1279 (TuningSystem::QUARTER_TONE.ratio(13) - (2.0 as FloatType).powf(13.0 / 24.0)).abs()
1280 < 1e-12
1281 );
1282
1283 assert_eq!(TuningSystem::Thai.label(7), "C0");
1284 assert_eq!(TuningSystem::Thai.degrees_per_period(), 7);
1285 assert_eq!(TuningSystem::Thai.ratio(7), 2.0);
1286
1287 assert_eq!(TuningSystem::PtolemyIntenseDiatonic.label(8), "Re0");
1288 assert_eq!(TuningSystem::PtolemyIntenseDiatonic.degrees_per_period(), 7);
1289 assert_eq!(TuningSystem::PtolemyIntenseDiatonic.ratio(8), 2.25);
1290
1291 assert_eq!(TuningSystem::FortyThreeTone.label(68), "G0(+2c)");
1292 assert_eq!(TuningSystem::FortyThreeTone.degrees_per_period(), 43);
1293 assert_eq!(TuningSystem::FortyThreeTone.ratio(68), 3.0);
1294 }
1295
1296 #[test]
1297 fn tuning_system_display_and_parse_are_canonical() {
1298 let system = TuningSystem::FiveLimit;
1299 assert_eq!(system.id(), "FiveLimit");
1300 assert_eq!(system.to_string(), "FiveLimit");
1301 assert_eq!("FiveLimit".parse::<TuningSystem>().unwrap(), system);
1302
1303 let err = "not-a-system".parse::<TuningSystem>().unwrap_err();
1304 assert_eq!(
1305 err,
1306 Error::TuningSystem("unknown tuning system \"not-a-system\"".to_string())
1307 );
1308 }
1309
1310 #[test]
1311 fn tuning_system_display_names_cover_variants() {
1312 for system in ALL_TUNING_SYSTEMS {
1313 assert!(!system.id().is_empty());
1314 assert!(!system.display_name().is_empty());
1315 assert!(!system.description().is_empty());
1316 assert!(system.degrees_per_period() > 0);
1317 assert_eq!(system.to_string(), system.id());
1318 }
1319 }
1320
1321 #[test]
1322 fn twelve_tone_systems_keep_chromatic_ratios_ascending() {
1323 for system in ALL_TUNING_SYSTEMS
1324 .into_iter()
1325 .filter(|system| system.degrees_per_period() == OCTAVE_SIZE)
1326 {
1327 let mut previous = system.ratio(0);
1328 for degree in 1..=OCTAVE_SIZE {
1329 let ratio = system.ratio(degree as usize);
1330 assert!(
1331 ratio > previous,
1332 "{} degree {degree} ratio {ratio} should be higher than {previous}",
1333 system.id()
1334 );
1335 previous = ratio;
1336 }
1337 }
1338 }
1339
1340 #[test]
1341 fn all_ratio_tables_keep_degrees_strictly_ascending_within_the_octave() {
1342 for system in ALL_TUNING_SYSTEMS {
1343 let octave_size = system.degrees_per_period();
1344 let mut previous = system.ratio(0);
1345 for degree in 1..=octave_size {
1346 let ratio = system.ratio(degree as usize);
1347 assert!(
1348 ratio > previous,
1349 "{} degree {degree} ratio {ratio} should be higher than {previous} \
1350 (a table entry is likely mistranscribed)",
1351 system.id()
1352 );
1353 previous = ratio;
1354 }
1355 }
1356 }
1357}