1pub mod adaptive;
2mod generated;
3pub mod scala;
5#[cfg(feature = "scala-archive")]
6pub mod scala_bundled;
7
8pub use generated::*;
9
10use crate::defaults::{FloatType, IntegerType, UnsignedIntegerType};
11use crate::error::{Error, Result};
12use crate::tuningsystem::adaptive::AdaptiveTuningSystem;
13
14use std::fmt::{Display, Formatter};
15use std::str::FromStr;
16
17pub const OCTAVE_SIZE: UnsignedIntegerType = 12;
19
20pub const C4: FloatType = 261.6256;
22pub const C0: FloatType = C4 / 16.0;
24pub const CN1: FloatType = C4 / 32.0;
26
27pub const A4: FloatType = 440.0;
29pub const A0: FloatType = A4 / 16.0;
31pub const AN1: FloatType = A4 / 32.0;
33
34pub const TWELVE_TONE_NAMES: [&str; 12] = [
36 "C", "C#/Db", "D", "D#/Eb", "E", "F", "F#/Gb", "G", "G#/Ab", "A", "A#/Bb", "B",
37];
38
39pub const TWELVE_TONE_NAMES_SHARP: [&str; 12] = [
41 "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
42];
43
44pub const TWELVE_TONE_NAMES_FLAT: [&str; 12] = [
46 "C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B",
47];
48
49pub const WHOLE_TONE_NAMES: [&str; 6] = ["C", "D", "E", "F#/Gb", "G#/Ab", "A#/Bb"];
51
52pub const COMMON_TWELVE_TONE_TUNING_SYSTEMS: [TuningSystem; 4] = [
54 TuningSystem::EqualTemperament {
55 octave_size: OCTAVE_SIZE,
56 },
57 TuningSystem::CarlosHarmonic,
58 TuningSystem::PythagoreanTuning,
59 TuningSystem::FiveLimit,
60];
61
62pub const COMMON_EQUAL_TEMPERAMENTS: [TuningSystem; 8] = [
70 TuningSystem::EqualTemperament { octave_size: 12 },
71 TuningSystem::EqualTemperament { octave_size: 19 },
72 TuningSystem::EqualTemperament { octave_size: 22 },
73 TuningSystem::EqualTemperament { octave_size: 24 },
74 TuningSystem::EqualTemperament { octave_size: 31 },
75 TuningSystem::EqualTemperament { octave_size: 41 },
76 TuningSystem::EqualTemperament { octave_size: 53 },
77 TuningSystem::EqualTemperament { octave_size: 72 },
78];
79
80pub const HISTORICAL_TEMPERAMENTS: [TuningSystem; 14] = [
86 TuningSystem::QuarterCommaMeantone,
87 TuningSystem::WerckmeisterIII,
88 TuningSystem::Rameau,
89 TuningSystem::KirnbergerIII,
90 TuningSystem::Vallotti,
91 TuningSystem::YoungII,
92 TuningSystem::ThirdCommaMeantone,
93 TuningSystem::SixthCommaMeantone,
94 TuningSystem::WerckmeisterIV,
95 TuningSystem::WerckmeisterV,
96 TuningSystem::KirnbergerI,
97 TuningSystem::NeidhardtI,
98 TuningSystem::Silbermann,
99 TuningSystem::LehmanBach,
100];
101
102#[derive(Clone, Copy, Debug, Eq, PartialEq)]
104#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
105pub enum AnyTuningSystem {
106 Fixed(TuningSystem),
107 Adaptive(AdaptiveTuningSystem),
108}
109
110impl AnyTuningSystem {
111 pub fn frequency_at(
112 self,
113 context: FloatType,
114 index: FloatType,
115 size: Option<UnsignedIntegerType>,
116 ) -> FloatType {
117 match self {
118 Self::Fixed(tuning_system) => {
119 let _ = context;
120 get_frequency_at(tuning_system, index, size)
121 }
122 Self::Adaptive(adaptive_tuning_system) => {
123 adaptive_tuning_system.frequency_at(context, index, size)
124 }
125 }
126 }
127
128 pub fn cents_at(
129 self,
130 context: FloatType,
131 index: FloatType,
132 size: Option<UnsignedIntegerType>,
133 ) -> FloatType {
134 match self {
135 Self::Fixed(tuning_system) => {
136 let _ = context;
137 tuning_system.cents_at(index)
138 }
139 Self::Adaptive(adaptive_tuning_system) => {
140 adaptive_tuning_system.cents_at(context, index, size)
141 }
142 }
143 }
144
145 pub fn is_adaptive(self) -> bool {
146 matches!(self, Self::Adaptive(_))
147 }
148}
149
150impl From<TuningSystem> for AnyTuningSystem {
151 fn from(tuning_system: TuningSystem) -> Self {
152 Self::Fixed(tuning_system)
153 }
154}
155
156impl From<AdaptiveTuningSystem> for AnyTuningSystem {
157 fn from(adaptive_tuning_system: AdaptiveTuningSystem) -> Self {
158 Self::Adaptive(adaptive_tuning_system)
159 }
160}
161
162pub const ALL_TUNING_SYSTEMS: [TuningSystem; 28] = [
164 TuningSystem::EqualTemperament {
165 octave_size: OCTAVE_SIZE,
166 },
167 TuningSystem::WholeTone,
168 TuningSystem::QuarterTone,
169 TuningSystem::CarlosHarmonic,
170 TuningSystem::CarlosHarmonic24,
171 TuningSystem::PythagoreanTuning,
172 TuningSystem::FiveLimit,
173 TuningSystem::ElevenLimit,
174 TuningSystem::FortyThreeTone,
175 TuningSystem::Javanese,
176 TuningSystem::Thai,
177 TuningSystem::PtolemyIntenseDiatonic,
178 TuningSystem::IndianAlt,
179 TuningSystem::Indian22,
180 TuningSystem::QuarterCommaMeantone,
181 TuningSystem::WerckmeisterIII,
182 TuningSystem::Rameau,
183 TuningSystem::KirnbergerIII,
184 TuningSystem::Vallotti,
185 TuningSystem::YoungII,
186 TuningSystem::ThirdCommaMeantone,
187 TuningSystem::SixthCommaMeantone,
188 TuningSystem::WerckmeisterIV,
189 TuningSystem::WerckmeisterV,
190 TuningSystem::KirnbergerI,
191 TuningSystem::NeidhardtI,
192 TuningSystem::Silbermann,
193 TuningSystem::LehmanBach,
194];
195
196#[derive(Clone, Copy, Debug, Eq, PartialEq)]
197#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
198pub struct Fraction {
200 pub numerator: UnsignedIntegerType,
202 pub denominator: UnsignedIntegerType,
204 pub base: UnsignedIntegerType,
206}
207
208impl Fraction {
209 pub const fn new(numerator: UnsignedIntegerType, denominator: UnsignedIntegerType) -> Self {
211 Self::new_with_base(numerator, denominator, 0)
212 }
213
214 pub const fn new_with_base(
216 numerator: UnsignedIntegerType,
217 denominator: UnsignedIntegerType,
218 base: UnsignedIntegerType,
219 ) -> Self {
220 Self {
221 numerator,
222 denominator,
223 base,
224 }
225 }
226
227 pub const fn numerator(&self) -> UnsignedIntegerType {
229 self.numerator
230 }
231
232 pub const fn denominator(&self) -> UnsignedIntegerType {
234 self.denominator
235 }
236
237 pub const fn base(&self) -> UnsignedIntegerType {
239 self.base
240 }
241
242 pub fn ratio(self) -> FloatType {
244 self.into()
245 }
246
247 pub fn label(self) -> String {
249 self.to_string()
250 }
251
252 pub fn with_octaves(mut self, octaves: UnsignedIntegerType) -> Self {
254 if octaves == 0 {
255 return self;
256 }
257
258 if self.base == 0 {
259 let multiplier = (2 as UnsignedIntegerType)
260 .checked_pow(octaves)
261 .expect("octave multiplier exceeds u32 range");
262 self.numerator = self
263 .numerator
264 .checked_mul(multiplier)
265 .expect("fraction numerator exceeds u32 range");
266 } else {
267 let octave_offset = self
268 .denominator
269 .checked_mul(octaves)
270 .expect("fraction octave offset exceeds u32 range");
271 self.numerator = self
272 .numerator
273 .checked_add(octave_offset)
274 .expect("fraction numerator exceeds u32 range");
275 }
276
277 self
278 }
279}
280
281impl From<Fraction> for FloatType {
282 fn from(frac: Fraction) -> Self {
283 if frac.base == 0 {
284 frac.numerator as FloatType / frac.denominator as FloatType
285 } else {
286 (frac.base as FloatType)
287 .powf(frac.numerator as FloatType / frac.denominator as FloatType)
288 }
289 }
290}
291
292impl Display for Fraction {
293 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
294 if self.base == 0 {
295 if self.denominator == 1 {
296 write!(f, "{}", self.numerator)
297 } else {
298 write!(f, "{}/{}", self.numerator, self.denominator)
299 }
300 } else if self.numerator == 0 {
301 write!(f, "1")
302 } else {
303 write!(f, "{}^({}/{})", self.base, self.numerator, self.denominator)
304 }
305 }
306}
307
308impl From<(UnsignedIntegerType, UnsignedIntegerType)> for Fraction {
309 fn from(frac: (UnsignedIntegerType, UnsignedIntegerType)) -> Self {
310 Self::new(frac.0, frac.1)
311 }
312}
313
314impl
315 From<(
316 UnsignedIntegerType,
317 UnsignedIntegerType,
318 UnsignedIntegerType,
319 )> for Fraction
320{
321 fn from(
322 frac: (
323 UnsignedIntegerType,
324 UnsignedIntegerType,
325 UnsignedIntegerType,
326 ),
327 ) -> Self {
328 Self::new_with_base(frac.0, frac.1, frac.2)
329 }
330}
331
332#[derive(Clone, Copy, Debug, Eq, PartialEq)]
333#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
334pub enum TuningSystem {
336 EqualTemperament {
338 octave_size: UnsignedIntegerType,
340 },
341 WholeTone,
343 QuarterTone,
345
346 CarlosHarmonic,
352 CarlosHarmonic24,
354 PythagoreanTuning,
356
357 FiveLimit,
359 ElevenLimit,
361
362 FortyThreeTone,
364
365 Javanese,
368 Thai,
370 PtolemyIntenseDiatonic,
375 IndianAlt,
377 Indian22,
379
380 QuarterCommaMeantone,
383 WerckmeisterIII,
385 Rameau,
387 KirnbergerIII,
389 Vallotti,
391 YoungII,
393 ThirdCommaMeantone,
395 SixthCommaMeantone,
397 WerckmeisterIV,
399 WerckmeisterV,
401 KirnbergerI,
403 NeidhardtI,
405 Silbermann,
407 LehmanBach,
409}
410
411impl TuningSystem {
412 pub fn id(self) -> &'static str {
414 match self {
415 Self::EqualTemperament { .. } => "EqualTemperament",
416 Self::WholeTone => "WholeTone",
417 Self::QuarterTone => "QuarterTone",
418 Self::CarlosHarmonic => "CarlosHarmonic",
419 Self::CarlosHarmonic24 => "CarlosHarmonic24",
420 Self::PythagoreanTuning => "PythagoreanTuning",
421 Self::FiveLimit => "FiveLimit",
422 Self::ElevenLimit => "ElevenLimit",
423 Self::FortyThreeTone => "FortyThreeTone",
424 Self::Javanese => "Javanese",
425 Self::Thai => "Thai",
426 Self::PtolemyIntenseDiatonic => "PtolemyIntenseDiatonic",
427 Self::IndianAlt => "IndianAlt",
428 Self::Indian22 => "Indian22",
429 Self::QuarterCommaMeantone => "QuarterCommaMeantone",
430 Self::WerckmeisterIII => "WerckmeisterIII",
431 Self::Rameau => "Rameau",
432 Self::KirnbergerIII => "KirnbergerIII",
433 Self::Vallotti => "Vallotti",
434 Self::YoungII => "YoungII",
435 Self::ThirdCommaMeantone => "ThirdCommaMeantone",
436 Self::SixthCommaMeantone => "SixthCommaMeantone",
437 Self::WerckmeisterIV => "WerckmeisterIV",
438 Self::WerckmeisterV => "WerckmeisterV",
439 Self::KirnbergerI => "KirnbergerI",
440 Self::NeidhardtI => "NeidhardtI",
441 Self::Silbermann => "Silbermann",
442 Self::LehmanBach => "LehmanBach",
443 }
444 }
445
446 pub fn display_name(self) -> &'static str {
448 match self {
449 Self::EqualTemperament { .. } => "Equal temperament",
450 Self::WholeTone => "Whole tone",
451 Self::QuarterTone => "Quarter tone",
452 Self::CarlosHarmonic => "Carlos Harmonic",
453 Self::CarlosHarmonic24 => "Carlos Harmonic 24",
454 Self::PythagoreanTuning => "Pythagorean",
455 Self::FiveLimit => "Five-limit",
456 Self::ElevenLimit => "Partch 11-limit diamond",
457 Self::FortyThreeTone => "Partch 43-tone",
458 Self::Javanese => "Javanese",
459 Self::Thai => "Thai",
460 Self::PtolemyIntenseDiatonic => "Ptolemy intense diatonic",
461 Self::IndianAlt => "Indian Sa-grama",
462 Self::Indian22 => "Indian shruti",
463 Self::QuarterCommaMeantone => "Quarter-comma meantone",
464 Self::WerckmeisterIII => "Werckmeister III",
465 Self::Rameau => "Rameau",
466 Self::KirnbergerIII => "Kirnberger III",
467 Self::Vallotti => "Vallotti",
468 Self::YoungII => "Young II",
469 Self::ThirdCommaMeantone => "Third-comma meantone",
470 Self::SixthCommaMeantone => "Sixth-comma meantone",
471 Self::WerckmeisterIV => "Werckmeister IV",
472 Self::WerckmeisterV => "Werckmeister V",
473 Self::KirnbergerI => "Kirnberger I",
474 Self::NeidhardtI => "Neidhardt I",
475 Self::Silbermann => "Silbermann",
476 Self::LehmanBach => "Lehman-Bach",
477 }
478 }
479
480 pub fn description(self) -> &'static str {
482 match self {
483 Self::EqualTemperament { .. } => "Twelve equal divisions of the octave.",
484 Self::WholeTone => "Six equal whole-tone steps per octave.",
485 Self::QuarterTone => "Twenty-four equal quarter-tone steps per octave.",
486 Self::CarlosHarmonic => "A twelve-tone harmonic-series scale, reaching the 19-limit.",
487 Self::CarlosHarmonic24 => "A twenty-four-tone harmonic-series scale.",
488 Self::PythagoreanTuning => "A twelve-tone tuning table built from pure fifths.",
489 Self::FiveLimit => "A twelve-tone table using five-limit just ratios.",
490 Self::ElevenLimit => "Harry Partch's twenty-nine-tone 11-limit tonality diamond.",
491 Self::FortyThreeTone => "Harry Partch's forty-three-tone pure scale.",
492 Self::Javanese => "A five-tone Javanese equal-temperament approximation.",
493 Self::Thai => "A seven-tone Thai equal-temperament approximation.",
494 Self::PtolemyIntenseDiatonic => {
495 "Ptolemy's intense diatonic, also Zarlino's just major scale."
496 }
497 Self::IndianAlt => "The Indian Sa-grama mode, the inverse of Didymus' diatonic.",
498 Self::Indian22 => "The twenty-two-shruti Indian scale.",
499 Self::QuarterCommaMeantone => {
500 "A twelve-tone quarter-comma meantone temperament (Aaron, 1523)."
501 }
502 Self::WerckmeisterIII => "A twelve-tone Werckmeister III well temperament (1681).",
503 Self::Rameau => "A twelve-tone Rameau modified meantone temperament (1725).",
504 Self::KirnbergerIII => "A twelve-tone Kirnberger III well temperament (1744).",
505 Self::Vallotti => "A twelve-tone Vallotti well temperament (c. 1754).",
506 Self::YoungII => "A twelve-tone Thomas Young well temperament no. 2 (1799).",
507 Self::ThirdCommaMeantone => {
508 "A twelve-tone third-comma meantone temperament (Salinas, 1577)."
509 }
510 Self::SixthCommaMeantone => {
511 "A twelve-tone sixth-comma meantone temperament (Salinas, 1577)."
512 }
513 Self::WerckmeisterIV => "A twelve-tone Werckmeister IV well temperament (1681).",
514 Self::WerckmeisterV => "A twelve-tone Werckmeister V well temperament (1681).",
515 Self::KirnbergerI => "A twelve-tone Kirnberger I well temperament (1766).",
516 Self::NeidhardtI => "A twelve-tone Neidhardt I well temperament (1724).",
517 Self::Silbermann => "A twelve-tone Gottfried Silbermann temperament no. 1 (c. 1730).",
518 Self::LehmanBach => "A twelve-tone Lehman-Bach temperament (2005).",
519 }
520 }
521
522 pub fn ratio(self, index: usize) -> FloatType {
524 get_ratio(self, index, None)
525 }
526
527 pub fn fraction(self, index: usize) -> Fraction {
529 get_fraction(self, index, None)
530 }
531
532 pub fn label(self, index: UnsignedIntegerType) -> String {
534 get_label(self, index, None)
535 }
536
537 pub fn octave(self, index: UnsignedIntegerType) -> UnsignedIntegerType {
539 index / self.octave_size()
540 }
541
542 pub fn frequency(self, index: UnsignedIntegerType) -> FloatType {
544 get_frequency(self, index, None)
545 }
546
547 pub fn frequency_at(self, index: FloatType) -> FloatType {
549 get_frequency_at(self, index, None)
550 }
551
552 pub fn cents(self, index: UnsignedIntegerType) -> FloatType {
554 get_cents(self, index, None)
555 }
556
557 pub fn cents_at(self, index: FloatType) -> FloatType {
559 get_cents_at(self, index, None)
560 }
561
562 pub fn octave_size(self) -> UnsignedIntegerType {
564 match self {
565 Self::EqualTemperament { octave_size } => octave_size,
566 Self::WholeTone => 6,
567 Self::QuarterTone | Self::CarlosHarmonic24 => 24,
568 Self::FortyThreeTone => 43,
569 Self::ElevenLimit => 29,
570 Self::Javanese => 5,
571 Self::Thai | Self::PtolemyIntenseDiatonic | Self::IndianAlt => 7,
572 Self::Indian22 => 22,
573 Self::QuarterCommaMeantone
574 | Self::WerckmeisterIII
575 | Self::Rameau
576 | Self::KirnbergerIII
577 | Self::Vallotti
578 | Self::YoungII
579 | Self::ThirdCommaMeantone
580 | Self::SixthCommaMeantone
581 | Self::WerckmeisterIV
582 | Self::WerckmeisterV
583 | Self::KirnbergerI
584 | Self::NeidhardtI
585 | Self::Silbermann
586 | Self::LehmanBach => OCTAVE_SIZE,
587 Self::CarlosHarmonic | Self::PythagoreanTuning | Self::FiveLimit => OCTAVE_SIZE,
588 }
589 }
590
591 fn ratio_table(self) -> Option<&'static [Fraction]> {
592 match self {
593 Self::CarlosHarmonic => Some(&CARLOS_HARMONIC),
594 Self::CarlosHarmonic24 => Some(&CARLOS_HARMONIC_24),
595 Self::PythagoreanTuning => Some(&PYTHAGOREAN_TUNING),
596 Self::FiveLimit => Some(&FIVE_LIMIT),
597 Self::ElevenLimit => Some(&ELEVEN_LIMIT),
598 Self::FortyThreeTone => Some(&FORTY_THREE_TONE),
599 Self::Javanese => Some(&JAVANESE),
600 Self::Thai => Some(&THAI),
601 Self::PtolemyIntenseDiatonic => Some(&PTOLEMY_INTENSE_DIATONIC),
602 Self::IndianAlt => Some(&INDIA_SCALE_ALT),
603 Self::Indian22 => Some(&INDIAN_SCALE_22),
604 Self::QuarterCommaMeantone => Some(&QUARTER_COMMA_MEANTONE),
605 Self::WerckmeisterIII => Some(&WERCKMEISTER_III),
606 Self::Rameau => Some(&RAMEAU),
607 Self::KirnbergerIII => Some(&KIRNBERGER_III),
608 Self::Vallotti => Some(&VALLOTTI),
609 Self::YoungII => Some(&YOUNG_II),
610 Self::ThirdCommaMeantone => Some(&THIRD_COMMA_MEANTONE),
611 Self::SixthCommaMeantone => Some(&SIXTH_COMMA_MEANTONE),
612 Self::WerckmeisterIV => Some(&WERCKMEISTER_IV),
613 Self::WerckmeisterV => Some(&WERCKMEISTER_V),
614 Self::KirnbergerI => Some(&KIRNBERGER_I),
615 Self::NeidhardtI => Some(&NEIDHARDT_I),
616 Self::Silbermann => Some(&SILBERMANN),
617 Self::LehmanBach => Some(&LEHMAN_BACH),
618 Self::EqualTemperament { .. } | Self::WholeTone | Self::QuarterTone => None,
619 }
620 }
621
622 fn degree_label(self, index: UnsignedIntegerType, octave_size: UnsignedIntegerType) -> String {
623 if octave_size == 0 {
624 return default_degree_label(OCTAVE_SIZE, index);
625 }
626
627 let degree = index % octave_size;
628 match self {
629 Self::WholeTone if octave_size == 6 => WHOLE_TONE_NAMES[degree as usize].to_string(),
630 Self::PtolemyIntenseDiatonic | Self::IndianAlt if octave_size == 7 => {
631 INDIAN_SCALE_NAMES[degree as usize].to_string()
632 }
633 _ => default_degree_label(octave_size, index),
634 }
635 }
636}
637
638impl Display for TuningSystem {
639 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
640 f.write_str(self.id())
641 }
642}
643
644impl FromStr for TuningSystem {
645 type Err = Error;
646
647 fn from_str(s: &str) -> Result<Self, Self::Err> {
648 match s {
649 "EqualTemperament" => Ok(Self::EqualTemperament {
650 octave_size: OCTAVE_SIZE,
651 }),
652 "WholeTone" => Ok(Self::WholeTone),
653 "QuarterTone" => Ok(Self::QuarterTone),
654 "CarlosHarmonic" => Ok(Self::CarlosHarmonic),
655 "CarlosHarmonic24" => Ok(Self::CarlosHarmonic24),
656 "PythagoreanTuning" => Ok(Self::PythagoreanTuning),
657 "FiveLimit" => Ok(Self::FiveLimit),
658 "ElevenLimit" => Ok(Self::ElevenLimit),
659 "FortyThreeTone" => Ok(Self::FortyThreeTone),
660 "Javanese" => Ok(Self::Javanese),
661 "Thai" => Ok(Self::Thai),
662 "PtolemyIntenseDiatonic" => Ok(Self::PtolemyIntenseDiatonic),
663 "IndianAlt" => Ok(Self::IndianAlt),
664 "Indian22" => Ok(Self::Indian22),
665 "QuarterCommaMeantone" => Ok(Self::QuarterCommaMeantone),
666 "WerckmeisterIII" => Ok(Self::WerckmeisterIII),
667 "Rameau" => Ok(Self::Rameau),
668 "KirnbergerIII" => Ok(Self::KirnbergerIII),
669 "Vallotti" => Ok(Self::Vallotti),
670 "YoungII" => Ok(Self::YoungII),
671 "ThirdCommaMeantone" => Ok(Self::ThirdCommaMeantone),
672 "SixthCommaMeantone" => Ok(Self::SixthCommaMeantone),
673 "WerckmeisterIV" => Ok(Self::WerckmeisterIV),
674 "WerckmeisterV" => Ok(Self::WerckmeisterV),
675 "KirnbergerI" => Ok(Self::KirnbergerI),
676 "NeidhardtI" => Ok(Self::NeidhardtI),
677 "Silbermann" => Ok(Self::Silbermann),
678 "LehmanBach" => Ok(Self::LehmanBach),
679 _ => Err(Error::TuningSystem(format!("unknown tuning system {s:?}"))),
680 }
681 }
682}
683
684pub fn equal_temperament(tone: UnsignedIntegerType, octave_size: UnsignedIntegerType) -> Fraction {
686 Fraction::new_with_base(tone, octave_size, 2)
687}
688
689pub fn equal_temperament_12(tone: UnsignedIntegerType) -> Fraction {
691 equal_temperament(tone, 12)
692}
693
694pub fn equal_temperament_default(tone: UnsignedIntegerType) -> Fraction {
696 equal_temperament(tone, OCTAVE_SIZE)
697}
698
699pub fn get_ratio(
701 tuning_system: TuningSystem,
702 index: usize,
703 size: Option<UnsignedIntegerType>,
704) -> FloatType {
705 get_fraction(tuning_system, index, size).into()
706}
707
708pub fn get_fraction(
713 tuning_system: TuningSystem,
714 index: usize,
715 size: Option<UnsignedIntegerType>,
716) -> Fraction {
717 match tuning_system {
718 TuningSystem::EqualTemperament { octave_size } => equal_temperament(
719 index_to_unsigned_integer(index),
720 size.unwrap_or(octave_size),
721 ),
722 TuningSystem::WholeTone => {
723 equal_temperament(index_to_unsigned_integer(index), size.unwrap_or(6))
724 }
725 TuningSystem::QuarterTone => {
726 equal_temperament(index_to_unsigned_integer(index), size.unwrap_or(24))
727 }
728 _ => get_fraction_from_table(tuning_system, index),
729 }
730}
731
732pub fn get_label(
737 tuning_system: TuningSystem,
738 index: UnsignedIntegerType,
739 size: Option<UnsignedIntegerType>,
740) -> String {
741 let octave_size = size.unwrap_or_else(|| tuning_system.octave_size());
742 assert!(octave_size > 0, "octave_size must be greater than zero");
743 degree_name_with_octave(
744 &tuning_system.degree_label(index, octave_size),
745 index / octave_size,
746 )
747}
748
749pub fn get_frequency(
754 tuning_system: TuningSystem,
755 index: UnsignedIntegerType,
756 size: Option<UnsignedIntegerType>,
757) -> FloatType {
758 get_frequency_at(tuning_system, FloatType::from(index), size)
759}
760
761pub fn get_frequency_at(
766 tuning_system: TuningSystem,
767 index: FloatType,
768 size: Option<UnsignedIntegerType>,
769) -> FloatType {
770 CN1 * get_ratio_at(tuning_system, index, size)
771}
772
773fn get_ratio_at(
774 tuning_system: TuningSystem,
775 index: FloatType,
776 size: Option<UnsignedIntegerType>,
777) -> FloatType {
778 assert!(index.is_finite(), "degree index must be finite");
779 let octave_size = size.unwrap_or_else(|| tuning_system.octave_size());
780 assert!(octave_size > 0, "octave_size must be greater than zero");
781
782 if tuning_system.ratio_table().is_none() {
783 return (2.0 as FloatType).powf(index / FloatType::from(octave_size));
784 }
785
786 let base_index = index.floor() as IntegerType;
787 let fractional_degree = index - FloatType::from(base_index);
788 get_ratio_at_integer_index(tuning_system, base_index)
789 * (2.0 as FloatType).powf(fractional_degree / FloatType::from(octave_size))
790}
791
792pub fn get_cents(
797 tuning_system: TuningSystem,
798 index: UnsignedIntegerType,
799 size: Option<UnsignedIntegerType>,
800) -> FloatType {
801 get_cents_at(tuning_system, FloatType::from(index), size)
802}
803
804pub fn get_cents_at(
809 tuning_system: TuningSystem,
810 index: FloatType,
811 size: Option<UnsignedIntegerType>,
812) -> FloatType {
813 let octave_size = size.unwrap_or_else(|| tuning_system.octave_size());
814 assert!(octave_size > 0, "octave_size must be greater than zero");
815 let reference_freq = get_frequency_at(
816 TuningSystem::EqualTemperament { octave_size },
817 index,
818 Some(octave_size),
819 );
820 let comparison_freq = get_frequency_at(tuning_system, index, size);
821 1200.0 * (comparison_freq / reference_freq).log2()
822}
823
824fn get_fraction_from_table(tuning_system: TuningSystem, index: usize) -> Fraction {
825 let table = tuning_system
826 .ratio_table()
827 .expect("tuning system does not have a ratio table");
828 let len = table.len();
829 let octaves = (index / len) as UnsignedIntegerType;
830 table[index % len].with_octaves(octaves)
831}
832
833fn get_ratio_at_integer_index(tuning_system: TuningSystem, index: IntegerType) -> FloatType {
834 let table = tuning_system
835 .ratio_table()
836 .expect("tuning system does not have a ratio table");
837 let len = IntegerType::try_from(table.len()).expect("ratio table length exceeds i32 range");
838 let octave = index.div_euclid(len);
839 let degree = index.rem_euclid(len) as usize;
840 table[degree].ratio() * (2.0 as FloatType).powi(octave)
841}
842
843fn index_to_unsigned_integer(index: usize) -> UnsignedIntegerType {
844 UnsignedIntegerType::try_from(index).expect("tone index exceeds u32 range")
845}
846
847fn default_degree_label(octave_size: UnsignedIntegerType, index: UnsignedIntegerType) -> String {
848 if octave_size == OCTAVE_SIZE {
849 TWELVE_TONE_NAMES[(index % OCTAVE_SIZE) as usize].to_string()
850 } else {
851 format!("T{}", index % octave_size)
852 }
853}
854
855fn degree_name_with_octave(degree_label: &str, octave: UnsignedIntegerType) -> String {
856 let adjusted_octave = i64::from(octave) - 1;
857 let generic_degree_label = degree_label
858 .strip_prefix('T')
859 .is_some_and(|rest| !rest.is_empty() && rest.chars().all(|ch| ch.is_ascii_digit()));
860
861 if generic_degree_label {
862 return if adjusted_octave < 0 {
863 format!("{degree_label}ON{}", -adjusted_octave)
864 } else {
865 format!("{degree_label}O{adjusted_octave}")
866 };
867 }
868
869 if adjusted_octave < 0 {
870 format!("{degree_label}N{}", -adjusted_octave)
871 } else {
872 format!("{degree_label}{adjusted_octave}")
873 }
874}
875
876pub const FORTYTHREE_TONE: [Fraction; 43] = FORTY_THREE_TONE;
878
879pub const JAVANESE: [Fraction; 5] = [
881 Fraction::new_with_base(0, 5, 2),
882 Fraction::new_with_base(1, 5, 2),
883 Fraction::new_with_base(2, 5, 2),
884 Fraction::new_with_base(3, 5, 2),
885 Fraction::new_with_base(4, 5, 2),
886];
887
888pub const THAI: [Fraction; 7] = [
890 Fraction::new_with_base(0, 7, 2),
891 Fraction::new_with_base(1, 7, 2),
892 Fraction::new_with_base(2, 7, 2),
893 Fraction::new_with_base(3, 7, 2),
894 Fraction::new_with_base(4, 7, 2),
895 Fraction::new_with_base(5, 7, 2),
896 Fraction::new_with_base(6, 7, 2),
897];
898
899pub const INDIAN_SCALE_NAMES: [&str; 7] = ["Sa", "Re", "Ga", "Ma", "Pa", "Dha", "Ni"];
901
902#[cfg(test)]
903mod tests {
904 use super::*;
905
906 fn cents_at_degree(system: TuningSystem, degree: usize) -> FloatType {
908 1200.0 * system.ratio(degree).log2()
909 }
910
911 #[test]
912 fn meantone_fifths_match_their_comma_fractions() {
913 const PURE_FIFTH: FloatType = 701.955;
917 const SYNTONIC_COMMA: FloatType = 21.506;
918 for (system, divisor) in [
919 (TuningSystem::ThirdCommaMeantone, 3.0),
920 (TuningSystem::QuarterCommaMeantone, 4.0),
921 (TuningSystem::SixthCommaMeantone, 6.0),
922 ] {
923 let expected = PURE_FIFTH - SYNTONIC_COMMA / divisor;
924 let actual = cents_at_degree(system, 7);
925 assert!(
926 (actual - expected).abs() < 0.01,
927 "{} fifth: expected {expected:.3}, got {actual:.3}",
928 system.display_name()
929 );
930 }
931 }
932
933 #[test]
934 fn kirnberger_i_keeps_its_pure_fifth_and_third() {
935 assert!((cents_at_degree(TuningSystem::KirnbergerI, 7) - 701.955).abs() < 0.01);
938 assert!((cents_at_degree(TuningSystem::KirnbergerI, 4) - 386.314).abs() < 0.01);
939 }
940
941 #[test]
942 fn common_equal_temperaments_divide_the_octave_evenly() {
943 for system in COMMON_EQUAL_TEMPERAMENTS {
944 let size = system.octave_size();
945 let step = 1200.0 / FloatType::from(size);
946 for degree in 0..size as usize {
947 let expected = step * degree as FloatType;
948 let actual = cents_at_degree(system, degree);
949 assert!(
950 (actual - expected).abs() < 0.001,
951 "{size}-EDO degree {degree}: expected {expected:.3}, got {actual:.3}"
952 );
953 }
954 }
955 }
956
957 #[test]
958 fn historical_temperaments_match_their_published_values() {
959 let cases = [
963 (TuningSystem::QuarterCommaMeantone, 386.314, 696.578),
964 (TuningSystem::WerckmeisterIII, 390.225, 696.090),
965 (TuningSystem::Rameau, 386.314, 696.578),
966 (TuningSystem::KirnbergerIII, 386.314, 696.578),
967 (TuningSystem::Vallotti, 392.180, 698.045),
968 (TuningSystem::YoungII, 392.180, 698.045),
969 ];
970
971 for (system, major_third, fifth) in cases {
972 let actual_third = cents_at_degree(system, 4);
973 let actual_fifth = cents_at_degree(system, 7);
974 assert!(
975 (actual_third - major_third).abs() < 0.001,
976 "{} major third: expected {major_third}, got {actual_third}",
977 system.display_name()
978 );
979 assert!(
980 (actual_fifth - fifth).abs() < 0.001,
981 "{} fifth: expected {fifth}, got {actual_fifth}",
982 system.display_name()
983 );
984 }
985 }
986
987 #[test]
988 fn quarter_comma_meantone_and_kirnberger_have_a_pure_major_third() {
989 for system in [
998 TuningSystem::QuarterCommaMeantone,
999 TuningSystem::KirnbergerIII,
1000 ] {
1001 let cents = cents_at_degree(system, 4);
1002 assert!(
1003 (cents - 386.313_714).abs() < 0.001,
1004 "{} major third should be the just 386.314 cents, got {cents}",
1005 system.display_name()
1006 );
1007 }
1008 }
1009
1010 #[test]
1011 fn every_historical_temperament_is_wired_up() {
1012 for system in HISTORICAL_TEMPERAMENTS {
1013 assert_eq!(system.octave_size(), OCTAVE_SIZE, "{system:?}");
1014 assert!(system.ratio_table().is_some(), "{system:?} has no table");
1015 assert_eq!(system.ratio_table().unwrap().len(), 12, "{system:?}");
1016 assert!(!system.description().is_empty(), "{system:?}");
1017 assert_eq!(
1018 TuningSystem::from_str(system.id()).unwrap(),
1019 system,
1020 "{system:?} does not round-trip through its id"
1021 );
1022 assert!(
1023 ALL_TUNING_SYSTEMS.contains(&system),
1024 "{system:?} missing from ALL_TUNING_SYSTEMS"
1025 );
1026 }
1027 }
1028
1029 #[test]
1030 fn equal_temperament_degree_helpers_work_without_tone_objects() {
1031 assert_eq!(
1032 TuningSystem::EqualTemperament { octave_size: 12 }.label(0),
1033 "CN1"
1034 );
1035 assert_eq!(
1036 TuningSystem::EqualTemperament { octave_size: 12 }.octave(0),
1037 0
1038 );
1039 assert_eq!(
1040 TuningSystem::EqualTemperament { octave_size: 12 }.frequency(0),
1041 8.1758
1042 );
1043
1044 assert_eq!(
1045 TuningSystem::EqualTemperament { octave_size: 12 }.label(69),
1046 "A4"
1047 );
1048 assert_eq!(
1049 TuningSystem::EqualTemperament { octave_size: 12 }.octave(69),
1050 5
1051 );
1052 assert!(
1053 (TuningSystem::EqualTemperament { octave_size: 12 }.frequency(69) - 440.0).abs()
1054 < 0.0001
1055 );
1056 }
1057
1058 #[test]
1059 fn fractional_frequency_helpers_support_pitch_space_values() {
1060 let equal = TuningSystem::EqualTemperament {
1061 octave_size: OCTAVE_SIZE,
1062 };
1063 assert!((equal.frequency_at(69.0) - A4).abs() < 0.0001);
1064 assert!((equal.frequency_at(60.0) - C4).abs() < 0.0001);
1065 assert!((TuningSystem::FiveLimit.frequency_at(64.0) - (C4 * 5.0 / 4.0)).abs() < 0.0001);
1066 assert!(
1067 (TuningSystem::PythagoreanTuning.frequency_at(67.0) - (C4 * 3.0 / 2.0)).abs() < 0.0001
1068 );
1069 assert!(TuningSystem::FiveLimit.cents_at(64.0) < -13.0);
1070 }
1071
1072 #[test]
1073 fn ratio_helpers_cover_octaves() {
1074 let two_one: FloatType = Fraction::new(2, 1).into();
1075 assert_eq!(get_ratio(TuningSystem::CarlosHarmonic, 12, None), two_one);
1076 assert_eq!(get_ratio(TuningSystem::CarlosHarmonic24, 24, None), two_one);
1077 assert_eq!(
1078 get_ratio(
1079 TuningSystem::EqualTemperament {
1080 octave_size: OCTAVE_SIZE,
1081 },
1082 12,
1083 None,
1084 ),
1085 two_one
1086 );
1087 }
1088
1089 #[test]
1090 fn fraction_helpers_cover_rational_and_exponential_forms() {
1091 let rational = Fraction::from((3, 2));
1092 assert_eq!(rational.numerator(), 3);
1093 assert_eq!(rational.denominator(), 2);
1094 assert_eq!(rational.base(), 0);
1095 assert_eq!(rational.ratio(), 1.5);
1096 assert_eq!(rational.label(), "3/2");
1097 assert_eq!(rational.with_octaves(2), Fraction::new(12, 2));
1098
1099 let exponential = Fraction::from((7, 12, 2));
1100 assert_eq!(exponential.label(), "2^(7/12)");
1101 assert_eq!(
1102 exponential.with_octaves(1),
1103 Fraction::new_with_base(19, 12, 2)
1104 );
1105 assert!((exponential.ratio() - 2.0_f64.powf(7.0 / 12.0)).abs() < 1e-12);
1106 }
1107
1108 #[test]
1109 fn free_tuning_helpers_accept_size_overrides() {
1110 let system = TuningSystem::EqualTemperament { octave_size: 12 };
1111 assert_eq!(equal_temperament_12(12), Fraction::new_with_base(12, 12, 2));
1112 assert_eq!(
1113 equal_temperament_default(3),
1114 Fraction::new_with_base(3, OCTAVE_SIZE, 2)
1115 );
1116 assert_eq!(
1117 get_fraction(system, 6, Some(24)),
1118 Fraction::new_with_base(6, 24, 2)
1119 );
1120 assert_eq!(get_label(system, 24, Some(24)), "T0O0");
1121 assert!((get_frequency(system, 12, Some(24)) - CN1 * 2.0_f64.sqrt()).abs() < 1e-10);
1122 assert_eq!(get_cents(system, 12, Some(24)), 0.0);
1123 }
1124
1125 #[test]
1126 fn current_tuning_system_variants_return_ratios() {
1127 assert_eq!(TuningSystem::WholeTone.ratio(6), 2.0);
1128 assert_eq!(TuningSystem::QuarterTone.ratio(24), 2.0);
1129 assert_eq!(TuningSystem::PythagoreanTuning.ratio(7), 1.5);
1130 assert_eq!(TuningSystem::Indian22.ratio(22), 2.0);
1131 }
1132
1133 #[test]
1134 fn table_ratios_shift_by_real_octaves() {
1135 assert_eq!(TuningSystem::CarlosHarmonic.ratio(19), 3.0);
1136 assert_eq!(TuningSystem::FortyThreeTone.ratio(68), 3.0);
1137 assert_eq!(TuningSystem::PtolemyIntenseDiatonic.ratio(8), 2.25);
1138 }
1139
1140 #[test]
1141 fn non_twelve_tone_systems_keep_system_octaves_and_labels() {
1142 assert_eq!(TuningSystem::WholeTone.label(1), "DN1");
1143 assert_eq!(TuningSystem::WholeTone.octave_size(), 6);
1144 assert!(
1145 (TuningSystem::WholeTone.ratio(1) - (2.0 as FloatType).powf(1.0 / 6.0)).abs() < 1e-12
1146 );
1147
1148 assert_eq!(TuningSystem::QuarterTone.label(13), "T13ON1");
1149 assert_eq!(TuningSystem::QuarterTone.octave_size(), 24);
1150 assert!(
1151 (TuningSystem::QuarterTone.ratio(13) - (2.0 as FloatType).powf(13.0 / 24.0)).abs()
1152 < 1e-12
1153 );
1154
1155 assert_eq!(TuningSystem::Thai.label(7), "T0O0");
1156 assert_eq!(TuningSystem::Thai.octave_size(), 7);
1157 assert_eq!(TuningSystem::Thai.ratio(7), 2.0);
1158
1159 assert_eq!(TuningSystem::PtolemyIntenseDiatonic.label(8), "Re0");
1160 assert_eq!(TuningSystem::PtolemyIntenseDiatonic.octave_size(), 7);
1161 assert_eq!(TuningSystem::PtolemyIntenseDiatonic.ratio(8), 2.25);
1162
1163 assert_eq!(TuningSystem::FortyThreeTone.label(68), "T25O0");
1164 assert_eq!(TuningSystem::FortyThreeTone.octave_size(), 43);
1165 assert_eq!(TuningSystem::FortyThreeTone.ratio(68), 3.0);
1166 }
1167
1168 #[test]
1169 fn tuning_system_display_and_parse_are_canonical() {
1170 let system = TuningSystem::FiveLimit;
1171 assert_eq!(system.id(), "FiveLimit");
1172 assert_eq!(system.to_string(), "FiveLimit");
1173 assert_eq!("FiveLimit".parse::<TuningSystem>().unwrap(), system);
1174
1175 let err = "not-a-system".parse::<TuningSystem>().unwrap_err();
1176 assert_eq!(
1177 err,
1178 Error::TuningSystem("unknown tuning system \"not-a-system\"".to_string())
1179 );
1180 }
1181
1182 #[test]
1183 fn tuning_system_display_names_cover_variants() {
1184 for system in ALL_TUNING_SYSTEMS {
1185 assert!(!system.id().is_empty());
1186 assert!(!system.display_name().is_empty());
1187 assert!(!system.description().is_empty());
1188 assert!(system.octave_size() > 0);
1189 assert_eq!(system.to_string(), system.id());
1190 }
1191 }
1192
1193 #[test]
1194 fn twelve_tone_systems_keep_chromatic_ratios_ascending() {
1195 for system in ALL_TUNING_SYSTEMS
1196 .into_iter()
1197 .filter(|system| system.octave_size() == OCTAVE_SIZE)
1198 {
1199 let mut previous = system.ratio(0);
1200 for degree in 1..=OCTAVE_SIZE {
1201 let ratio = system.ratio(degree as usize);
1202 assert!(
1203 ratio > previous,
1204 "{} degree {degree} ratio {ratio} should be higher than {previous}",
1205 system.id()
1206 );
1207 previous = ratio;
1208 }
1209 }
1210 }
1211
1212 #[test]
1213 fn all_ratio_tables_keep_degrees_strictly_ascending_within_the_octave() {
1214 for system in ALL_TUNING_SYSTEMS {
1215 let octave_size = system.octave_size();
1216 let mut previous = system.ratio(0);
1217 for degree in 1..=octave_size {
1218 let ratio = system.ratio(degree as usize);
1219 assert!(
1220 ratio > previous,
1221 "{} degree {degree} ratio {ratio} should be higher than {previous} \
1222 (a table entry is likely mistranscribed)",
1223 system.id()
1224 );
1225 previous = ratio;
1226 }
1227 }
1228 }
1229}