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