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;
24
25pub use equal::{EqualDivision, TRITAVE_CENTS};
26pub use generated::*;
27pub use monzo::{Monzo, PRIMES, Val};
28pub use mos::{Mos, MosScale, OCTAVE_CENTS, moment_of_symmetry_sizes};
29pub use temperament::Temperament;
30pub use temperaments_generated::*;
31
32use crate::defaults::{FloatType, IntegerType, UnsignedIntegerType};
33use crate::error::{Error, Result};
34use crate::tuningsystem::adaptive::AdaptiveTuningSystem;
35
36use std::fmt::{Display, Formatter};
37use std::str::FromStr;
38
39pub const OCTAVE_SIZE: UnsignedIntegerType = 12;
41
42pub const C4: FloatType = 261.625_565_300_598_6;
44pub const C0: FloatType = C4 / 16.0;
46pub const CN1: FloatType = C4 / 32.0;
48
49pub const A4: FloatType = 440.0;
51pub const A0: FloatType = A4 / 16.0;
53pub const AN1: FloatType = A4 / 32.0;
55
56pub const TWELVE_TONE_NAMES: [&str; 12] = [
58 "C", "C#/Db", "D", "D#/Eb", "E", "F", "F#/Gb", "G", "G#/Ab", "A", "A#/Bb", "B",
59];
60
61pub const TWELVE_TONE_NAMES_SHARP: [&str; 12] = [
63 "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
64];
65
66pub const TWELVE_TONE_NAMES_FLAT: [&str; 12] = [
68 "C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B",
69];
70
71pub const WHOLE_TONE_NAMES: [&str; 6] = ["C", "D", "E", "F#/Gb", "G#/Ab", "A#/Bb"];
73
74pub const COMMON_TWELVE_TONE_TUNING_SYSTEMS: [TuningSystem; 4] = [
76 TuningSystem::EqualTemperament {
77 octave_size: OCTAVE_SIZE,
78 },
79 TuningSystem::CarlosHarmonic,
80 TuningSystem::PythagoreanTuning,
81 TuningSystem::FiveLimit,
82];
83
84pub const COMMON_EQUAL_TEMPERAMENTS: [TuningSystem; 8] = [
92 TuningSystem::EqualTemperament { octave_size: 12 },
93 TuningSystem::EqualTemperament { octave_size: 19 },
94 TuningSystem::EqualTemperament { octave_size: 22 },
95 TuningSystem::EqualTemperament { octave_size: 24 },
96 TuningSystem::EqualTemperament { octave_size: 31 },
97 TuningSystem::EqualTemperament { octave_size: 41 },
98 TuningSystem::EqualTemperament { octave_size: 53 },
99 TuningSystem::EqualTemperament { octave_size: 72 },
100];
101
102pub const HISTORICAL_TEMPERAMENTS: [TuningSystem; 14] = [
108 TuningSystem::QuarterCommaMeantone,
109 TuningSystem::WerckmeisterIII,
110 TuningSystem::Rameau,
111 TuningSystem::KirnbergerIII,
112 TuningSystem::Vallotti,
113 TuningSystem::YoungII,
114 TuningSystem::ThirdCommaMeantone,
115 TuningSystem::SixthCommaMeantone,
116 TuningSystem::WerckmeisterIV,
117 TuningSystem::WerckmeisterV,
118 TuningSystem::KirnbergerI,
119 TuningSystem::NeidhardtI,
120 TuningSystem::Silbermann,
121 TuningSystem::LehmanBach,
122];
123
124#[derive(Clone, Copy, Debug, Eq, PartialEq)]
126#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
127#[must_use]
128pub enum AnyTuningSystem {
129 Fixed(TuningSystem),
131 Adaptive(AdaptiveTuningSystem),
133}
134
135impl AnyTuningSystem {
136 pub fn frequency_at(
141 self,
142 context: FloatType,
143 index: FloatType,
144 size: Option<UnsignedIntegerType>,
145 ) -> FloatType {
146 match self {
147 Self::Fixed(tuning_system) => {
148 let _ = context;
149 get_frequency_at(tuning_system, index, size)
150 }
151 Self::Adaptive(adaptive_tuning_system) => {
152 adaptive_tuning_system.frequency_at(context, index, size)
153 }
154 }
155 }
156
157 pub fn cents_at(
162 self,
163 context: FloatType,
164 index: FloatType,
165 size: Option<UnsignedIntegerType>,
166 ) -> FloatType {
167 match self {
168 Self::Fixed(tuning_system) => {
169 let _ = context;
170 tuning_system.cents_at(index)
171 }
172 Self::Adaptive(adaptive_tuning_system) => {
173 adaptive_tuning_system.cents_at(context, index, size)
174 }
175 }
176 }
177
178 pub fn is_adaptive(self) -> bool {
180 matches!(self, Self::Adaptive(_))
181 }
182}
183
184impl From<TuningSystem> for AnyTuningSystem {
185 fn from(tuning_system: TuningSystem) -> Self {
186 Self::Fixed(tuning_system)
187 }
188}
189
190impl From<AdaptiveTuningSystem> for AnyTuningSystem {
191 fn from(adaptive_tuning_system: AdaptiveTuningSystem) -> Self {
192 Self::Adaptive(adaptive_tuning_system)
193 }
194}
195
196pub const ALL_TUNING_SYSTEMS: [TuningSystem; 28] = [
198 TuningSystem::EqualTemperament {
199 octave_size: OCTAVE_SIZE,
200 },
201 TuningSystem::WholeTone,
202 TuningSystem::QuarterTone,
203 TuningSystem::CarlosHarmonic,
204 TuningSystem::CarlosHarmonic24,
205 TuningSystem::PythagoreanTuning,
206 TuningSystem::FiveLimit,
207 TuningSystem::ElevenLimit,
208 TuningSystem::FortyThreeTone,
209 TuningSystem::Javanese,
210 TuningSystem::Thai,
211 TuningSystem::PtolemyIntenseDiatonic,
212 TuningSystem::IndianAlt,
213 TuningSystem::Indian22,
214 TuningSystem::QuarterCommaMeantone,
215 TuningSystem::WerckmeisterIII,
216 TuningSystem::Rameau,
217 TuningSystem::KirnbergerIII,
218 TuningSystem::Vallotti,
219 TuningSystem::YoungII,
220 TuningSystem::ThirdCommaMeantone,
221 TuningSystem::SixthCommaMeantone,
222 TuningSystem::WerckmeisterIV,
223 TuningSystem::WerckmeisterV,
224 TuningSystem::KirnbergerI,
225 TuningSystem::NeidhardtI,
226 TuningSystem::Silbermann,
227 TuningSystem::LehmanBach,
228];
229
230#[derive(Clone, Copy, Debug, Eq, PartialEq)]
231#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
232#[must_use]
234pub struct Fraction {
235 pub numerator: UnsignedIntegerType,
237 pub denominator: UnsignedIntegerType,
239 pub base: UnsignedIntegerType,
241}
242
243impl Fraction {
244 pub const fn new(numerator: UnsignedIntegerType, denominator: UnsignedIntegerType) -> Self {
246 Self::new_with_base(numerator, denominator, 0)
247 }
248
249 pub const fn new_with_base(
251 numerator: UnsignedIntegerType,
252 denominator: UnsignedIntegerType,
253 base: UnsignedIntegerType,
254 ) -> Self {
255 Self {
256 numerator,
257 denominator,
258 base,
259 }
260 }
261
262 pub const fn numerator(&self) -> UnsignedIntegerType {
264 self.numerator
265 }
266
267 pub const fn denominator(&self) -> UnsignedIntegerType {
269 self.denominator
270 }
271
272 pub const fn base(&self) -> UnsignedIntegerType {
274 self.base
275 }
276
277 pub fn ratio(self) -> FloatType {
279 self.into()
280 }
281
282 pub fn label(self) -> String {
284 self.to_string()
285 }
286
287 pub fn with_octaves(mut self, octaves: UnsignedIntegerType) -> Self {
289 if octaves == 0 {
290 return self;
291 }
292
293 if self.base == 0 {
294 let multiplier = (2 as UnsignedIntegerType)
295 .checked_pow(octaves)
296 .expect("octave multiplier exceeds u32 range");
297 self.numerator = self
298 .numerator
299 .checked_mul(multiplier)
300 .expect("fraction numerator exceeds u32 range");
301 } else {
302 let octave_offset = self
303 .denominator
304 .checked_mul(octaves)
305 .expect("fraction octave offset exceeds u32 range");
306 self.numerator = self
307 .numerator
308 .checked_add(octave_offset)
309 .expect("fraction numerator exceeds u32 range");
310 }
311
312 self
313 }
314}
315
316impl From<Fraction> for FloatType {
317 fn from(frac: Fraction) -> Self {
318 if frac.base == 0 {
319 frac.numerator as FloatType / frac.denominator as FloatType
320 } else {
321 (frac.base as FloatType)
322 .powf(frac.numerator as FloatType / frac.denominator as FloatType)
323 }
324 }
325}
326
327impl Display for Fraction {
328 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
329 if self.base == 0 {
330 if self.denominator == 1 {
331 write!(f, "{}", self.numerator)
332 } else {
333 write!(f, "{}/{}", self.numerator, self.denominator)
334 }
335 } else if self.numerator == 0 {
336 write!(f, "1")
337 } else {
338 write!(f, "{}^({}/{})", self.base, self.numerator, self.denominator)
339 }
340 }
341}
342
343impl From<(UnsignedIntegerType, UnsignedIntegerType)> for Fraction {
344 fn from(frac: (UnsignedIntegerType, UnsignedIntegerType)) -> Self {
345 Self::new(frac.0, frac.1)
346 }
347}
348
349impl
350 From<(
351 UnsignedIntegerType,
352 UnsignedIntegerType,
353 UnsignedIntegerType,
354 )> for Fraction
355{
356 fn from(
357 frac: (
358 UnsignedIntegerType,
359 UnsignedIntegerType,
360 UnsignedIntegerType,
361 ),
362 ) -> Self {
363 Self::new_with_base(frac.0, frac.1, frac.2)
364 }
365}
366
367#[derive(Clone, Copy, Debug, Eq, PartialEq)]
368#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
369#[must_use]
371pub enum TuningSystem {
372 EqualTemperament {
374 octave_size: UnsignedIntegerType,
376 },
377 WholeTone,
379 QuarterTone,
381
382 CarlosHarmonic,
388 CarlosHarmonic24,
390 PythagoreanTuning,
392
393 FiveLimit,
395 ElevenLimit,
397
398 FortyThreeTone,
400
401 Javanese,
404 Thai,
406 PtolemyIntenseDiatonic,
411 IndianAlt,
413 Indian22,
415
416 QuarterCommaMeantone,
419 WerckmeisterIII,
421 Rameau,
423 KirnbergerIII,
425 Vallotti,
427 YoungII,
429 ThirdCommaMeantone,
431 SixthCommaMeantone,
433 WerckmeisterIV,
435 WerckmeisterV,
437 KirnbergerI,
439 NeidhardtI,
441 Silbermann,
443 LehmanBach,
445}
446
447impl TuningSystem {
448 pub fn id(self) -> &'static str {
450 match self {
451 Self::EqualTemperament { .. } => "EqualTemperament",
452 Self::WholeTone => "WholeTone",
453 Self::QuarterTone => "QuarterTone",
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 }
481
482 pub fn display_name(self) -> &'static str {
484 match self {
485 Self::EqualTemperament { .. } => "Equal temperament",
486 Self::WholeTone => "Whole tone",
487 Self::QuarterTone => "Quarter tone",
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 }
515
516 pub fn description(self) -> &'static str {
518 match self {
519 Self::EqualTemperament { .. } => "Twelve equal divisions of the octave.",
520 Self::WholeTone => "Six equal whole-tone steps per octave.",
521 Self::QuarterTone => "Twenty-four equal quarter-tone steps per octave.",
522 Self::CarlosHarmonic => "A twelve-tone harmonic-series scale, reaching the 19-limit.",
523 Self::CarlosHarmonic24 => "A twenty-four-tone harmonic-series scale.",
524 Self::PythagoreanTuning => "A twelve-tone tuning table built from pure fifths.",
525 Self::FiveLimit => "A twelve-tone table using five-limit just ratios.",
526 Self::ElevenLimit => "Harry Partch's twenty-nine-tone 11-limit tonality diamond.",
527 Self::FortyThreeTone => "Harry Partch's forty-three-tone pure scale.",
528 Self::Javanese => "A five-tone Javanese equal-temperament approximation.",
529 Self::Thai => "A seven-tone Thai equal-temperament approximation.",
530 Self::PtolemyIntenseDiatonic => {
531 "Ptolemy's intense diatonic, also Zarlino's just major scale."
532 }
533 Self::IndianAlt => "The Indian Sa-grama mode, the inverse of Didymus' diatonic.",
534 Self::Indian22 => "The twenty-two-shruti Indian scale.",
535 Self::QuarterCommaMeantone => {
536 "A twelve-tone quarter-comma meantone temperament (Aaron, 1523)."
537 }
538 Self::WerckmeisterIII => "A twelve-tone Werckmeister III well temperament (1681).",
539 Self::Rameau => "A twelve-tone Rameau modified meantone temperament (1725).",
540 Self::KirnbergerIII => "A twelve-tone Kirnberger III well temperament (1744).",
541 Self::Vallotti => "A twelve-tone Vallotti well temperament (c. 1754).",
542 Self::YoungII => "A twelve-tone Thomas Young well temperament no. 2 (1799).",
543 Self::ThirdCommaMeantone => {
544 "A twelve-tone third-comma meantone temperament (Salinas, 1577)."
545 }
546 Self::SixthCommaMeantone => {
547 "A twelve-tone sixth-comma meantone temperament (Salinas, 1577)."
548 }
549 Self::WerckmeisterIV => "A twelve-tone Werckmeister IV well temperament (1681).",
550 Self::WerckmeisterV => "A twelve-tone Werckmeister V well temperament (1681).",
551 Self::KirnbergerI => "A twelve-tone Kirnberger I well temperament (1766).",
552 Self::NeidhardtI => "A twelve-tone Neidhardt I well temperament (1724).",
553 Self::Silbermann => "A twelve-tone Gottfried Silbermann temperament no. 1 (c. 1730).",
554 Self::LehmanBach => "A twelve-tone Lehman-Bach temperament (2005).",
555 }
556 }
557
558 pub fn ratio(self, index: usize) -> FloatType {
560 get_ratio(self, index, None)
561 }
562
563 pub fn fraction(self, index: usize) -> Fraction {
565 get_fraction(self, index, None)
566 }
567
568 pub fn label(self, index: UnsignedIntegerType) -> String {
570 get_label(self, index, None)
571 }
572
573 pub fn octave(self, index: UnsignedIntegerType) -> UnsignedIntegerType {
575 index / self.octave_size()
576 }
577
578 pub fn frequency(self, index: UnsignedIntegerType) -> FloatType {
580 get_frequency(self, index, None)
581 }
582
583 pub fn frequency_at(self, index: FloatType) -> FloatType {
585 get_frequency_at(self, index, None)
586 }
587
588 pub fn cents(self, index: UnsignedIntegerType) -> FloatType {
590 get_cents(self, index, None)
591 }
592
593 pub fn cents_at(self, index: FloatType) -> FloatType {
595 get_cents_at(self, index, None)
596 }
597
598 pub fn octave_size(self) -> UnsignedIntegerType {
600 match self {
601 Self::EqualTemperament { octave_size } => octave_size,
602 Self::WholeTone => 6,
603 Self::QuarterTone | Self::CarlosHarmonic24 => 24,
604 Self::FortyThreeTone => 43,
605 Self::ElevenLimit => 29,
606 Self::Javanese => 5,
607 Self::Thai | Self::PtolemyIntenseDiatonic | Self::IndianAlt => 7,
608 Self::Indian22 => 22,
609 Self::QuarterCommaMeantone
610 | Self::WerckmeisterIII
611 | Self::Rameau
612 | Self::KirnbergerIII
613 | Self::Vallotti
614 | Self::YoungII
615 | Self::ThirdCommaMeantone
616 | Self::SixthCommaMeantone
617 | Self::WerckmeisterIV
618 | Self::WerckmeisterV
619 | Self::KirnbergerI
620 | Self::NeidhardtI
621 | Self::Silbermann
622 | Self::LehmanBach => OCTAVE_SIZE,
623 Self::CarlosHarmonic | Self::PythagoreanTuning | Self::FiveLimit => OCTAVE_SIZE,
624 }
625 }
626
627 fn ratio_table(self) -> Option<&'static [Fraction]> {
628 match self {
629 Self::CarlosHarmonic => Some(&CARLOS_HARMONIC),
630 Self::CarlosHarmonic24 => Some(&CARLOS_HARMONIC_24),
631 Self::PythagoreanTuning => Some(&PYTHAGOREAN_TUNING),
632 Self::FiveLimit => Some(&FIVE_LIMIT),
633 Self::ElevenLimit => Some(&ELEVEN_LIMIT),
634 Self::FortyThreeTone => Some(&FORTY_THREE_TONE),
635 Self::Javanese => Some(&JAVANESE),
636 Self::Thai => Some(&THAI),
637 Self::PtolemyIntenseDiatonic => Some(&PTOLEMY_INTENSE_DIATONIC),
638 Self::IndianAlt => Some(&INDIA_SCALE_ALT),
639 Self::Indian22 => Some(&INDIAN_SCALE_22),
640 Self::QuarterCommaMeantone => Some(&QUARTER_COMMA_MEANTONE),
641 Self::WerckmeisterIII => Some(&WERCKMEISTER_III),
642 Self::Rameau => Some(&RAMEAU),
643 Self::KirnbergerIII => Some(&KIRNBERGER_III),
644 Self::Vallotti => Some(&VALLOTTI),
645 Self::YoungII => Some(&YOUNG_II),
646 Self::ThirdCommaMeantone => Some(&THIRD_COMMA_MEANTONE),
647 Self::SixthCommaMeantone => Some(&SIXTH_COMMA_MEANTONE),
648 Self::WerckmeisterIV => Some(&WERCKMEISTER_IV),
649 Self::WerckmeisterV => Some(&WERCKMEISTER_V),
650 Self::KirnbergerI => Some(&KIRNBERGER_I),
651 Self::NeidhardtI => Some(&NEIDHARDT_I),
652 Self::Silbermann => Some(&SILBERMANN),
653 Self::LehmanBach => Some(&LEHMAN_BACH),
654 Self::EqualTemperament { .. } | Self::WholeTone | Self::QuarterTone => None,
655 }
656 }
657
658 fn degree_label(self, index: UnsignedIntegerType, octave_size: UnsignedIntegerType) -> String {
659 if octave_size == 0 {
660 return default_degree_label(OCTAVE_SIZE, index);
661 }
662
663 let degree = index % octave_size;
664 match self {
665 Self::WholeTone if octave_size == 6 => WHOLE_TONE_NAMES[degree as usize].to_string(),
666 Self::PtolemyIntenseDiatonic | Self::IndianAlt if octave_size == 7 => {
667 INDIAN_SCALE_NAMES[degree as usize].to_string()
668 }
669 _ => default_degree_label(octave_size, index),
670 }
671 }
672}
673
674impl Display for TuningSystem {
675 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
676 f.write_str(self.id())
677 }
678}
679
680impl FromStr for TuningSystem {
681 type Err = Error;
682
683 fn from_str(s: &str) -> Result<Self, Self::Err> {
684 match s {
685 "EqualTemperament" => Ok(Self::EqualTemperament {
686 octave_size: OCTAVE_SIZE,
687 }),
688 "WholeTone" => Ok(Self::WholeTone),
689 "QuarterTone" => Ok(Self::QuarterTone),
690 "CarlosHarmonic" => Ok(Self::CarlosHarmonic),
691 "CarlosHarmonic24" => Ok(Self::CarlosHarmonic24),
692 "PythagoreanTuning" => Ok(Self::PythagoreanTuning),
693 "FiveLimit" => Ok(Self::FiveLimit),
694 "ElevenLimit" => Ok(Self::ElevenLimit),
695 "FortyThreeTone" => Ok(Self::FortyThreeTone),
696 "Javanese" => Ok(Self::Javanese),
697 "Thai" => Ok(Self::Thai),
698 "PtolemyIntenseDiatonic" => Ok(Self::PtolemyIntenseDiatonic),
699 "IndianAlt" => Ok(Self::IndianAlt),
700 "Indian22" => Ok(Self::Indian22),
701 "QuarterCommaMeantone" => Ok(Self::QuarterCommaMeantone),
702 "WerckmeisterIII" => Ok(Self::WerckmeisterIII),
703 "Rameau" => Ok(Self::Rameau),
704 "KirnbergerIII" => Ok(Self::KirnbergerIII),
705 "Vallotti" => Ok(Self::Vallotti),
706 "YoungII" => Ok(Self::YoungII),
707 "ThirdCommaMeantone" => Ok(Self::ThirdCommaMeantone),
708 "SixthCommaMeantone" => Ok(Self::SixthCommaMeantone),
709 "WerckmeisterIV" => Ok(Self::WerckmeisterIV),
710 "WerckmeisterV" => Ok(Self::WerckmeisterV),
711 "KirnbergerI" => Ok(Self::KirnbergerI),
712 "NeidhardtI" => Ok(Self::NeidhardtI),
713 "Silbermann" => Ok(Self::Silbermann),
714 "LehmanBach" => Ok(Self::LehmanBach),
715 _ => Err(Error::TuningSystem(format!("unknown tuning system {s:?}"))),
716 }
717 }
718}
719
720pub fn equal_temperament(tone: UnsignedIntegerType, octave_size: UnsignedIntegerType) -> Fraction {
722 Fraction::new_with_base(tone, octave_size, 2)
723}
724
725pub fn equal_temperament_12(tone: UnsignedIntegerType) -> Fraction {
727 equal_temperament(tone, 12)
728}
729
730pub fn equal_temperament_default(tone: UnsignedIntegerType) -> Fraction {
732 equal_temperament(tone, OCTAVE_SIZE)
733}
734
735pub fn get_ratio(
737 tuning_system: TuningSystem,
738 index: usize,
739 size: Option<UnsignedIntegerType>,
740) -> FloatType {
741 get_fraction(tuning_system, index, size).into()
742}
743
744pub fn get_fraction(
749 tuning_system: TuningSystem,
750 index: usize,
751 size: Option<UnsignedIntegerType>,
752) -> Fraction {
753 match tuning_system {
754 TuningSystem::EqualTemperament { octave_size } => equal_temperament(
755 index_to_unsigned_integer(index),
756 size.unwrap_or(octave_size),
757 ),
758 TuningSystem::WholeTone => {
759 equal_temperament(index_to_unsigned_integer(index), size.unwrap_or(6))
760 }
761 TuningSystem::QuarterTone => {
762 equal_temperament(index_to_unsigned_integer(index), size.unwrap_or(24))
763 }
764 _ => get_fraction_from_table(tuning_system, index),
765 }
766}
767
768pub fn get_label(
773 tuning_system: TuningSystem,
774 index: UnsignedIntegerType,
775 size: Option<UnsignedIntegerType>,
776) -> String {
777 let octave_size = size.unwrap_or_else(|| tuning_system.octave_size());
778 assert!(octave_size > 0, "octave_size must be greater than zero");
779 degree_name_with_octave(
780 &tuning_system.degree_label(index, octave_size),
781 index / octave_size,
782 )
783}
784
785pub fn get_frequency(
790 tuning_system: TuningSystem,
791 index: UnsignedIntegerType,
792 size: Option<UnsignedIntegerType>,
793) -> FloatType {
794 get_frequency_at(tuning_system, FloatType::from(index), size)
795}
796
797pub fn get_frequency_at(
802 tuning_system: TuningSystem,
803 index: FloatType,
804 size: Option<UnsignedIntegerType>,
805) -> FloatType {
806 CN1 * get_ratio_at(tuning_system, index, size)
807}
808
809fn get_ratio_at(
810 tuning_system: TuningSystem,
811 index: FloatType,
812 size: Option<UnsignedIntegerType>,
813) -> FloatType {
814 assert!(index.is_finite(), "degree index must be finite");
815 let octave_size = size.unwrap_or_else(|| tuning_system.octave_size());
816 assert!(octave_size > 0, "octave_size must be greater than zero");
817
818 if tuning_system.ratio_table().is_none() {
819 return (2.0 as FloatType).powf(index / FloatType::from(octave_size));
820 }
821
822 let base_index = index.floor() as IntegerType;
823 let fractional_degree = index - FloatType::from(base_index);
824 get_ratio_at_integer_index(tuning_system, base_index)
825 * (2.0 as FloatType).powf(fractional_degree / FloatType::from(octave_size))
826}
827
828pub fn get_cents(
833 tuning_system: TuningSystem,
834 index: UnsignedIntegerType,
835 size: Option<UnsignedIntegerType>,
836) -> FloatType {
837 get_cents_at(tuning_system, FloatType::from(index), size)
838}
839
840pub fn get_cents_at(
845 tuning_system: TuningSystem,
846 index: FloatType,
847 size: Option<UnsignedIntegerType>,
848) -> FloatType {
849 let octave_size = size.unwrap_or_else(|| tuning_system.octave_size());
850 assert!(octave_size > 0, "octave_size must be greater than zero");
851 let reference_freq = get_frequency_at(
852 TuningSystem::EqualTemperament { octave_size },
853 index,
854 Some(octave_size),
855 );
856 let comparison_freq = get_frequency_at(tuning_system, index, size);
857 1200.0 * (comparison_freq / reference_freq).log2()
858}
859
860fn get_fraction_from_table(tuning_system: TuningSystem, index: usize) -> Fraction {
861 let table = tuning_system
862 .ratio_table()
863 .expect("tuning system does not have a ratio table");
864 let len = table.len();
865 let octaves = (index / len) as UnsignedIntegerType;
866 table[index % len].with_octaves(octaves)
867}
868
869fn get_ratio_at_integer_index(tuning_system: TuningSystem, index: IntegerType) -> FloatType {
870 let table = tuning_system
871 .ratio_table()
872 .expect("tuning system does not have a ratio table");
873 let len = IntegerType::try_from(table.len()).expect("ratio table length exceeds i32 range");
874 let octave = index.div_euclid(len);
875 let degree = index.rem_euclid(len) as usize;
876 table[degree].ratio() * (2.0 as FloatType).powi(octave)
877}
878
879fn index_to_unsigned_integer(index: usize) -> UnsignedIntegerType {
880 UnsignedIntegerType::try_from(index).expect("tone index exceeds u32 range")
881}
882
883fn default_degree_label(octave_size: UnsignedIntegerType, index: UnsignedIntegerType) -> String {
884 if octave_size == OCTAVE_SIZE {
885 TWELVE_TONE_NAMES[(index % OCTAVE_SIZE) as usize].to_string()
886 } else {
887 format!("T{}", index % octave_size)
888 }
889}
890
891fn degree_name_with_octave(degree_label: &str, octave: UnsignedIntegerType) -> String {
892 let adjusted_octave = i64::from(octave) - 1;
893 let generic_degree_label = degree_label
894 .strip_prefix('T')
895 .is_some_and(|rest| !rest.is_empty() && rest.chars().all(|ch| ch.is_ascii_digit()));
896
897 if generic_degree_label {
898 return if adjusted_octave < 0 {
899 format!("{degree_label}ON{}", -adjusted_octave)
900 } else {
901 format!("{degree_label}O{adjusted_octave}")
902 };
903 }
904
905 if adjusted_octave < 0 {
906 format!("{degree_label}N{}", -adjusted_octave)
907 } else {
908 format!("{degree_label}{adjusted_octave}")
909 }
910}
911
912pub const FORTYTHREE_TONE: [Fraction; 43] = FORTY_THREE_TONE;
914
915pub const JAVANESE: [Fraction; 5] = [
917 Fraction::new_with_base(0, 5, 2),
918 Fraction::new_with_base(1, 5, 2),
919 Fraction::new_with_base(2, 5, 2),
920 Fraction::new_with_base(3, 5, 2),
921 Fraction::new_with_base(4, 5, 2),
922];
923
924pub const THAI: [Fraction; 7] = [
926 Fraction::new_with_base(0, 7, 2),
927 Fraction::new_with_base(1, 7, 2),
928 Fraction::new_with_base(2, 7, 2),
929 Fraction::new_with_base(3, 7, 2),
930 Fraction::new_with_base(4, 7, 2),
931 Fraction::new_with_base(5, 7, 2),
932 Fraction::new_with_base(6, 7, 2),
933];
934
935pub const INDIAN_SCALE_NAMES: [&str; 7] = ["Sa", "Re", "Ga", "Ma", "Pa", "Dha", "Ni"];
937
938#[cfg(test)]
939mod tests {
940 use super::*;
941
942 #[test]
943 fn every_system_answers_cents_and_a_fraction_for_its_degrees() {
944 for system in ALL_TUNING_SYSTEMS {
945 let unison = system.fraction(0);
946 assert!(unison.numerator == 0 || unison.numerator == unison.denominator);
947 assert_eq!(system.cents(0), 0.0);
948 assert!(system.cents(system.octave_size()).abs() < 1e-9);
949 }
950 }
951
952 fn cents_at_degree(system: TuningSystem, degree: usize) -> FloatType {
954 1200.0 * system.ratio(degree).log2()
955 }
956
957 #[test]
958 fn meantone_fifths_match_their_comma_fractions() {
959 const PURE_FIFTH: FloatType = 701.955;
963 const SYNTONIC_COMMA: FloatType = 21.506;
964 for (system, divisor) in [
965 (TuningSystem::ThirdCommaMeantone, 3.0),
966 (TuningSystem::QuarterCommaMeantone, 4.0),
967 (TuningSystem::SixthCommaMeantone, 6.0),
968 ] {
969 let expected = PURE_FIFTH - SYNTONIC_COMMA / divisor;
970 let actual = cents_at_degree(system, 7);
971 assert!(
972 (actual - expected).abs() < 0.01,
973 "{} fifth: expected {expected:.3}, got {actual:.3}",
974 system.display_name()
975 );
976 }
977 }
978
979 #[test]
980 fn kirnberger_i_keeps_its_pure_fifth_and_third() {
981 assert!((cents_at_degree(TuningSystem::KirnbergerI, 7) - 701.955).abs() < 0.01);
984 assert!((cents_at_degree(TuningSystem::KirnbergerI, 4) - 386.314).abs() < 0.01);
985 }
986
987 #[test]
988 fn common_equal_temperaments_divide_the_octave_evenly() {
989 for system in COMMON_EQUAL_TEMPERAMENTS {
990 let size = system.octave_size();
991 let step = 1200.0 / FloatType::from(size);
992 for degree in 0..size as usize {
993 let expected = step * degree as FloatType;
994 let actual = cents_at_degree(system, degree);
995 assert!(
996 (actual - expected).abs() < 0.001,
997 "{size}-EDO degree {degree}: expected {expected:.3}, got {actual:.3}"
998 );
999 }
1000 }
1001 }
1002
1003 #[test]
1004 fn historical_temperaments_match_their_published_values() {
1005 let cases = [
1009 (TuningSystem::QuarterCommaMeantone, 386.314, 696.578),
1010 (TuningSystem::WerckmeisterIII, 390.225, 696.090),
1011 (TuningSystem::Rameau, 386.314, 696.578),
1012 (TuningSystem::KirnbergerIII, 386.314, 696.578),
1013 (TuningSystem::Vallotti, 392.180, 698.045),
1014 (TuningSystem::YoungII, 392.180, 698.045),
1015 ];
1016
1017 for (system, major_third, fifth) in cases {
1018 let actual_third = cents_at_degree(system, 4);
1019 let actual_fifth = cents_at_degree(system, 7);
1020 assert!(
1021 (actual_third - major_third).abs() < 0.001,
1022 "{} major third: expected {major_third}, got {actual_third}",
1023 system.display_name()
1024 );
1025 assert!(
1026 (actual_fifth - fifth).abs() < 0.001,
1027 "{} fifth: expected {fifth}, got {actual_fifth}",
1028 system.display_name()
1029 );
1030 }
1031 }
1032
1033 #[test]
1034 fn quarter_comma_meantone_and_kirnberger_have_a_pure_major_third() {
1035 for system in [
1044 TuningSystem::QuarterCommaMeantone,
1045 TuningSystem::KirnbergerIII,
1046 ] {
1047 let cents = cents_at_degree(system, 4);
1048 assert!(
1049 (cents - 386.313_714).abs() < 0.001,
1050 "{} major third should be the just 386.314 cents, got {cents}",
1051 system.display_name()
1052 );
1053 }
1054 }
1055
1056 #[test]
1057 fn every_historical_temperament_is_wired_up() {
1058 for system in HISTORICAL_TEMPERAMENTS {
1059 assert_eq!(system.octave_size(), OCTAVE_SIZE, "{system:?}");
1060 assert!(system.ratio_table().is_some(), "{system:?} has no table");
1061 assert_eq!(system.ratio_table().unwrap().len(), 12, "{system:?}");
1062 assert!(!system.description().is_empty(), "{system:?}");
1063 assert_eq!(
1064 TuningSystem::from_str(system.id()).unwrap(),
1065 system,
1066 "{system:?} does not round-trip through its id"
1067 );
1068 assert!(
1069 ALL_TUNING_SYSTEMS.contains(&system),
1070 "{system:?} missing from ALL_TUNING_SYSTEMS"
1071 );
1072 }
1073 }
1074
1075 #[test]
1076 fn equal_temperament_degree_helpers_work_without_tone_objects() {
1077 assert_eq!(
1078 TuningSystem::EqualTemperament { octave_size: 12 }.label(0),
1079 "CN1"
1080 );
1081 assert_eq!(
1082 TuningSystem::EqualTemperament { octave_size: 12 }.octave(0),
1083 0
1084 );
1085 assert!(
1086 (TuningSystem::EqualTemperament { octave_size: 12 }.frequency(0) - CN1).abs() < 1e-12
1087 );
1088
1089 assert_eq!(
1090 TuningSystem::EqualTemperament { octave_size: 12 }.label(69),
1091 "A4"
1092 );
1093 assert_eq!(
1094 TuningSystem::EqualTemperament { octave_size: 12 }.octave(69),
1095 5
1096 );
1097 assert!(
1098 (TuningSystem::EqualTemperament { octave_size: 12 }.frequency(69) - 440.0).abs()
1099 < 0.0001
1100 );
1101 }
1102
1103 #[test]
1104 fn fractional_frequency_helpers_support_pitch_space_values() {
1105 let equal = TuningSystem::EqualTemperament {
1106 octave_size: OCTAVE_SIZE,
1107 };
1108 assert!((equal.frequency_at(69.0) - A4).abs() < 0.0001);
1109 assert!((equal.frequency_at(60.0) - C4).abs() < 0.0001);
1110 assert!((TuningSystem::FiveLimit.frequency_at(64.0) - (C4 * 5.0 / 4.0)).abs() < 0.0001);
1111 assert!(
1112 (TuningSystem::PythagoreanTuning.frequency_at(67.0) - (C4 * 3.0 / 2.0)).abs() < 0.0001
1113 );
1114 assert!(TuningSystem::FiveLimit.cents_at(64.0) < -13.0);
1115 }
1116
1117 #[test]
1118 fn ratio_helpers_cover_octaves() {
1119 let two_one: FloatType = Fraction::new(2, 1).into();
1120 assert_eq!(get_ratio(TuningSystem::CarlosHarmonic, 12, None), two_one);
1121 assert_eq!(get_ratio(TuningSystem::CarlosHarmonic24, 24, None), two_one);
1122 assert_eq!(
1123 get_ratio(
1124 TuningSystem::EqualTemperament {
1125 octave_size: OCTAVE_SIZE,
1126 },
1127 12,
1128 None,
1129 ),
1130 two_one
1131 );
1132 }
1133
1134 #[test]
1135 fn fraction_helpers_cover_rational_and_exponential_forms() {
1136 let rational = Fraction::from((3, 2));
1137 assert_eq!(rational.numerator(), 3);
1138 assert_eq!(rational.denominator(), 2);
1139 assert_eq!(rational.base(), 0);
1140 assert_eq!(rational.ratio(), 1.5);
1141 assert_eq!(rational.label(), "3/2");
1142 assert_eq!(rational.with_octaves(2), Fraction::new(12, 2));
1143
1144 let exponential = Fraction::from((7, 12, 2));
1145 assert_eq!(exponential.label(), "2^(7/12)");
1146 assert_eq!(
1147 exponential.with_octaves(1),
1148 Fraction::new_with_base(19, 12, 2)
1149 );
1150 assert!((exponential.ratio() - 2.0_f64.powf(7.0 / 12.0)).abs() < 1e-12);
1151 }
1152
1153 #[test]
1154 fn free_tuning_helpers_accept_size_overrides() {
1155 let system = TuningSystem::EqualTemperament { octave_size: 12 };
1156 assert_eq!(equal_temperament_12(12), Fraction::new_with_base(12, 12, 2));
1157 assert_eq!(
1158 equal_temperament_default(3),
1159 Fraction::new_with_base(3, OCTAVE_SIZE, 2)
1160 );
1161 assert_eq!(
1162 get_fraction(system, 6, Some(24)),
1163 Fraction::new_with_base(6, 24, 2)
1164 );
1165 assert_eq!(get_label(system, 24, Some(24)), "T0O0");
1166 assert!((get_frequency(system, 12, Some(24)) - CN1 * 2.0_f64.sqrt()).abs() < 1e-10);
1167 assert_eq!(get_cents(system, 12, Some(24)), 0.0);
1168 }
1169
1170 #[test]
1171 fn current_tuning_system_variants_return_ratios() {
1172 assert_eq!(TuningSystem::WholeTone.ratio(6), 2.0);
1173 assert_eq!(TuningSystem::QuarterTone.ratio(24), 2.0);
1174 assert_eq!(TuningSystem::PythagoreanTuning.ratio(7), 1.5);
1175 assert_eq!(TuningSystem::Indian22.ratio(22), 2.0);
1176 }
1177
1178 #[test]
1179 fn table_ratios_shift_by_real_octaves() {
1180 assert_eq!(TuningSystem::CarlosHarmonic.ratio(19), 3.0);
1181 assert_eq!(TuningSystem::FortyThreeTone.ratio(68), 3.0);
1182 assert_eq!(TuningSystem::PtolemyIntenseDiatonic.ratio(8), 2.25);
1183 }
1184
1185 #[test]
1186 fn non_twelve_tone_systems_keep_system_octaves_and_labels() {
1187 assert_eq!(TuningSystem::WholeTone.label(1), "DN1");
1188 assert_eq!(TuningSystem::WholeTone.octave_size(), 6);
1189 assert!(
1190 (TuningSystem::WholeTone.ratio(1) - (2.0 as FloatType).powf(1.0 / 6.0)).abs() < 1e-12
1191 );
1192
1193 assert_eq!(TuningSystem::QuarterTone.label(13), "T13ON1");
1194 assert_eq!(TuningSystem::QuarterTone.octave_size(), 24);
1195 assert!(
1196 (TuningSystem::QuarterTone.ratio(13) - (2.0 as FloatType).powf(13.0 / 24.0)).abs()
1197 < 1e-12
1198 );
1199
1200 assert_eq!(TuningSystem::Thai.label(7), "T0O0");
1201 assert_eq!(TuningSystem::Thai.octave_size(), 7);
1202 assert_eq!(TuningSystem::Thai.ratio(7), 2.0);
1203
1204 assert_eq!(TuningSystem::PtolemyIntenseDiatonic.label(8), "Re0");
1205 assert_eq!(TuningSystem::PtolemyIntenseDiatonic.octave_size(), 7);
1206 assert_eq!(TuningSystem::PtolemyIntenseDiatonic.ratio(8), 2.25);
1207
1208 assert_eq!(TuningSystem::FortyThreeTone.label(68), "T25O0");
1209 assert_eq!(TuningSystem::FortyThreeTone.octave_size(), 43);
1210 assert_eq!(TuningSystem::FortyThreeTone.ratio(68), 3.0);
1211 }
1212
1213 #[test]
1214 fn tuning_system_display_and_parse_are_canonical() {
1215 let system = TuningSystem::FiveLimit;
1216 assert_eq!(system.id(), "FiveLimit");
1217 assert_eq!(system.to_string(), "FiveLimit");
1218 assert_eq!("FiveLimit".parse::<TuningSystem>().unwrap(), system);
1219
1220 let err = "not-a-system".parse::<TuningSystem>().unwrap_err();
1221 assert_eq!(
1222 err,
1223 Error::TuningSystem("unknown tuning system \"not-a-system\"".to_string())
1224 );
1225 }
1226
1227 #[test]
1228 fn tuning_system_display_names_cover_variants() {
1229 for system in ALL_TUNING_SYSTEMS {
1230 assert!(!system.id().is_empty());
1231 assert!(!system.display_name().is_empty());
1232 assert!(!system.description().is_empty());
1233 assert!(system.octave_size() > 0);
1234 assert_eq!(system.to_string(), system.id());
1235 }
1236 }
1237
1238 #[test]
1239 fn twelve_tone_systems_keep_chromatic_ratios_ascending() {
1240 for system in ALL_TUNING_SYSTEMS
1241 .into_iter()
1242 .filter(|system| system.octave_size() == OCTAVE_SIZE)
1243 {
1244 let mut previous = system.ratio(0);
1245 for degree in 1..=OCTAVE_SIZE {
1246 let ratio = system.ratio(degree as usize);
1247 assert!(
1248 ratio > previous,
1249 "{} degree {degree} ratio {ratio} should be higher than {previous}",
1250 system.id()
1251 );
1252 previous = ratio;
1253 }
1254 }
1255 }
1256
1257 #[test]
1258 fn all_ratio_tables_keep_degrees_strictly_ascending_within_the_octave() {
1259 for system in ALL_TUNING_SYSTEMS {
1260 let octave_size = system.octave_size();
1261 let mut previous = system.ratio(0);
1262 for degree in 1..=octave_size {
1263 let ratio = system.ratio(degree as usize);
1264 assert!(
1265 ratio > previous,
1266 "{} degree {degree} ratio {ratio} should be higher than {previous} \
1267 (a table entry is likely mistranscribed)",
1268 system.id()
1269 );
1270 previous = ratio;
1271 }
1272 }
1273 }
1274}