1pub mod adaptive;
2
3use crate::defaults::{FloatType, IntegerType, UnsignedIntegerType};
4use crate::error::{Error, Result};
5use crate::tuningsystem::adaptive::AdaptiveTuningSystem;
6
7use std::fmt::{Display, Formatter};
8use std::str::FromStr;
9
10pub const OCTAVE_SIZE: UnsignedIntegerType = 12;
12
13pub const C4: FloatType = 261.6256;
15pub const C0: FloatType = C4 / 16.0;
17pub const CN1: FloatType = C4 / 32.0;
19
20pub const A4: FloatType = 440.0;
22pub const A0: FloatType = A4 / 16.0;
24pub const AN1: FloatType = A4 / 32.0;
26
27pub const TWELVE_TONE_NAMES: [&str; 12] = [
29 "C", "C#/Db", "D", "D#/Eb", "E", "F", "F#/Gb", "G", "G#/Ab", "A", "A#/Bb", "B",
30];
31
32pub const TWELVE_TONE_NAMES_SHARP: [&str; 12] = [
34 "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
35];
36
37pub const TWELVE_TONE_NAMES_FLAT: [&str; 12] = [
39 "C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B",
40];
41
42pub const WHOLE_TONE_NAMES: [&str; 6] = ["C", "D", "E", "F#/Gb", "G#/Ab", "A#/Bb"];
44
45pub const COMMON_TWELVE_TONE_TUNING_SYSTEMS: [TuningSystem; 4] = [
47 TuningSystem::EqualTemperament {
48 octave_size: OCTAVE_SIZE,
49 },
50 TuningSystem::JustIntonation,
51 TuningSystem::PythagoreanTuning,
52 TuningSystem::FiveLimit,
53];
54
55#[derive(Clone, Copy, Debug, Eq, PartialEq)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
58pub enum AnyTuningSystem {
59 Fixed(TuningSystem),
60 Adaptive(AdaptiveTuningSystem),
61}
62
63impl AnyTuningSystem {
64 pub fn frequency_at(
65 self,
66 context: FloatType,
67 index: FloatType,
68 size: Option<UnsignedIntegerType>,
69 ) -> FloatType {
70 match self {
71 Self::Fixed(tuning_system) => {
72 let _ = context;
73 get_frequency_at(tuning_system, index, size)
74 }
75 Self::Adaptive(adaptive_tuning_system) => {
76 adaptive_tuning_system.frequency_at(context, index, size)
77 }
78 }
79 }
80
81 pub fn cents_at(
82 self,
83 context: FloatType,
84 index: FloatType,
85 size: Option<UnsignedIntegerType>,
86 ) -> FloatType {
87 match self {
88 Self::Fixed(tuning_system) => {
89 let _ = context;
90 tuning_system.cents_at(index)
91 }
92 Self::Adaptive(adaptive_tuning_system) => {
93 adaptive_tuning_system.cents_at(context, index, size)
94 }
95 }
96 }
97
98 pub fn is_adaptive(self) -> bool {
99 matches!(self, Self::Adaptive(_))
100 }
101}
102
103impl From<TuningSystem> for AnyTuningSystem {
104 fn from(tuning_system: TuningSystem) -> Self {
105 Self::Fixed(tuning_system)
106 }
107}
108
109impl From<AdaptiveTuningSystem> for AnyTuningSystem {
110 fn from(adaptive_tuning_system: AdaptiveTuningSystem) -> Self {
111 Self::Adaptive(adaptive_tuning_system)
112 }
113}
114
115pub const ALL_TUNING_SYSTEMS: [TuningSystem; 15] = [
117 TuningSystem::EqualTemperament {
118 octave_size: OCTAVE_SIZE,
119 },
120 TuningSystem::WholeTone,
121 TuningSystem::QuarterTone,
122 TuningSystem::JustIntonation,
123 TuningSystem::JustIntonation24,
124 TuningSystem::PythagoreanTuning,
125 TuningSystem::FiveLimit,
126 TuningSystem::ElevenLimit,
127 TuningSystem::FortyThreeTone,
128 TuningSystem::Javanese,
129 TuningSystem::Thai,
130 TuningSystem::Indian,
131 TuningSystem::IndianAlt,
132 TuningSystem::Indian22,
133 TuningSystem::IndianFull,
134];
135
136#[derive(Clone, Copy, Debug, Eq, PartialEq)]
137#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
138pub struct Fraction {
140 pub numerator: UnsignedIntegerType,
142 pub denominator: UnsignedIntegerType,
144 pub base: UnsignedIntegerType,
146}
147
148impl Fraction {
149 pub const fn new(numerator: UnsignedIntegerType, denominator: UnsignedIntegerType) -> Self {
151 Self::new_with_base(numerator, denominator, 0)
152 }
153
154 pub const fn new_with_base(
156 numerator: UnsignedIntegerType,
157 denominator: UnsignedIntegerType,
158 base: UnsignedIntegerType,
159 ) -> Self {
160 Self {
161 numerator,
162 denominator,
163 base,
164 }
165 }
166
167 pub const fn numerator(&self) -> UnsignedIntegerType {
169 self.numerator
170 }
171
172 pub const fn denominator(&self) -> UnsignedIntegerType {
174 self.denominator
175 }
176
177 pub const fn base(&self) -> UnsignedIntegerType {
179 self.base
180 }
181
182 pub fn ratio(self) -> FloatType {
184 self.into()
185 }
186
187 pub fn label(self) -> String {
189 self.to_string()
190 }
191
192 pub fn with_octaves(mut self, octaves: UnsignedIntegerType) -> Self {
194 if octaves == 0 {
195 return self;
196 }
197
198 if self.base == 0 {
199 let multiplier = (2 as UnsignedIntegerType)
200 .checked_pow(octaves)
201 .expect("octave multiplier exceeds u32 range");
202 self.numerator = self
203 .numerator
204 .checked_mul(multiplier)
205 .expect("fraction numerator exceeds u32 range");
206 } else {
207 let octave_offset = self
208 .denominator
209 .checked_mul(octaves)
210 .expect("fraction octave offset exceeds u32 range");
211 self.numerator = self
212 .numerator
213 .checked_add(octave_offset)
214 .expect("fraction numerator exceeds u32 range");
215 }
216
217 self
218 }
219}
220
221impl From<Fraction> for FloatType {
222 fn from(frac: Fraction) -> Self {
223 if frac.base == 0 {
224 frac.numerator as FloatType / frac.denominator as FloatType
225 } else {
226 (frac.base as FloatType)
227 .powf(frac.numerator as FloatType / frac.denominator as FloatType)
228 }
229 }
230}
231
232impl Display for Fraction {
233 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
234 if self.base == 0 {
235 if self.denominator == 1 {
236 write!(f, "{}", self.numerator)
237 } else {
238 write!(f, "{}/{}", self.numerator, self.denominator)
239 }
240 } else if self.numerator == 0 {
241 write!(f, "1")
242 } else {
243 write!(f, "{}^({}/{})", self.base, self.numerator, self.denominator)
244 }
245 }
246}
247
248impl From<(UnsignedIntegerType, UnsignedIntegerType)> for Fraction {
249 fn from(frac: (UnsignedIntegerType, UnsignedIntegerType)) -> Self {
250 Self::new(frac.0, frac.1)
251 }
252}
253
254impl
255 From<(
256 UnsignedIntegerType,
257 UnsignedIntegerType,
258 UnsignedIntegerType,
259 )> for Fraction
260{
261 fn from(
262 frac: (
263 UnsignedIntegerType,
264 UnsignedIntegerType,
265 UnsignedIntegerType,
266 ),
267 ) -> Self {
268 Self::new_with_base(frac.0, frac.1, frac.2)
269 }
270}
271
272#[derive(Clone, Copy, Debug, Eq, PartialEq)]
273#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
274pub enum TuningSystem {
276 EqualTemperament {
278 octave_size: UnsignedIntegerType,
280 },
281 WholeTone,
283 QuarterTone,
285
286 JustIntonation,
288 JustIntonation24,
290 PythagoreanTuning,
292
293 FiveLimit,
295 ElevenLimit,
297
298 FortyThreeTone,
300
301 Javanese,
304 Thai,
306 Indian,
308 IndianAlt,
310 Indian22,
312 IndianFull,
314}
315
316impl TuningSystem {
317 pub fn id(self) -> &'static str {
319 match self {
320 Self::EqualTemperament { .. } => "EqualTemperament",
321 Self::WholeTone => "WholeTone",
322 Self::QuarterTone => "QuarterTone",
323 Self::JustIntonation => "JustIntonation",
324 Self::JustIntonation24 => "JustIntonation24",
325 Self::PythagoreanTuning => "PythagoreanTuning",
326 Self::FiveLimit => "FiveLimit",
327 Self::ElevenLimit => "ElevenLimit",
328 Self::FortyThreeTone => "FortyThreeTone",
329 Self::Javanese => "Javanese",
330 Self::Thai => "Thai",
331 Self::Indian => "Indian",
332 Self::IndianAlt => "IndianAlt",
333 Self::Indian22 => "Indian22",
334 Self::IndianFull => "IndianFull",
335 }
336 }
337
338 pub fn display_name(self) -> &'static str {
340 match self {
341 Self::EqualTemperament { .. } => "Equal temperament",
342 Self::WholeTone => "Whole tone",
343 Self::QuarterTone => "Quarter tone",
344 Self::JustIntonation => "Just intonation",
345 Self::JustIntonation24 => "Just intonation 24",
346 Self::PythagoreanTuning => "Pythagorean",
347 Self::FiveLimit => "Five-limit",
348 Self::ElevenLimit => "Eleven-limit",
349 Self::FortyThreeTone => "Forty-three tone",
350 Self::Javanese => "Javanese",
351 Self::Thai => "Thai",
352 Self::Indian => "Indian",
353 Self::IndianAlt => "Indian alternate",
354 Self::Indian22 => "Indian 22",
355 Self::IndianFull => "Indian full",
356 }
357 }
358
359 pub fn description(self) -> &'static str {
361 match self {
362 Self::EqualTemperament { .. } => "Twelve equal divisions of the octave.",
363 Self::WholeTone => "Six equal whole-tone steps per octave.",
364 Self::QuarterTone => "Twenty-four equal quarter-tone steps per octave.",
365 Self::JustIntonation => "A twelve-tone just-intonation ratio table.",
366 Self::JustIntonation24 => "A twenty-four-tone just-intonation ratio table.",
367 Self::PythagoreanTuning => "A twelve-tone tuning table built from pure fifths.",
368 Self::FiveLimit => "A twelve-tone table using five-limit just ratios.",
369 Self::ElevenLimit => "A twenty-nine-tone table using eleven-limit ratios.",
370 Self::FortyThreeTone => "A forty-three-tone ratio table.",
371 Self::Javanese => "A five-tone Javanese equal-temperament approximation.",
372 Self::Thai => "A seven-tone Thai equal-temperament approximation.",
373 Self::Indian => "A seven-tone Indian scale ratio table.",
374 Self::IndianAlt => "An alternate seven-tone Indian scale ratio table.",
375 Self::Indian22 => "A twenty-two-tone Indian scale ratio table.",
376 Self::IndianFull => "The full twenty-two-tone Indian scale table.",
377 }
378 }
379
380 pub fn ratio(self, index: usize) -> FloatType {
382 get_ratio(self, index, None)
383 }
384
385 pub fn fraction(self, index: usize) -> Fraction {
387 get_fraction(self, index, None)
388 }
389
390 pub fn label(self, index: UnsignedIntegerType) -> String {
392 get_label(self, index, None)
393 }
394
395 pub fn octave(self, index: UnsignedIntegerType) -> UnsignedIntegerType {
397 index / self.octave_size()
398 }
399
400 pub fn frequency(self, index: UnsignedIntegerType) -> FloatType {
402 get_frequency(self, index, None)
403 }
404
405 pub fn frequency_at(self, index: FloatType) -> FloatType {
407 get_frequency_at(self, index, None)
408 }
409
410 pub fn cents(self, index: UnsignedIntegerType) -> FloatType {
412 get_cents(self, index, None)
413 }
414
415 pub fn cents_at(self, index: FloatType) -> FloatType {
417 get_cents_at(self, index, None)
418 }
419
420 pub fn octave_size(self) -> UnsignedIntegerType {
422 match self {
423 Self::EqualTemperament { octave_size } => octave_size,
424 Self::WholeTone => 6,
425 Self::QuarterTone | Self::JustIntonation24 => 24,
426 Self::FortyThreeTone => 43,
427 Self::ElevenLimit => 29,
428 Self::Javanese => 5,
429 Self::Thai | Self::Indian | Self::IndianAlt => 7,
430 Self::Indian22 | Self::IndianFull => 22,
431 Self::JustIntonation | Self::PythagoreanTuning | Self::FiveLimit => OCTAVE_SIZE,
432 }
433 }
434
435 fn ratio_table(self) -> Option<&'static [Fraction]> {
436 match self {
437 Self::JustIntonation => Some(&JUST_INTONATION),
438 Self::JustIntonation24 => Some(&JUST_INTONATION_24),
439 Self::PythagoreanTuning => Some(&PYTHAGOREAN_TUNING),
440 Self::FiveLimit => Some(&FIVE_LIMIT),
441 Self::ElevenLimit => Some(&ELEVEN_LIMIT),
442 Self::FortyThreeTone => Some(&FORTY_THREE_TONE),
443 Self::Javanese => Some(&JAVANESE),
444 Self::Thai => Some(&THAI),
445 Self::Indian => Some(&INDIAN_SCALE),
446 Self::IndianAlt => Some(&INDIA_SCALE_ALT),
447 Self::Indian22 | Self::IndianFull => Some(&INDIAN_SCALE_22),
448 Self::EqualTemperament { .. } | Self::WholeTone | Self::QuarterTone => None,
449 }
450 }
451
452 fn degree_label(self, index: UnsignedIntegerType, octave_size: UnsignedIntegerType) -> String {
453 if octave_size == 0 {
454 return default_degree_label(OCTAVE_SIZE, index);
455 }
456
457 let degree = index % octave_size;
458 match self {
459 Self::WholeTone if octave_size == 6 => WHOLE_TONE_NAMES[degree as usize].to_string(),
460 Self::Indian | Self::IndianAlt if octave_size == 7 => {
461 INDIAN_SCALE_NAMES[degree as usize].to_string()
462 }
463 _ => default_degree_label(octave_size, index),
464 }
465 }
466}
467
468impl Display for TuningSystem {
469 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
470 f.write_str(self.id())
471 }
472}
473
474impl FromStr for TuningSystem {
475 type Err = Error;
476
477 fn from_str(s: &str) -> Result<Self, Self::Err> {
478 match s {
479 "EqualTemperament" => Ok(Self::EqualTemperament {
480 octave_size: OCTAVE_SIZE,
481 }),
482 "WholeTone" => Ok(Self::WholeTone),
483 "QuarterTone" => Ok(Self::QuarterTone),
484 "JustIntonation" => Ok(Self::JustIntonation),
485 "JustIntonation24" => Ok(Self::JustIntonation24),
486 "PythagoreanTuning" => Ok(Self::PythagoreanTuning),
487 "FiveLimit" => Ok(Self::FiveLimit),
488 "ElevenLimit" => Ok(Self::ElevenLimit),
489 "FortyThreeTone" => Ok(Self::FortyThreeTone),
490 "Javanese" => Ok(Self::Javanese),
491 "Thai" => Ok(Self::Thai),
492 "Indian" => Ok(Self::Indian),
493 "IndianAlt" => Ok(Self::IndianAlt),
494 "Indian22" => Ok(Self::Indian22),
495 "IndianFull" => Ok(Self::IndianFull),
496 _ => Err(Error::TuningSystem(format!("unknown tuning system {s:?}"))),
497 }
498 }
499}
500
501pub fn equal_temperament(tone: UnsignedIntegerType, octave_size: UnsignedIntegerType) -> Fraction {
503 Fraction::new_with_base(tone, octave_size, 2)
504}
505
506pub fn equal_temperament_12(tone: UnsignedIntegerType) -> Fraction {
508 equal_temperament(tone, 12)
509}
510
511pub fn equal_temperament_default(tone: UnsignedIntegerType) -> Fraction {
513 equal_temperament(tone, OCTAVE_SIZE)
514}
515
516pub fn get_ratio(
518 tuning_system: TuningSystem,
519 index: usize,
520 size: Option<UnsignedIntegerType>,
521) -> FloatType {
522 get_fraction(tuning_system, index, size).into()
523}
524
525pub fn get_fraction(
530 tuning_system: TuningSystem,
531 index: usize,
532 size: Option<UnsignedIntegerType>,
533) -> Fraction {
534 match tuning_system {
535 TuningSystem::EqualTemperament { octave_size } => equal_temperament(
536 index_to_unsigned_integer(index),
537 size.unwrap_or(octave_size),
538 ),
539 TuningSystem::WholeTone => {
540 equal_temperament(index_to_unsigned_integer(index), size.unwrap_or(6))
541 }
542 TuningSystem::QuarterTone => {
543 equal_temperament(index_to_unsigned_integer(index), size.unwrap_or(24))
544 }
545 _ => get_fraction_from_table(tuning_system, index),
546 }
547}
548
549pub fn get_label(
554 tuning_system: TuningSystem,
555 index: UnsignedIntegerType,
556 size: Option<UnsignedIntegerType>,
557) -> String {
558 let octave_size = size.unwrap_or_else(|| tuning_system.octave_size());
559 assert!(octave_size > 0, "octave_size must be greater than zero");
560 degree_name_with_octave(
561 &tuning_system.degree_label(index, octave_size),
562 index / octave_size,
563 )
564}
565
566pub fn get_frequency(
571 tuning_system: TuningSystem,
572 index: UnsignedIntegerType,
573 size: Option<UnsignedIntegerType>,
574) -> FloatType {
575 get_frequency_at(tuning_system, FloatType::from(index), size)
576}
577
578pub fn get_frequency_at(
583 tuning_system: TuningSystem,
584 index: FloatType,
585 size: Option<UnsignedIntegerType>,
586) -> FloatType {
587 CN1 * get_ratio_at(tuning_system, index, size)
588}
589
590fn get_ratio_at(
591 tuning_system: TuningSystem,
592 index: FloatType,
593 size: Option<UnsignedIntegerType>,
594) -> FloatType {
595 assert!(index.is_finite(), "degree index must be finite");
596 let octave_size = size.unwrap_or_else(|| tuning_system.octave_size());
597 assert!(octave_size > 0, "octave_size must be greater than zero");
598
599 if tuning_system.ratio_table().is_none() {
600 return (2.0 as FloatType).powf(index / FloatType::from(octave_size));
601 }
602
603 let base_index = index.floor() as IntegerType;
604 let fractional_degree = index - FloatType::from(base_index);
605 get_ratio_at_integer_index(tuning_system, base_index)
606 * (2.0 as FloatType).powf(fractional_degree / FloatType::from(octave_size))
607}
608
609pub fn get_cents(
614 tuning_system: TuningSystem,
615 index: UnsignedIntegerType,
616 size: Option<UnsignedIntegerType>,
617) -> FloatType {
618 get_cents_at(tuning_system, FloatType::from(index), size)
619}
620
621pub fn get_cents_at(
626 tuning_system: TuningSystem,
627 index: FloatType,
628 size: Option<UnsignedIntegerType>,
629) -> FloatType {
630 let octave_size = size.unwrap_or_else(|| tuning_system.octave_size());
631 assert!(octave_size > 0, "octave_size must be greater than zero");
632 let reference_freq = get_frequency_at(
633 TuningSystem::EqualTemperament { octave_size },
634 index,
635 Some(octave_size),
636 );
637 let comparison_freq = get_frequency_at(tuning_system, index, size);
638 1200.0 * (comparison_freq / reference_freq).log2()
639}
640
641fn get_fraction_from_table(tuning_system: TuningSystem, index: usize) -> Fraction {
642 let table = tuning_system
643 .ratio_table()
644 .expect("tuning system does not have a ratio table");
645 let len = table.len();
646 let octaves = (index / len) as UnsignedIntegerType;
647 table[index % len].with_octaves(octaves)
648}
649
650fn get_ratio_at_integer_index(tuning_system: TuningSystem, index: IntegerType) -> FloatType {
651 let table = tuning_system
652 .ratio_table()
653 .expect("tuning system does not have a ratio table");
654 let len = IntegerType::try_from(table.len()).expect("ratio table length exceeds i32 range");
655 let octave = index.div_euclid(len);
656 let degree = index.rem_euclid(len) as usize;
657 table[degree].ratio() * (2.0 as FloatType).powi(octave)
658}
659
660fn index_to_unsigned_integer(index: usize) -> UnsignedIntegerType {
661 UnsignedIntegerType::try_from(index).expect("tone index exceeds u32 range")
662}
663
664fn default_degree_label(octave_size: UnsignedIntegerType, index: UnsignedIntegerType) -> String {
665 if octave_size == OCTAVE_SIZE {
666 TWELVE_TONE_NAMES[(index % OCTAVE_SIZE) as usize].to_string()
667 } else {
668 format!("T{}", index % octave_size)
669 }
670}
671
672fn degree_name_with_octave(degree_label: &str, octave: UnsignedIntegerType) -> String {
673 let adjusted_octave = i64::from(octave) - 1;
674 let generic_degree_label = degree_label
675 .strip_prefix('T')
676 .is_some_and(|rest| !rest.is_empty() && rest.chars().all(|ch| ch.is_ascii_digit()));
677
678 if generic_degree_label {
679 return if adjusted_octave < 0 {
680 format!("{degree_label}ON{}", -adjusted_octave)
681 } else {
682 format!("{degree_label}O{adjusted_octave}")
683 };
684 }
685
686 if adjusted_octave < 0 {
687 format!("{degree_label}N{}", -adjusted_octave)
688 } else {
689 format!("{degree_label}{adjusted_octave}")
690 }
691}
692
693pub const JUST_INTONATION: [Fraction; 12] = [
695 Fraction::new(1, 1),
696 Fraction::new(17, 16),
697 Fraction::new(9, 8),
698 Fraction::new(19, 16),
699 Fraction::new(5, 4),
700 Fraction::new(21, 16),
701 Fraction::new(11, 8),
702 Fraction::new(3, 2),
703 Fraction::new(13, 8),
704 Fraction::new(27, 16),
705 Fraction::new(7, 4),
706 Fraction::new(15, 8),
707];
708
709pub const JUST_INTONATION_24: [Fraction; 24] = [
711 Fraction::new(1, 1),
712 Fraction::new(33, 32),
713 Fraction::new(17, 16),
714 Fraction::new(35, 32),
715 Fraction::new(9, 8),
716 Fraction::new(37, 32),
717 Fraction::new(19, 16),
718 Fraction::new(39, 32),
719 Fraction::new(5, 4),
720 Fraction::new(21, 16),
721 Fraction::new(43, 32),
722 Fraction::new(11, 8),
723 Fraction::new(45, 32),
724 Fraction::new(23, 16),
725 Fraction::new(3, 2),
726 Fraction::new(25, 16),
727 Fraction::new(51, 32),
728 Fraction::new(13, 8),
729 Fraction::new(27, 16),
730 Fraction::new(7, 4),
731 Fraction::new(57, 32),
732 Fraction::new(29, 16),
733 Fraction::new(15, 8),
734 Fraction::new(31, 16),
735];
736
737pub const PYTHAGOREAN_TUNING: [Fraction; 12] = [
739 Fraction::new(1, 1),
740 Fraction::new(256, 243),
741 Fraction::new(9, 8),
742 Fraction::new(32, 27),
743 Fraction::new(81, 64),
744 Fraction::new(4, 3),
745 Fraction::new(729, 512),
746 Fraction::new(3, 2),
747 Fraction::new(128, 81),
748 Fraction::new(27, 16),
749 Fraction::new(16, 9),
750 Fraction::new(243, 128),
751];
752
753pub const FIVE_LIMIT: [Fraction; 12] = [
755 Fraction::new(1, 1),
756 Fraction::new(16, 15),
757 Fraction::new(9, 8),
758 Fraction::new(6, 5),
759 Fraction::new(5, 4),
760 Fraction::new(4, 3),
761 Fraction::new(64, 45),
762 Fraction::new(3, 2),
763 Fraction::new(8, 5),
764 Fraction::new(5, 3),
765 Fraction::new(16, 9),
766 Fraction::new(15, 8),
767];
768
769pub const ELEVEN_LIMIT: [Fraction; 29] = [
771 Fraction::new(1, 1),
772 Fraction::new(12, 11),
773 Fraction::new(11, 10),
774 Fraction::new(10, 9),
775 Fraction::new(9, 8),
776 Fraction::new(8, 7),
777 Fraction::new(7, 6),
778 Fraction::new(6, 5),
779 Fraction::new(11, 9),
780 Fraction::new(5, 4),
781 Fraction::new(14, 11),
782 Fraction::new(9, 7),
783 Fraction::new(4, 3),
784 Fraction::new(11, 8),
785 Fraction::new(7, 5),
786 Fraction::new(10, 7),
787 Fraction::new(16, 11),
788 Fraction::new(3, 2),
789 Fraction::new(14, 9),
790 Fraction::new(11, 7),
791 Fraction::new(8, 5),
792 Fraction::new(18, 11),
793 Fraction::new(5, 3),
794 Fraction::new(12, 7),
795 Fraction::new(7, 4),
796 Fraction::new(16, 9),
797 Fraction::new(9, 5),
798 Fraction::new(20, 11),
799 Fraction::new(11, 6),
800];
801
802pub const FORTY_THREE_TONE: [Fraction; 43] = [
804 Fraction::new(1, 1),
805 Fraction::new(81, 80),
806 Fraction::new(33, 32),
807 Fraction::new(21, 20),
808 Fraction::new(16, 15),
809 Fraction::new(12, 11),
810 Fraction::new(11, 10),
811 Fraction::new(10, 9),
812 Fraction::new(9, 8),
813 Fraction::new(8, 7),
814 Fraction::new(7, 6),
815 Fraction::new(32, 27),
816 Fraction::new(6, 5),
817 Fraction::new(11, 9),
818 Fraction::new(5, 4),
819 Fraction::new(14, 11),
820 Fraction::new(9, 7),
821 Fraction::new(21, 16),
822 Fraction::new(4, 3),
823 Fraction::new(27, 20),
824 Fraction::new(11, 8),
825 Fraction::new(7, 5),
826 Fraction::new(10, 7),
827 Fraction::new(16, 11),
828 Fraction::new(40, 27),
829 Fraction::new(3, 2),
830 Fraction::new(23, 21),
831 Fraction::new(14, 9),
832 Fraction::new(11, 7),
833 Fraction::new(8, 5),
834 Fraction::new(18, 11),
835 Fraction::new(5, 3),
836 Fraction::new(27, 16),
837 Fraction::new(12, 7),
838 Fraction::new(7, 4),
839 Fraction::new(16, 8),
840 Fraction::new(9, 5),
841 Fraction::new(20, 11),
842 Fraction::new(11, 6),
843 Fraction::new(15, 8),
844 Fraction::new(40, 21),
845 Fraction::new(64, 33),
846 Fraction::new(160, 81),
847];
848
849pub const FORTYTHREE_TONE: [Fraction; 43] = FORTY_THREE_TONE;
851
852pub const JAVANESE: [Fraction; 5] = [
854 Fraction::new_with_base(0, 5, 2),
855 Fraction::new_with_base(1, 5, 2),
856 Fraction::new_with_base(2, 5, 2),
857 Fraction::new_with_base(3, 5, 2),
858 Fraction::new_with_base(4, 5, 2),
859];
860
861pub const THAI: [Fraction; 7] = [
863 Fraction::new_with_base(0, 7, 2),
864 Fraction::new_with_base(1, 7, 2),
865 Fraction::new_with_base(2, 7, 2),
866 Fraction::new_with_base(3, 7, 2),
867 Fraction::new_with_base(4, 7, 2),
868 Fraction::new_with_base(5, 7, 2),
869 Fraction::new_with_base(6, 7, 2),
870];
871
872pub const INDIAN_SCALE_NAMES: [&str; 7] = ["Sa", "Re", "Ga", "Ma", "Pa", "Dha", "Ni"];
874
875pub const INDIAN_SCALE: [Fraction; 7] = [
877 Fraction::new(1, 1),
878 Fraction::new(9, 8),
879 Fraction::new(5, 4),
880 Fraction::new(4, 3),
881 Fraction::new(3, 2),
882 Fraction::new(5, 3),
883 Fraction::new(15, 8),
884];
885
886pub const INDIA_SCALE_ALT: [Fraction; 7] = [
888 Fraction::new(1, 1),
889 Fraction::new(9, 8),
890 Fraction::new(5, 4),
891 Fraction::new(4, 3),
892 Fraction::new(3, 2),
893 Fraction::new(27, 16),
894 Fraction::new(15, 8),
895];
896
897pub const INDIAN_SCALE_22: [Fraction; 22] = [
899 Fraction::new(1, 1),
900 Fraction::new(256, 243),
901 Fraction::new(16, 15),
902 Fraction::new(10, 9),
903 Fraction::new(9, 8),
904 Fraction::new(32, 27),
905 Fraction::new(6, 5),
906 Fraction::new(5, 4),
907 Fraction::new(81, 64),
908 Fraction::new(4, 3),
909 Fraction::new(27, 20),
910 Fraction::new(45, 32),
911 Fraction::new(729, 512),
912 Fraction::new(3, 2),
913 Fraction::new(128, 81),
914 Fraction::new(8, 5),
915 Fraction::new(5, 3),
916 Fraction::new(27, 16),
917 Fraction::new(16, 9),
918 Fraction::new(9, 5),
919 Fraction::new(15, 8),
920 Fraction::new(243, 128),
921];
922
923#[cfg(test)]
924mod tests {
925 use super::*;
926
927 #[test]
928 fn equal_temperament_degree_helpers_work_without_tone_objects() {
929 assert_eq!(
930 TuningSystem::EqualTemperament { octave_size: 12 }.label(0),
931 "CN1"
932 );
933 assert_eq!(
934 TuningSystem::EqualTemperament { octave_size: 12 }.octave(0),
935 0
936 );
937 assert_eq!(
938 TuningSystem::EqualTemperament { octave_size: 12 }.frequency(0),
939 8.1758
940 );
941
942 assert_eq!(
943 TuningSystem::EqualTemperament { octave_size: 12 }.label(69),
944 "A4"
945 );
946 assert_eq!(
947 TuningSystem::EqualTemperament { octave_size: 12 }.octave(69),
948 5
949 );
950 assert!(
951 (TuningSystem::EqualTemperament { octave_size: 12 }.frequency(69) - 440.0).abs()
952 < 0.0001
953 );
954 }
955
956 #[test]
957 fn fractional_frequency_helpers_support_pitch_space_values() {
958 let equal = TuningSystem::EqualTemperament {
959 octave_size: OCTAVE_SIZE,
960 };
961 assert!((equal.frequency_at(69.0) - A4).abs() < 0.0001);
962 assert!((equal.frequency_at(60.0) - C4).abs() < 0.0001);
963 assert!((TuningSystem::FiveLimit.frequency_at(64.0) - (C4 * 5.0 / 4.0)).abs() < 0.0001);
964 assert!(
965 (TuningSystem::PythagoreanTuning.frequency_at(67.0) - (C4 * 3.0 / 2.0)).abs() < 0.0001
966 );
967 assert!(TuningSystem::FiveLimit.cents_at(64.0) < -13.0);
968 }
969
970 #[test]
971 fn ratio_helpers_cover_octaves() {
972 let two_one: FloatType = Fraction::new(2, 1).into();
973 assert_eq!(get_ratio(TuningSystem::JustIntonation, 12, None), two_one);
974 assert_eq!(get_ratio(TuningSystem::JustIntonation24, 24, None), two_one);
975 assert_eq!(
976 get_ratio(
977 TuningSystem::EqualTemperament {
978 octave_size: OCTAVE_SIZE,
979 },
980 12,
981 None,
982 ),
983 two_one
984 );
985 }
986
987 #[test]
988 fn fraction_helpers_cover_rational_and_exponential_forms() {
989 let rational = Fraction::from((3, 2));
990 assert_eq!(rational.numerator(), 3);
991 assert_eq!(rational.denominator(), 2);
992 assert_eq!(rational.base(), 0);
993 assert_eq!(rational.ratio(), 1.5);
994 assert_eq!(rational.label(), "3/2");
995 assert_eq!(rational.with_octaves(2), Fraction::new(12, 2));
996
997 let exponential = Fraction::from((7, 12, 2));
998 assert_eq!(exponential.label(), "2^(7/12)");
999 assert_eq!(
1000 exponential.with_octaves(1),
1001 Fraction::new_with_base(19, 12, 2)
1002 );
1003 assert!((exponential.ratio() - 2.0_f64.powf(7.0 / 12.0)).abs() < 1e-12);
1004 }
1005
1006 #[test]
1007 fn free_tuning_helpers_accept_size_overrides() {
1008 let system = TuningSystem::EqualTemperament { octave_size: 12 };
1009 assert_eq!(equal_temperament_12(12), Fraction::new_with_base(12, 12, 2));
1010 assert_eq!(
1011 equal_temperament_default(3),
1012 Fraction::new_with_base(3, OCTAVE_SIZE, 2)
1013 );
1014 assert_eq!(
1015 get_fraction(system, 6, Some(24)),
1016 Fraction::new_with_base(6, 24, 2)
1017 );
1018 assert_eq!(get_label(system, 24, Some(24)), "T0O0");
1019 assert!((get_frequency(system, 12, Some(24)) - CN1 * 2.0_f64.sqrt()).abs() < 1e-10);
1020 assert_eq!(get_cents(system, 12, Some(24)), 0.0);
1021 }
1022
1023 #[test]
1024 fn current_tuning_system_variants_return_ratios() {
1025 assert_eq!(TuningSystem::WholeTone.ratio(6), 2.0);
1026 assert_eq!(TuningSystem::QuarterTone.ratio(24), 2.0);
1027 assert_eq!(TuningSystem::PythagoreanTuning.ratio(7), 1.5);
1028 assert_eq!(TuningSystem::Indian22.ratio(22), 2.0);
1029 }
1030
1031 #[test]
1032 fn table_ratios_shift_by_real_octaves() {
1033 assert_eq!(TuningSystem::JustIntonation.ratio(19), 3.0);
1034 assert_eq!(TuningSystem::FortyThreeTone.ratio(68), 3.0);
1035 assert_eq!(TuningSystem::Indian.ratio(8), 2.25);
1036 }
1037
1038 #[test]
1039 fn non_twelve_tone_systems_keep_system_octaves_and_labels() {
1040 assert_eq!(TuningSystem::WholeTone.label(1), "DN1");
1041 assert_eq!(TuningSystem::WholeTone.octave_size(), 6);
1042 assert!(
1043 (TuningSystem::WholeTone.ratio(1) - (2.0 as FloatType).powf(1.0 / 6.0)).abs() < 1e-12
1044 );
1045
1046 assert_eq!(TuningSystem::QuarterTone.label(13), "T13ON1");
1047 assert_eq!(TuningSystem::QuarterTone.octave_size(), 24);
1048 assert!(
1049 (TuningSystem::QuarterTone.ratio(13) - (2.0 as FloatType).powf(13.0 / 24.0)).abs()
1050 < 1e-12
1051 );
1052
1053 assert_eq!(TuningSystem::Thai.label(7), "T0O0");
1054 assert_eq!(TuningSystem::Thai.octave_size(), 7);
1055 assert_eq!(TuningSystem::Thai.ratio(7), 2.0);
1056
1057 assert_eq!(TuningSystem::Indian.label(8), "Re0");
1058 assert_eq!(TuningSystem::Indian.octave_size(), 7);
1059 assert_eq!(TuningSystem::Indian.ratio(8), 2.25);
1060
1061 assert_eq!(TuningSystem::FortyThreeTone.label(68), "T25O0");
1062 assert_eq!(TuningSystem::FortyThreeTone.octave_size(), 43);
1063 assert_eq!(TuningSystem::FortyThreeTone.ratio(68), 3.0);
1064 }
1065
1066 #[test]
1067 fn tuning_system_display_and_parse_are_canonical() {
1068 let system = TuningSystem::FiveLimit;
1069 assert_eq!(system.id(), "FiveLimit");
1070 assert_eq!(system.to_string(), "FiveLimit");
1071 assert_eq!("FiveLimit".parse::<TuningSystem>().unwrap(), system);
1072
1073 let err = "not-a-system".parse::<TuningSystem>().unwrap_err();
1074 assert_eq!(
1075 err,
1076 Error::TuningSystem("unknown tuning system \"not-a-system\"".to_string())
1077 );
1078 }
1079
1080 #[test]
1081 fn tuning_system_display_names_cover_variants() {
1082 for system in ALL_TUNING_SYSTEMS {
1083 assert!(!system.id().is_empty());
1084 assert!(!system.display_name().is_empty());
1085 assert!(!system.description().is_empty());
1086 assert!(system.octave_size() > 0);
1087 assert_eq!(system.to_string(), system.id());
1088 }
1089 }
1090
1091 #[test]
1092 fn twelve_tone_systems_keep_chromatic_ratios_ascending() {
1093 for system in ALL_TUNING_SYSTEMS
1094 .into_iter()
1095 .filter(|system| system.octave_size() == OCTAVE_SIZE)
1096 {
1097 let mut previous = system.ratio(0);
1098 for degree in 1..=OCTAVE_SIZE {
1099 let ratio = system.ratio(degree as usize);
1100 assert!(
1101 ratio > previous,
1102 "{} degree {degree} ratio {ratio} should be higher than {previous}",
1103 system.id()
1104 );
1105 previous = ratio;
1106 }
1107 }
1108 }
1109}