1use crate::defaults::{FloatType, UnsignedIntegerType};
18use crate::error::{Error, Result};
19
20use std::fmt::{Display, Formatter};
21use std::str::FromStr;
22
23const STEP_TOLERANCE: FloatType = 1e-6;
28
29pub const OCTAVE_CENTS: FloatType = 1200.0;
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49#[must_use]
50pub struct Mos {
51 large: UnsignedIntegerType,
52 small: UnsignedIntegerType,
53}
54
55impl Mos {
56 pub fn new(large: UnsignedIntegerType, small: UnsignedIntegerType) -> Result<Self> {
61 if large == 0 && small == 0 {
62 return Err(Error::TuningSystem(
63 "a moment of symmetry needs at least one step".to_owned(),
64 ));
65 }
66 Ok(Self { large, small })
67 }
68
69 #[must_use]
71 pub fn large(self) -> UnsignedIntegerType {
72 self.large
73 }
74
75 #[must_use]
77 pub fn small(self) -> UnsignedIntegerType {
78 self.small
79 }
80
81 #[must_use]
83 pub fn notes(self) -> UnsignedIntegerType {
84 self.large + self.small
85 }
86
87 pub fn inverted(self) -> Self {
93 Self {
94 large: self.small,
95 small: self.large,
96 }
97 }
98
99 #[must_use]
104 pub fn brightest_word(self) -> String {
105 let notes = self.notes();
106 (0..notes)
109 .map(|position| {
110 let before = ceiling_ratio(position * self.large, notes);
111 let after = ceiling_ratio((position + 1) * self.large, notes);
112 if after > before { 'L' } else { 's' }
113 })
114 .collect()
115 }
116}
117
118fn ceiling_ratio(numerator: UnsignedIntegerType, denominator: UnsignedIntegerType) -> u64 {
120 let numerator = u64::from(numerator);
121 let denominator = u64::from(denominator);
122 numerator.div_ceil(denominator)
123}
124
125impl Display for Mos {
126 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
128 write!(f, "{}L {}s", self.large, self.small)
129 }
130}
131
132impl FromStr for Mos {
133 type Err = Error;
134
135 fn from_str(text: &str) -> Result<Self> {
137 let malformed =
138 || Error::TuningSystem(format!("{text} is not a pattern; a pattern reads 5L 2s"));
139 let (large, rest) = text
140 .trim()
141 .split_once('L')
142 .ok_or_else(malformed)
143 .map(|(large, rest)| (large.trim(), rest.trim()))?;
144 let small = rest.strip_suffix('s').ok_or_else(malformed)?.trim();
145 Self::new(
146 large.parse().map_err(|_| malformed())?,
147 small.parse().map_err(|_| malformed())?,
148 )
149 }
150}
151
152#[derive(Clone, Copy, Debug, PartialEq)]
169#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
170#[must_use]
171pub struct MosScale {
172 generator: FloatType,
173 period: FloatType,
174 notes: UnsignedIntegerType,
175}
176
177impl MosScale {
178 pub fn new(
185 generator: FloatType,
186 period: FloatType,
187 notes: UnsignedIntegerType,
188 ) -> Result<Self> {
189 if !period.is_finite() || period <= 0.0 {
190 return Err(Error::TuningSystem(format!(
191 "{period} cents is not a period a scale can be generated in"
192 )));
193 }
194 if !generator.is_finite() {
195 return Err(Error::TuningSystem(
196 "a generator has to be a real number of cents".to_owned(),
197 ));
198 }
199 if notes == 0 {
200 return Err(Error::TuningSystem(
201 "a scale of no notes is not a scale".to_owned(),
202 ));
203 }
204 let folded = generator.rem_euclid(period);
205 if folded < STEP_TOLERANCE || period - folded < STEP_TOLERANCE {
206 return Err(Error::TuningSystem(format!(
207 "a generator of {generator} cents folds to the period itself and generates nothing"
208 )));
209 }
210 Ok(Self {
211 generator: folded,
212 period,
213 notes,
214 })
215 }
216
217 pub fn from_equal_division(
223 steps: UnsignedIntegerType,
224 divisions: UnsignedIntegerType,
225 period: FloatType,
226 notes: UnsignedIntegerType,
227 ) -> Result<Self> {
228 if divisions == 0 {
229 return Err(Error::TuningSystem(
230 "a period cannot be divided into no steps".to_owned(),
231 ));
232 }
233 let generator = period * FloatType::from(steps) / FloatType::from(divisions);
234 Self::new(generator, period, notes)
235 }
236
237 #[must_use]
239 pub fn generator(self) -> FloatType {
240 self.generator
241 }
242
243 #[must_use]
245 pub fn period(self) -> FloatType {
246 self.period
247 }
248
249 #[must_use]
251 pub fn notes(self) -> UnsignedIntegerType {
252 self.notes
253 }
254
255 #[must_use]
257 pub fn degrees(self) -> Vec<FloatType> {
258 let mut degrees = (0..self.notes)
259 .map(|index| (FloatType::from(index) * self.generator).rem_euclid(self.period))
260 .collect::<Vec<_>>();
261 degrees.sort_by(|left, right| left.partial_cmp(right).expect("cents are real numbers"));
262 degrees
263 }
264
265 #[must_use]
267 pub fn steps(self) -> Vec<FloatType> {
268 let degrees = self.degrees();
269 degrees
270 .iter()
271 .zip(degrees.iter().skip(1))
272 .map(|(lower, higher)| higher - lower)
273 .chain(std::iter::once(
274 self.period - degrees.last().copied().unwrap_or(0.0),
275 ))
276 .collect()
277 }
278
279 fn step_sizes(self) -> Vec<FloatType> {
281 let mut sizes: Vec<FloatType> = Vec::new();
282 for step in self.steps() {
283 if !sizes
284 .iter()
285 .any(|known| (known - step).abs() < STEP_TOLERANCE)
286 {
287 sizes.push(step);
288 }
289 }
290 sizes.sort_by(|left, right| right.partial_cmp(left).expect("cents are real numbers"));
291 sizes
292 }
293
294 #[must_use]
296 pub fn is_moment_of_symmetry(self) -> bool {
297 self.step_sizes().len() == 2
298 }
299
300 #[must_use]
302 pub fn is_equal(self) -> bool {
303 self.step_sizes().len() == 1
304 }
305
306 pub fn pattern(self) -> Result<Mos> {
312 let sizes = self.step_sizes();
313 let [large, small] = sizes[..] else {
314 return Err(Error::TuningSystem(format!(
315 "{} notes of a generator of {:.3} cents give {} step sizes, not two",
316 self.notes,
317 self.generator,
318 sizes.len()
319 )));
320 };
321 let count = |width: FloatType| {
322 self.steps()
323 .iter()
324 .filter(|step| (*step - width).abs() < STEP_TOLERANCE)
325 .count() as UnsignedIntegerType
326 };
327 Mos::new(count(large), count(small))
328 }
329
330 pub fn word(self) -> Result<String> {
334 let sizes = self.step_sizes();
335 let [large, _] = sizes[..] else {
336 let _ = self.pattern()?;
337 unreachable!("pattern rejects anything but two step sizes");
338 };
339 Ok(self
340 .steps()
341 .iter()
342 .map(|step| {
343 if (step - large).abs() < STEP_TOLERANCE {
344 'L'
345 } else {
346 's'
347 }
348 })
349 .collect())
350 }
351}
352
353pub fn moment_of_symmetry_sizes(
360 generator: FloatType,
361 period: FloatType,
362 most: UnsignedIntegerType,
363) -> Result<Vec<UnsignedIntegerType>> {
364 let _ = MosScale::new(generator, period, 1)?;
366 Ok((2..=most)
367 .filter(|¬es| {
368 MosScale::new(generator, period, notes).is_ok_and(MosScale::is_moment_of_symmetry)
369 })
370 .collect())
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376
377 #[test]
378 fn a_pattern_is_read_from_its_name() {
379 let diatonic: Mos = "5L 2s".parse().unwrap();
380 assert_eq!(diatonic.notes(), 7);
381 assert_eq!("2L5s".parse::<Mos>().unwrap().notes(), 7);
382 assert!("5L".parse::<Mos>().is_err());
383 assert!("xL 2s".parse::<Mos>().is_err());
384 }
385
386 #[test]
387 fn the_diatonic_scale_is_five_large_steps_and_two_small() {
388 let diatonic = MosScale::new(701.955, OCTAVE_CENTS, 7).expect("a fifth");
389 assert!(diatonic.is_moment_of_symmetry());
390 assert_eq!(diatonic.pattern().expect("a moment").to_string(), "5L 2s");
391 assert_eq!(diatonic.word().expect("a moment"), "LLLsLLs");
394 assert_eq!(diatonic.degrees().len(), 7);
395 let steps = diatonic.steps();
396 assert!((steps[0] - 203.910).abs() < 1e-3, "{steps:?}");
397 assert!((steps[3] - 90.225).abs() < 1e-3, "{steps:?}");
398 assert!((steps.iter().sum::<FloatType>() - OCTAVE_CENTS).abs() < 1e-9);
399 }
400
401 #[test]
402 fn the_pentatonic_is_the_moment_before_the_diatonic() {
403 let pentatonic = MosScale::new(701.955, OCTAVE_CENTS, 5).expect("a fifth");
404 assert_eq!(pentatonic.pattern().expect("a moment").to_string(), "2L 3s");
405 }
406
407 #[test]
408 fn twelve_fifths_come_out_equal_and_so_are_not_a_moment() {
409 let chromatic = MosScale::new(700.0, OCTAVE_CENTS, 12).expect("a fifth");
410 assert!(chromatic.is_equal());
411 assert!(!chromatic.is_moment_of_symmetry());
412 assert!(chromatic.pattern().is_err());
413 }
414
415 #[test]
416 fn a_count_between_two_moments_has_three_step_sizes() {
417 let six = MosScale::new(701.955, OCTAVE_CENTS, 6).expect("a fifth");
418 assert!(!six.is_moment_of_symmetry());
419 assert!(six.pattern().is_err());
420 }
421
422 #[test]
425 fn named_temperaments_give_the_patterns_they_are_listed_with() {
426 for (steps, divisions, notes, pattern) in [
427 (3, 22, 7, "1L 6s"), (3, 22, 8, "7L 1s"), (9, 16, 7, "2L 5s"), (9, 16, 9, "7L 2s"), (13, 23, 7, "2L 5s"), (7, 12, 7, "5L 2s"), (7, 12, 5, "2L 3s"), (18, 31, 7, "5L 2s"), (5, 17, 7, "3L 4s"),
438 ] {
439 let scale = MosScale::from_equal_division(steps, divisions, OCTAVE_CENTS, notes)
440 .expect("a generator");
441 assert_eq!(
442 scale.pattern().expect("a moment").to_string(),
443 pattern,
444 "{steps}\\{divisions} at {notes} notes"
445 );
446 }
447 }
448
449 #[test]
450 fn a_pattern_writes_and_reads_the_way_it_is_named() {
451 let diatonic = Mos::new(5, 2).expect("a pattern");
452 assert_eq!(diatonic.to_string(), "5L 2s");
453 assert_eq!("5L 2s".parse::<Mos>().expect("a pattern"), diatonic);
454 assert_eq!("5L2s".parse::<Mos>().expect("a pattern"), diatonic);
455 assert_eq!(diatonic.notes(), 7);
456 assert_eq!(diatonic.inverted().to_string(), "2L 5s");
457 assert!("5L".parse::<Mos>().is_err());
458 assert!("five L 2s".parse::<Mos>().is_err());
459 assert!(Mos::new(0, 0).is_err());
460 }
461
462 #[test]
464 fn the_brightest_mode_puts_its_large_steps_first() {
465 for (pattern, word) in [
466 ("5L 2s", "LLLsLLs"),
467 ("2L 5s", "LssLsss"),
468 ("1L 6s", "Lssssss"),
469 ("7L 1s", "LLLLLLLs"),
470 ("2L 3s", "LsLss"),
471 ("3L 4s", "LsLsLss"),
472 ] {
473 let mos: Mos = pattern.parse().expect("a pattern");
474 assert_eq!(mos.brightest_word(), word, "{pattern}");
475 assert_eq!(mos.brightest_word().len(), mos.notes() as usize);
476 }
477 }
478
479 #[test]
480 fn the_moments_of_a_fifth_are_the_ones_the_wiki_lists() {
481 let sizes = moment_of_symmetry_sizes(701.955, OCTAVE_CENTS, 12).expect("a fifth");
484 assert_eq!(sizes, vec![2, 3, 5, 7, 12]);
485 }
486
487 #[test]
489 fn a_period_need_not_be_an_octave() {
490 let tritave = 1200.0 * 3.0_f64.log2();
491 let lambda = MosScale::from_equal_division(3, 13, tritave, 9).expect("a generator");
492 assert_eq!(lambda.pattern().expect("a moment").to_string(), "4L 5s");
493 assert!((lambda.period() - 1901.955).abs() < 1e-3);
494 assert!((lambda.steps().iter().sum::<FloatType>() - tritave).abs() < 1e-9);
495 }
496
497 #[test]
498 fn a_generator_that_folds_to_the_period_generates_nothing() {
499 assert!(MosScale::new(1200.0, OCTAVE_CENTS, 7).is_err());
500 assert!(MosScale::new(0.0, OCTAVE_CENTS, 7).is_err());
501 assert!(MosScale::new(701.955, 0.0, 7).is_err());
502 assert!(MosScale::new(701.955, OCTAVE_CENTS, 0).is_err());
503 assert!(MosScale::from_equal_division(3, 0, OCTAVE_CENTS, 7).is_err());
504 assert!(moment_of_symmetry_sizes(0.0, OCTAVE_CENTS, 12).is_err());
505 }
506
507 #[test]
508 fn a_generator_is_folded_into_its_period() {
509 let fifth = MosScale::new(701.955, OCTAVE_CENTS, 7).expect("a fifth");
510 let twelfth = MosScale::new(1901.955, OCTAVE_CENTS, 7).expect("a twelfth");
511 assert!((fifth.generator() - twelfth.generator()).abs() < 1e-9);
512 assert_eq!(
513 fifth.pattern().expect("a moment"),
514 twelfth.pattern().expect("a moment")
515 );
516 }
517}