1use crate::diag::{ByteSpan, Diag, DiagCode};
4
5#[derive(Clone, Debug, PartialEq)]
7pub struct Scale {
8 pub cents: Vec<f64>,
9}
10
11const TWELVE: &str = "twelve-tone equal temperament";
12
13const MAQAM: &str = "24-tone equal temperament, as maqamworld.com notates the maqam";
16
17const MAJOR: &[f64] = &[200.0, 400.0, 500.0, 700.0, 900.0, 1100.0, 1200.0];
18const MINOR: &[f64] = &[200.0, 300.0, 500.0, 700.0, 800.0, 1000.0, 1200.0];
19
20pub const BUILTIN_SCALES: [(&str, &[f64], &str); 19] = [
23 ("major", MAJOR, TWELVE),
24 ("minor", MINOR, TWELVE),
25 ("ionian", MAJOR, TWELVE),
26 (
27 "dorian",
28 &[200.0, 300.0, 500.0, 700.0, 900.0, 1000.0, 1200.0],
29 TWELVE,
30 ),
31 (
32 "phrygian",
33 &[100.0, 300.0, 500.0, 700.0, 800.0, 1000.0, 1200.0],
34 TWELVE,
35 ),
36 (
37 "lydian",
38 &[200.0, 400.0, 600.0, 700.0, 900.0, 1100.0, 1200.0],
39 TWELVE,
40 ),
41 (
42 "mixolydian",
43 &[200.0, 400.0, 500.0, 700.0, 900.0, 1000.0, 1200.0],
44 TWELVE,
45 ),
46 ("aeolian", MINOR, TWELVE),
47 (
48 "locrian",
49 &[100.0, 300.0, 500.0, 600.0, 800.0, 1000.0, 1200.0],
50 TWELVE,
51 ),
52 (
53 "harmonic-minor",
54 &[200.0, 300.0, 500.0, 700.0, 800.0, 1100.0, 1200.0],
55 TWELVE,
56 ),
57 (
58 "melodic-minor",
59 &[200.0, 300.0, 500.0, 700.0, 900.0, 1100.0, 1200.0],
60 TWELVE,
61 ),
62 (
63 "major-pentatonic",
64 &[200.0, 400.0, 700.0, 900.0, 1200.0],
65 TWELVE,
66 ),
67 (
68 "minor-pentatonic",
69 &[300.0, 500.0, 700.0, 1000.0, 1200.0],
70 TWELVE,
71 ),
72 (
73 "blues",
74 &[300.0, 500.0, 600.0, 700.0, 1000.0, 1200.0],
75 TWELVE,
76 ),
77 (
78 "whole-tone",
79 &[200.0, 400.0, 600.0, 800.0, 1000.0, 1200.0],
80 TWELVE,
81 ),
82 (
83 "chromatic",
84 &[
85 100.0, 200.0, 300.0, 400.0, 500.0, 600.0, 700.0, 800.0, 900.0, 1000.0, 1100.0, 1200.0,
86 ],
87 TWELVE,
88 ),
89 (
90 "rast",
91 &[200.0, 350.0, 500.0, 700.0, 900.0, 1050.0, 1200.0],
92 MAQAM,
93 ),
94 (
95 "bayati",
96 &[150.0, 300.0, 500.0, 700.0, 800.0, 1000.0, 1200.0],
97 MAQAM,
98 ),
99 (
100 "hijaz",
101 &[100.0, 400.0, 500.0, 700.0, 800.0, 1000.0, 1200.0],
102 MAQAM,
103 ),
104];
105
106pub fn builtin_scale(name: &str) -> Option<Scale> {
107 BUILTIN_SCALES
108 .iter()
109 .find(|(known, ..)| *known == name)
110 .map(|(_, cents, _)| Scale {
111 cents: cents.to_vec(),
112 })
113}
114
115pub fn is_scale(rows: &[(usize, &str)]) -> bool {
116 rows.len() > 1
117 && rows
118 .iter()
119 .all(|(_, row)| row.trim().parse::<f64>().is_ok())
120}
121
122pub fn parse_scale(rows: &[(usize, &str)]) -> Result<Scale, Diag> {
123 let mut cents: Vec<f64> = Vec::with_capacity(rows.len());
124 for (at, row) in rows {
125 let value: f64 = row
126 .trim()
127 .parse()
128 .expect("is_scale read every row as a number");
129 let below = cents.last().copied().unwrap_or(0.0);
130 if !value.is_finite() || value <= below {
131 return Err(Diag::new(
132 DiagCode::BadScale,
133 ByteSpan::new(*at, at + row.len()),
134 format!(
135 "a scale row is a degree above the tonic in cents, each above the one \
136 before and the last the period, so `{}` cannot follow `{below}`",
137 row.trim()
138 ),
139 ));
140 }
141 cents.push(value);
142 }
143 Ok(Scale { cents })
144}
145
146impl Scale {
147 pub fn ratio(&self, degree: i64) -> Option<f64> {
150 let n = self.cents.len() as i64;
151 let period = *self.cents.last().expect("a scale holds a row");
152 let index = degree.checked_sub(1)?;
153 let (periods, step) = (index.div_euclid(n), index.rem_euclid(n));
154 let within = match step {
155 0 => 0.0,
156 k => self.cents[k as usize - 1],
157 };
158 let ratio = ((periods as f64 * period + within) / 1200.0).exp2();
159 (ratio.is_finite() && ratio > 0.0).then_some(ratio)
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 fn major() -> Scale {
168 Scale {
169 cents: vec![200.0, 400.0, 500.0, 700.0, 900.0, 1100.0, 1200.0],
170 }
171 }
172
173 #[test]
174 fn degrees_count_from_one_and_continue_through_periods_both_ways() {
175 let st = |n: f64| (n / 12.0).exp2();
176 let close = |a: Option<f64>, b: f64| a.is_some_and(|a| (a - b).abs() < 1e-12);
177 assert!(close(major().ratio(1), 1.0));
178 assert!(close(major().ratio(5), st(7.0)));
179 assert!(close(major().ratio(8), 2.0));
180 assert!(close(major().ratio(10), st(16.0)));
181 assert!(close(major().ratio(0), st(-1.0)), "the leading tone below");
182 assert!(close(major().ratio(-6), 0.5));
183 assert_eq!(major().ratio(i64::MIN), None);
184 assert_eq!(major().ratio(i64::MAX), None);
185 }
186
187 #[test]
188 fn every_builtin_scale_is_rows_a_scale_file_could_hold() {
189 for (name, cents, _) in BUILTIN_SCALES {
190 let text: Vec<String> = cents.iter().map(f64::to_string).collect();
191 let rows: Vec<(usize, &str)> = text.iter().map(|row| (0, row.as_str())).collect();
192 assert!(is_scale(&rows), "{name}");
193 assert_eq!(parse_scale(&rows).ok(), builtin_scale(name), "{name}");
194 assert_eq!(cents.last(), Some(&1200.0), "{name} repeats at the octave");
195 let named = BUILTIN_SCALES.iter().filter(|(n, ..)| *n == name).count();
196 assert_eq!(named, 1, "{name} is named once");
197 }
198 assert_eq!(builtin_scale("major"), Some(major()));
199 assert_eq!(builtin_scale("ionian"), builtin_scale("major"));
200 assert_eq!(builtin_scale("aeolian"), builtin_scale("minor"));
201 assert_eq!(builtin_scale("Major"), None);
202 }
203
204 #[test]
205 fn a_row_not_above_the_one_before_refuses_located() {
206 let rows = [(0, "200"), (4, "100")];
207 let refused = parse_scale(&rows).unwrap_err();
208 assert_eq!(refused.code, DiagCode::BadScale);
209 assert_eq!(refused.span, ByteSpan::new(4, 7));
210 }
211}