1use crate::defaults::{FloatType, FractionType, IntegerType};
15use crate::error::{Error, Result};
16
17use std::fmt::{Display, Formatter};
18use std::str::FromStr;
19
20pub const PRIMES: [IntegerType; 25] = [
26 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97,
27];
28
29fn prime_index(prime: IntegerType) -> Option<usize> {
31 PRIMES.iter().position(|&candidate| candidate == prime)
32}
33
34fn entries_for_limit(limit: IntegerType) -> Result<usize> {
36 prime_index(limit).map(|index| index + 1).ok_or_else(|| {
37 Error::TuningSystem(format!(
38 "{limit} is not a prime this module carries; the largest is {}",
39 PRIMES[PRIMES.len() - 1]
40 ))
41 })
42}
43
44fn trimmed(mut entries: Vec<IntegerType>) -> Vec<IntegerType> {
46 while entries.last() == Some(&0) {
47 let _ = entries.pop();
48 }
49 entries
50}
51
52fn parse_entries(body: &str) -> Result<Vec<IntegerType>> {
54 body.split([' ', ',', '\t'])
55 .filter(|piece| !piece.is_empty())
56 .map(|piece| {
57 piece.parse::<IntegerType>().map_err(|_| {
58 Error::TuningSystem(format!("{piece} is not a whole number in a monzo or val"))
59 })
60 })
61 .collect()
62}
63
64fn factor_out(mut value: IntegerType, prime: IntegerType) -> IntegerType {
66 let mut count = 0;
67 while value % prime == 0 {
68 value /= prime;
69 count += 1;
70 }
71 count
72}
73
74fn strip_primes(mut value: IntegerType) -> IntegerType {
76 for &prime in &PRIMES {
77 while value % prime == 0 {
78 value /= prime;
79 }
80 }
81 value
82}
83
84#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
100#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
101#[must_use]
102pub struct Monzo {
103 exponents: Vec<IntegerType>,
104}
105
106impl Monzo {
107 pub fn new(exponents: impl Into<Vec<IntegerType>>) -> Self {
109 Self {
110 exponents: trimmed(exponents.into()),
111 }
112 }
113
114 pub fn unison() -> Self {
116 Self::default()
117 }
118
119 pub fn from_ratio(numerator: IntegerType, denominator: IntegerType) -> Result<Self> {
124 if numerator <= 0 || denominator <= 0 {
125 return Err(Error::TuningSystem(format!(
126 "{numerator}/{denominator} is not a positive ratio"
127 )));
128 }
129 if strip_primes(numerator) * strip_primes(denominator) != 1 {
130 return Err(Error::TuningSystem(format!(
131 "{numerator}/{denominator} has a prime factor past the {}-limit",
132 PRIMES[PRIMES.len() - 1]
133 )));
134 }
135 let exponents = PRIMES
136 .iter()
137 .map(|&prime| factor_out(numerator, prime) - factor_out(denominator, prime))
138 .collect::<Vec<_>>();
139 Ok(Self::new(exponents))
140 }
141
142 pub fn pythagorean(staff_steps: IntegerType, semitones: IntegerType) -> Self {
150 Self::new([
154 19 * staff_steps - 11 * semitones,
155 7 * semitones - 12 * staff_steps,
156 ])
157 }
158
159 pub fn from_fraction(ratio: FractionType) -> Result<Self> {
161 match (ratio.numer(), ratio.denom()) {
162 (Some(&numerator), Some(&denominator)) => Self::from_ratio(numerator, denominator),
163 _ => Err(Error::TuningSystem(
164 "an infinite or undefined ratio has no monzo".to_owned(),
165 )),
166 }
167 }
168
169 #[must_use]
171 pub fn exponents(&self) -> &[IntegerType] {
172 &self.exponents
173 }
174
175 pub fn exponent_of(&self, prime: IntegerType) -> Result<IntegerType> {
179 let index = prime_index(prime).ok_or_else(|| {
180 Error::TuningSystem(format!("{prime} is not a prime this module carries"))
181 })?;
182 Ok(self.at(index))
183 }
184
185 #[must_use]
187 pub fn limit(&self) -> Option<IntegerType> {
188 self.exponents
189 .iter()
190 .rposition(|&exponent| exponent != 0)
191 .map(|index| PRIMES[index])
192 }
193
194 #[must_use]
196 pub fn is_unison(&self) -> bool {
197 self.exponents.is_empty()
198 }
199
200 pub fn ratio(&self) -> Result<FractionType> {
206 let mut numerator: IntegerType = 1;
207 let mut denominator: IntegerType = 1;
208 for (index, &exponent) in self.exponents.iter().enumerate() {
209 let prime = PRIMES[index];
210 let magnitude = exponent.unsigned_abs();
211 let power = prime.checked_pow(magnitude).ok_or_else(|| {
212 Error::TuningSystem(format!("{prime}^{magnitude} does not fit the ratio type"))
213 })?;
214 let side = if exponent >= 0 {
215 &mut numerator
216 } else {
217 &mut denominator
218 };
219 *side = side
220 .checked_mul(power)
221 .ok_or_else(|| Error::TuningSystem("the ratio does not fit its type".to_owned()))?;
222 }
223 Ok(FractionType::new(numerator, denominator))
224 }
225
226 #[must_use]
228 pub fn cents(&self) -> FloatType {
229 1200.0
230 * self
231 .exponents
232 .iter()
233 .enumerate()
234 .map(|(index, &exponent)| {
235 FloatType::from(exponent) * FloatType::from(PRIMES[index]).log2()
236 })
237 .sum::<FloatType>()
238 }
239
240 pub fn multiply(&self, other: &Self) -> Self {
242 let width = self.exponents.len().max(other.exponents.len());
243 Self::new(
244 (0..width)
245 .map(|index| self.at(index) + other.at(index))
246 .collect::<Vec<_>>(),
247 )
248 }
249
250 pub fn divide(&self, other: &Self) -> Self {
252 self.multiply(&other.inverse())
253 }
254
255 pub fn inverse(&self) -> Self {
257 Self::new(
258 self.exponents
259 .iter()
260 .map(|exponent| -exponent)
261 .collect::<Vec<_>>(),
262 )
263 }
264
265 pub fn pow(&self, count: IntegerType) -> Self {
267 Self::new(
268 self.exponents
269 .iter()
270 .map(|exponent| exponent * count)
271 .collect::<Vec<_>>(),
272 )
273 }
274
275 fn at(&self, index: usize) -> IntegerType {
277 self.exponents.get(index).copied().unwrap_or(0)
278 }
279}
280
281impl Display for Monzo {
282 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
284 write!(f, "[")?;
285 for (position, exponent) in self.exponents.iter().enumerate() {
286 if position > 0 {
287 write!(f, " ")?;
288 }
289 write!(f, "{exponent}")?;
290 }
291 write!(f, "⟩")
292 }
293}
294
295impl FromStr for Monzo {
296 type Err = Error;
297
298 fn from_str(text: &str) -> Result<Self> {
300 let text = text.trim();
301 let body = text
302 .strip_prefix('[')
303 .or_else(|| text.strip_prefix('|'))
304 .and_then(|rest| rest.strip_suffix('⟩').or_else(|| rest.strip_suffix('>')))
305 .ok_or_else(|| {
306 Error::TuningSystem(format!("{text} is not a monzo; a monzo reads [-4 4 -1⟩"))
307 })?;
308 Ok(Self::new(parse_entries(body)?))
309 }
310}
311
312#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
330#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
331#[must_use]
332pub struct Val {
333 entries: Vec<IntegerType>,
334}
335
336impl Val {
337 pub fn new(entries: impl Into<Vec<IntegerType>>) -> Self {
343 Self {
344 entries: entries.into(),
345 }
346 }
347
348 pub fn patent(divisions: IntegerType, limit: IntegerType) -> Result<Self> {
354 let width = entries_for_limit(limit)?;
355 Ok(Self::new(
356 PRIMES[..width]
357 .iter()
358 .map(|&prime| {
359 (FloatType::from(divisions) * FloatType::from(prime).log2()).round()
360 as IntegerType
361 })
362 .collect::<Vec<_>>(),
363 ))
364 }
365
366 #[must_use]
368 pub fn entries(&self) -> &[IntegerType] {
369 &self.entries
370 }
371
372 #[must_use]
377 pub fn map(&self, interval: &Monzo) -> IntegerType {
378 interval
379 .exponents()
380 .iter()
381 .enumerate()
382 .map(|(index, &exponent)| exponent * self.entries.get(index).copied().unwrap_or(0))
383 .sum()
384 }
385
386 #[must_use]
388 pub fn reaches(&self, interval: &Monzo) -> bool {
389 interval.exponents().len() <= self.entries.len()
390 }
391
392 #[must_use]
394 pub fn tempers_out(&self, comma: &Monzo) -> bool {
395 self.reaches(comma) && self.map(comma) == 0
396 }
397
398 #[must_use]
402 pub fn divisions(&self) -> IntegerType {
403 self.entries.first().copied().unwrap_or(0)
404 }
405
406 #[must_use]
408 pub fn limit(&self) -> Option<IntegerType> {
409 self.entries
410 .len()
411 .checked_sub(1)
412 .and_then(|index| PRIMES.get(index).copied())
413 }
414
415 pub fn error_cents(&self, interval: &Monzo) -> Result<FloatType> {
421 let divisions = self.divisions();
422 if divisions == 0 {
423 return Err(Error::TuningSystem(
424 "a val mapping the octave to no steps has no step size".to_owned(),
425 ));
426 }
427 let step = 1200.0 / FloatType::from(divisions);
428 Ok(FloatType::from(self.map(interval)) * step - interval.cents())
429 }
430}
431
432impl Display for Val {
433 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
435 write!(f, "⟨")?;
436 for (position, entry) in self.entries.iter().enumerate() {
437 if position > 0 {
438 write!(f, " ")?;
439 }
440 write!(f, "{entry}")?;
441 }
442 write!(f, "]")
443 }
444}
445
446impl FromStr for Val {
447 type Err = Error;
448
449 fn from_str(text: &str) -> Result<Self> {
451 let text = text.trim();
452 let body = text
453 .strip_prefix('⟨')
454 .or_else(|| text.strip_prefix('<'))
455 .and_then(|rest| rest.strip_suffix(']'))
456 .ok_or_else(|| {
457 Error::TuningSystem(format!("{text} is not a val; a val reads ⟨12 19 28]"))
458 })?;
459 Ok(Self::new(parse_entries(body)?))
460 }
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466
467 #[test]
468 fn a_monzo_is_read_from_a_fraction_and_back() {
469 use super::Monzo;
470 use crate::FractionType;
471
472 let comma = Monzo::from_fraction(FractionType::new(81, 80)).unwrap();
473 assert_eq!(comma.limit(), Some(5));
474 assert!(!comma.is_unison());
475 assert_eq!(comma.ratio().unwrap(), FractionType::new(81, 80));
476 let unison = Monzo::from_fraction(FractionType::new(1, 1)).unwrap();
477 assert!(unison.is_unison());
478 assert_eq!(unison.limit(), None);
479 assert!(Monzo::from_fraction(FractionType::new(1, 0)).is_err());
480 }
481
482 #[test]
484 fn monzos_factor_the_commas_they_are_named_for() {
485 for (numerator, denominator, exponents) in [
486 (81, 80, vec![-4, 4, -1]), (531_441, 524_288, vec![-19, 12]), (128, 125, vec![7, 0, -3]), (2048, 2025, vec![11, -4, -2]), (64, 63, vec![6, -2, 0, -1]), (225, 224, vec![-5, 2, 2, -1]), (2, 1, vec![1]), (3, 2, vec![-1, 1]), (5, 4, vec![-2, 0, 1]), ] {
496 let monzo = Monzo::from_ratio(numerator, denominator).expect("a factorable ratio");
497 assert_eq!(monzo.exponents(), exponents, "{numerator}/{denominator}");
498 assert_eq!(
499 monzo.ratio().expect("a ratio that fits"),
500 FractionType::new(numerator, denominator),
501 "{numerator}/{denominator} did not come back"
502 );
503 }
504 }
505
506 #[test]
507 fn a_monzos_cents_is_the_width_of_its_ratio() {
508 let comma = Monzo::from_ratio(81, 80).expect("the syntonic comma");
509 assert!((comma.cents() - 21.506_29).abs() < 1e-5);
510 let fifth = Monzo::from_ratio(3, 2).expect("the fifth");
511 assert!((fifth.cents() - 701.955_00).abs() < 1e-5);
512 assert_eq!(Monzo::unison().cents(), 0.0);
513 }
514
515 #[test]
516 fn stacking_intervals_adds_their_monzos() {
517 let fifth = Monzo::from_ratio(3, 2).expect("the fifth");
518 let octave = Monzo::from_ratio(2, 1).expect("the octave");
519 let third = Monzo::from_ratio(5, 4).expect("the third");
521 let comma = fifth.pow(4).divide(&octave.pow(2)).divide(&third);
522 assert_eq!(comma, Monzo::from_ratio(81, 80).expect("the comma"));
523 let pythagorean = fifth.pow(12).divide(&octave.pow(7));
525 assert_eq!(
526 pythagorean,
527 Monzo::from_ratio(531_441, 524_288).expect("the comma")
528 );
529 }
530
531 #[test]
532 fn a_monzo_writes_and_reads_the_wikis_notation() {
533 let comma = Monzo::from_ratio(81, 80).expect("the syntonic comma");
534 assert_eq!(comma.to_string(), "[-4 4 -1⟩");
535 assert_eq!("[-4 4 -1⟩".parse::<Monzo>().expect("a monzo"), comma);
536 assert_eq!("|-4 4 -1>".parse::<Monzo>().expect("a monzo"), comma);
537 assert_eq!("[-4, 4, -1⟩".parse::<Monzo>().expect("a monzo"), comma);
538 assert_eq!(Monzo::unison().to_string(), "[⟩");
539 assert!("-4 4 -1".parse::<Monzo>().is_err());
540 assert!("[-4 x -1⟩".parse::<Monzo>().is_err());
541 }
542
543 #[test]
544 fn a_ratio_past_the_table_or_below_zero_has_no_monzo() {
545 assert!(Monzo::from_ratio(101, 100).is_err());
546 assert!(Monzo::from_ratio(-3, 2).is_err());
547 assert!(Monzo::from_ratio(1, 0).is_err());
548 assert!(Monzo::from_ratio(97, 89).is_ok());
549 }
550
551 #[test]
553 fn patent_vals_match_the_ones_the_wiki_lists() {
554 for (divisions, limit, entries) in [
555 (5, 5, vec![5, 8, 12]),
556 (7, 5, vec![7, 11, 16]),
557 (12, 5, vec![12, 19, 28]),
558 (12, 7, vec![12, 19, 28, 34]),
559 (19, 5, vec![19, 30, 44]),
560 (22, 5, vec![22, 35, 51]),
561 (31, 11, vec![31, 49, 72, 87, 107]),
562 (41, 5, vec![41, 65, 95]),
563 (53, 5, vec![53, 84, 123]),
564 (72, 11, vec![72, 114, 167, 202, 249]),
565 ] {
566 let val = Val::patent(divisions, limit).expect("a prime limit");
567 assert_eq!(
568 val.entries(),
569 entries,
570 "{divisions}edo to the {limit}-limit"
571 );
572 assert_eq!(val.divisions(), divisions);
573 assert_eq!(val.limit(), Some(limit));
574 }
575 }
576
577 #[test]
578 fn a_temperament_is_the_commas_its_val_sends_to_nothing() {
579 let syntonic = Monzo::from_ratio(81, 80).expect("the syntonic comma");
580 let pythagorean = Monzo::from_ratio(531_441, 524_288).expect("the Pythagorean comma");
581
582 for divisions in [12, 19, 31] {
584 let val = Val::patent(divisions, 5).expect("the 5-limit");
585 assert!(val.tempers_out(&syntonic), "{divisions}edo is meantone");
586 }
587 let twenty_two = Val::patent(22, 5).expect("the 5-limit");
589 assert!(!twenty_two.tempers_out(&syntonic));
590
591 let twelve = Val::patent(12, 5).expect("the 5-limit");
593 assert!(twelve.tempers_out(&pythagorean));
594 assert!(
595 !Val::patent(19, 5)
596 .expect("the 5-limit")
597 .tempers_out(&pythagorean)
598 );
599 }
600
601 #[test]
602 fn a_val_says_nothing_about_a_prime_it_does_not_reach() {
603 let five_limit = Val::patent(12, 5).expect("the 5-limit");
604 let septimal = Monzo::from_ratio(7, 4).expect("the harmonic seventh");
605 assert!(!five_limit.reaches(&septimal));
606 assert!(!five_limit.tempers_out(&Monzo::from_ratio(64, 63).expect("the comma")));
607 assert!(Val::patent(12, 7).expect("the 7-limit").reaches(&septimal));
608 }
609
610 #[test]
611 fn error_cents_says_how_far_off_a_tuning_sounds() {
612 let fifth = Monzo::from_ratio(3, 2).expect("the fifth");
613 let twelve = Val::patent(12, 5).expect("the 5-limit");
614 let error = twelve.error_cents(&fifth).expect("a step size");
616 assert!((error + 1.955).abs() < 1e-3, "{error}");
617 let third = Monzo::from_ratio(5, 4).expect("the third");
619 let error = twelve.error_cents(&third).expect("a step size");
620 assert!((error - 13.686).abs() < 1e-3, "{error}");
621 assert!(Val::new([0, 0]).error_cents(&fifth).is_err());
622 }
623
624 #[test]
625 fn a_val_writes_and_reads_the_wikis_notation() {
626 let twelve = Val::patent(12, 5).expect("the 5-limit");
627 assert_eq!(twelve.to_string(), "⟨12 19 28]");
628 assert_eq!("⟨12 19 28]".parse::<Val>().expect("a val"), twelve);
629 assert_eq!("<12 19 28]".parse::<Val>().expect("a val"), twelve);
630 assert!("12 19 28".parse::<Val>().is_err());
631 }
632
633 #[test]
634 fn a_limit_that_is_not_a_prime_is_refused() {
635 assert!(Val::patent(12, 9).is_err());
636 assert!(Val::patent(12, 101).is_err());
637 assert!(
638 Monzo::from_ratio(9, 8)
639 .expect("a ratio")
640 .exponent_of(9)
641 .is_err()
642 );
643 }
644}