1use crate::defaults::{FloatType, IntegerType, UnsignedIntegerType};
26use crate::error::{Error, Result};
27use crate::tuningsystem::monzo::{Monzo, PRIMES, Val};
28use crate::tuningsystem::mos::{MosScale, OCTAVE_CENTS};
29
30use std::fmt::{Display, Formatter};
31use std::str::FromStr;
32
33pub const TRITAVE_CENTS: FloatType = 1_901.955_000_865_388_7;
35
36#[derive(Clone, Copy, Debug, PartialEq)]
43#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
44#[must_use]
45pub struct EqualDivision {
46 divisions: UnsignedIntegerType,
47 period_cents: FloatType,
48 period_ratio: Option<(IntegerType, IntegerType)>,
49}
50
51impl EqualDivision {
52 pub fn new(divisions: UnsignedIntegerType, period_cents: FloatType) -> Result<Self> {
57 if divisions == 0 {
58 return Err(Error::TuningSystem(
59 "a period cannot be divided into no steps".to_owned(),
60 ));
61 }
62 if !period_cents.is_finite() || period_cents <= 0.0 {
63 return Err(Error::TuningSystem(format!(
64 "{period_cents} cents is not an interval that can be divided"
65 )));
66 }
67 Ok(Self {
68 divisions,
69 period_cents,
70 period_ratio: None,
71 })
72 }
73
74 pub fn of_ratio(
78 divisions: UnsignedIntegerType,
79 numerator: IntegerType,
80 denominator: IntegerType,
81 ) -> Result<Self> {
82 if numerator <= 0 || denominator <= 0 || numerator == denominator {
83 return Err(Error::TuningSystem(format!(
84 "{numerator}/{denominator} is not an interval that can be divided"
85 )));
86 }
87 let ratio = FloatType::from(numerator) / FloatType::from(denominator);
88 let mut division = Self::new(divisions, OCTAVE_CENTS * ratio.log2())?;
89 division.period_ratio = Some((numerator, denominator));
90 Ok(division)
91 }
92
93 pub fn octave(divisions: UnsignedIntegerType) -> Result<Self> {
95 Self::of_ratio(divisions, 2, 1)
96 }
97
98 pub const fn edo(divisions: std::num::NonZeroU32) -> Self {
102 Self {
103 divisions: divisions.get(),
104 period_cents: OCTAVE_CENTS,
105 period_ratio: Some((2, 1)),
106 }
107 }
108
109 pub fn tritave(divisions: UnsignedIntegerType) -> Result<Self> {
111 Self::of_ratio(divisions, 3, 1)
112 }
113
114 #[must_use]
116 pub fn divisions(&self) -> UnsignedIntegerType {
117 self.divisions
118 }
119
120 #[must_use]
122 pub fn period_cents(&self) -> FloatType {
123 self.period_cents
124 }
125
126 #[must_use]
128 pub fn period_ratio(&self) -> Option<(IntegerType, IntegerType)> {
129 self.period_ratio
130 }
131
132 #[must_use]
134 pub fn repeats_at_the_octave(&self) -> bool {
135 (self.period_cents - OCTAVE_CENTS).abs() < 1e-9
136 }
137
138 #[must_use]
140 pub fn step_cents(&self) -> FloatType {
141 self.period_cents / FloatType::from(self.divisions)
142 }
143
144 #[must_use]
149 pub fn cents_at(&self, degree: IntegerType) -> FloatType {
150 FloatType::from(degree) * self.step_cents()
151 }
152
153 #[must_use]
155 pub fn ratio_at(&self, degree: IntegerType) -> FloatType {
156 (2.0 as FloatType).powf(self.cents_at(degree) / OCTAVE_CENTS)
157 }
158
159 #[must_use]
161 pub fn degrees(&self) -> Vec<FloatType> {
162 (0..self.divisions)
163 .map(|degree| FloatType::from(degree) * self.step_cents())
164 .collect()
165 }
166
167 #[must_use]
169 pub fn nearest_degree(&self, cents: FloatType) -> IntegerType {
170 (cents / self.step_cents()).round() as IntegerType
171 }
172
173 #[must_use]
175 pub fn error_at_cents(&self, cents: FloatType) -> FloatType {
176 self.cents_at(self.nearest_degree(cents)) - cents
177 }
178
179 #[must_use]
184 pub fn approximation_of(&self, interval: &Monzo) -> (IntegerType, FloatType) {
185 let cents = interval.cents();
186 (self.nearest_degree(cents), self.error_at_cents(cents))
187 }
188
189 pub fn patent_val(&self, limit: IntegerType) -> Result<Val> {
197 let width = PRIMES
198 .iter()
199 .position(|&prime| prime == limit)
200 .map(|index| index + 1)
201 .ok_or_else(|| {
202 Error::TuningSystem(format!("{limit} is not a prime this module carries"))
203 })?;
204 Ok(Val::new(
205 PRIMES[..width]
206 .iter()
207 .map(|&prime| self.nearest_degree(OCTAVE_CENTS * FloatType::from(prime).log2()))
208 .collect::<Vec<_>>(),
209 ))
210 }
211
212 pub fn mos(&self, steps: UnsignedIntegerType, notes: UnsignedIntegerType) -> Result<MosScale> {
217 MosScale::from_equal_division(steps, self.divisions, self.period_cents, notes)
218 }
219}
220
221impl Display for EqualDivision {
222 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
224 write!(f, "{}", self.divisions)?;
225 match self.period_ratio {
226 Some((2, 1)) => write!(f, "edo"),
227 Some((3, 1)) => write!(f, "edt"),
228 Some((numerator, 1)) => write!(f, "ed{numerator}"),
229 Some((numerator, denominator)) => write!(f, "ed{numerator}/{denominator}"),
230 None => write!(f, "ed{:.4}c", self.period_cents),
231 }
232 }
233}
234
235impl FromStr for EqualDivision {
236 type Err = Error;
237
238 fn from_str(text: &str) -> Result<Self> {
240 let text = text.trim();
241 let malformed =
242 || Error::TuningSystem(format!("{text} is not an equal division; one reads 13edt"));
243 let (count, period) = text.split_once("ed").ok_or_else(malformed)?;
244 let divisions = count.parse().map_err(|_| malformed())?;
245 match period {
246 "o" => Self::octave(divisions),
247 "t" => Self::tritave(divisions),
248 cents if cents.ends_with('c') => Self::new(
250 divisions,
251 cents[..cents.len() - 1].parse().map_err(|_| malformed())?,
252 ),
253 _ => {
254 let (numerator, denominator) = match period.split_once('/') {
255 Some((numerator, denominator)) => (numerator, denominator),
256 None => (period, "1"),
257 };
258 Self::of_ratio(
259 divisions,
260 numerator.parse().map_err(|_| malformed())?,
261 denominator.parse().map_err(|_| malformed())?,
262 )
263 }
264 }
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
273 fn an_equal_division_is_read_from_its_name() {
274 assert_eq!("13edt".parse::<EqualDivision>().unwrap().divisions(), 13);
275 assert_eq!("12edo".parse::<EqualDivision>().unwrap().divisions(), 12);
276 assert_eq!("9ed3/2".parse::<EqualDivision>().unwrap().divisions(), 9);
277 assert_eq!("13ed3".parse::<EqualDivision>().unwrap().divisions(), 13);
278 assert!("13".parse::<EqualDivision>().is_err());
279 assert!("xedo".parse::<EqualDivision>().is_err());
280 assert!("9ed3/x".parse::<EqualDivision>().is_err());
281 }
282
283 #[test]
284 fn an_octave_division_is_the_equal_temperament_everyone_means() {
285 let twelve = EqualDivision::octave(12).expect("an octave");
286 assert_eq!(twelve.to_string(), "12edo");
287 assert!(twelve.repeats_at_the_octave());
288 assert!((twelve.step_cents() - 100.0).abs() < 1e-9);
289 assert!((twelve.cents_at(7) - 700.0).abs() < 1e-9);
290 assert!((twelve.ratio_at(12) - 2.0).abs() < 1e-9);
291 assert_eq!(twelve.degrees().len(), 12);
292 assert_eq!(
294 twelve.patent_val(5).expect("the 5-limit"),
295 Val::new([12, 19, 28])
296 );
297 }
298
299 #[test]
300 fn bohlen_pierce_divides_a_twelfth_and_never_reaches_an_octave() {
301 let bohlen_pierce = EqualDivision::tritave(13).expect("a tritave");
302 assert_eq!(bohlen_pierce.to_string(), "13edt");
303 assert!(!bohlen_pierce.repeats_at_the_octave());
304 assert!((bohlen_pierce.period_cents() - TRITAVE_CENTS).abs() < 1e-6);
305 assert!((bohlen_pierce.step_cents() - 146.3042).abs() < 1e-4);
306 let octave = Monzo::from_ratio(2, 1).expect("the octave");
309 let (degree, error) = bohlen_pierce.approximation_of(&octave);
310 assert_eq!(degree, 8);
311 assert!((error + 29.6).abs() < 0.1, "{error}");
312 let tritave = Monzo::from_ratio(3, 1).expect("the tritave");
314 assert!(bohlen_pierce.approximation_of(&tritave).1.abs() < 1e-6);
315 }
316
317 #[test]
320 fn the_carlos_scales_have_periods_that_are_not_intervals_at_all() {
321 for (name, period, divisions, step) in [
322 ("alpha", 1404.0, 9, 156.0),
323 ("beta", 1403.6, 11, 127.6),
324 ("gamma", 1228.465, 20, 61.42),
325 ] {
326 let division = EqualDivision::new(divisions, period).expect("a period");
327 assert!(!division.repeats_at_the_octave(), "{name}");
328 assert!((division.step_cents() - step).abs() < 0.01, "{name}");
329 assert_eq!(division.period_ratio(), None, "{name}");
330 assert!(division.to_string().ends_with('c'), "{name}");
332 }
333 }
334
335 #[test]
336 fn how_well_a_division_does_the_just_intervals_is_the_whole_question() {
337 let twelve = EqualDivision::octave(12).expect("an octave");
338 let fifth = Monzo::from_ratio(3, 2).expect("the fifth");
339 let third = Monzo::from_ratio(5, 4).expect("the third");
340 assert_eq!(twelve.approximation_of(&fifth).0, 7);
341 assert!((twelve.approximation_of(&fifth).1 + 1.955).abs() < 1e-3);
342 assert!((twelve.approximation_of(&third).1 - 13.686).abs() < 1e-3);
344
345 let thirty_one = EqualDivision::octave(31).expect("an octave");
347 assert!(thirty_one.approximation_of(&third).1.abs() < 1.0);
348 let fifty_three = EqualDivision::octave(53).expect("an octave");
350 assert!(fifty_three.approximation_of(&fifth).1.abs() < 0.1);
351 assert!(fifty_three.approximation_of(&third).1.abs() < 1.5);
352 }
353
354 #[test]
355 fn a_division_hands_its_steps_to_the_moment_of_symmetry_machinery() {
356 let twenty_two = EqualDivision::octave(22).expect("an octave");
357 assert_eq!(
358 twenty_two
359 .mos(3, 7)
360 .expect("a generator")
361 .pattern()
362 .expect("a moment")
363 .to_string(),
364 "1L 6s"
365 );
366 let bohlen_pierce = EqualDivision::tritave(13).expect("a tritave");
367 assert_eq!(
368 bohlen_pierce
369 .mos(3, 9)
370 .expect("a generator")
371 .pattern()
372 .expect("a moment")
373 .to_string(),
374 "4L 5s"
375 );
376 }
377
378 #[test]
379 fn a_division_writes_and_reads_the_way_it_is_named() {
380 for name in ["12edo", "13edt", "9ed3/2", "5ed5"] {
381 let division: EqualDivision = name.parse().expect("a division");
382 assert_eq!(division.to_string(), name);
383 }
384 assert_eq!(
387 "13ed3"
388 .parse::<EqualDivision>()
389 .expect("a division")
390 .to_string(),
391 "13edt"
392 );
393 assert!("edo".parse::<EqualDivision>().is_err());
394 assert!("12".parse::<EqualDivision>().is_err());
395 assert!("12edx".parse::<EqualDivision>().is_err());
396 assert!("0edo".parse::<EqualDivision>().is_err());
397 }
398
399 #[test]
400 fn a_period_that_is_not_an_interval_is_refused() {
401 assert!(EqualDivision::new(0, OCTAVE_CENTS).is_err());
402 assert!(EqualDivision::new(12, 0.0).is_err());
403 assert!(EqualDivision::new(12, -1200.0).is_err());
404 assert!(EqualDivision::of_ratio(12, 1, 1).is_err());
405 assert!(EqualDivision::of_ratio(12, -2, 1).is_err());
406 assert!(
407 EqualDivision::octave(12)
408 .expect("an octave")
409 .patent_val(9)
410 .is_err()
411 );
412 }
413}