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