1use crate::defaults::{FloatType, IntegerType};
17use crate::error::Result;
18use crate::interval::{ChromaticInterval, Interval};
19use crate::pitch::Pitch;
20use crate::scale::Scale;
21use crate::sieve::Sieve;
22
23use std::sync::LazyLock;
24
25static DEFAULT_STEP: LazyLock<Interval> =
27 LazyLock::new(|| Interval::from_name("m2").expect("m2 is a valid interval name"));
28
29#[derive(Clone, Debug)]
47#[must_use]
48pub struct StepScale {
49 tonic: Pitch,
50 steps: Vec<Interval>,
51 closed: bool,
54}
55
56impl StepScale {
57 pub fn cyclical(tonic: Pitch, steps: &[&str]) -> Result<Self> {
61 Ok(Self::cyclical_of(tonic, parse_steps(steps)?))
62 }
63
64 pub fn cyclical_of(tonic: Pitch, steps: Vec<Interval>) -> Self {
67 Self {
68 tonic,
69 steps: or_default(steps),
70 closed: false,
71 }
72 }
73
74 pub fn octave_repeating(tonic: Pitch, steps: &[&str]) -> Result<Self> {
94 Self::octave_repeating_of(tonic, parse_steps(steps)?)
95 }
96
97 pub fn octave_repeating_of(tonic: Pitch, steps: Vec<Interval>) -> Result<Self> {
99 let mut steps = or_default(steps);
100 if let Ok(sum) = interval_sum(&tonic, &steps)
104 && sum.semitones() == 12.0
105 && sum.short_name() != "P8"
106 {
107 let last = steps.len() - 1;
108 let start = interval_sum(&tonic, &steps[..last])?
109 .transpose_pitch_with_options(&tonic, false, None)?;
110 let octave =
111 Interval::from_name("P8")?.transpose_pitch_with_options(&tonic, false, None)?;
112 steps[last] = Interval::between_pitches(&start, &octave)?;
113 return Ok(Self {
114 tonic,
115 steps,
116 closed: true,
117 });
118 }
119 let closing = match interval_sum(&tonic, &steps).and_then(|sum| sum.inversion()) {
120 Ok(closing) => closing,
121 Err(_) => {
125 let risen: FloatType = steps.iter().map(Interval::semitones).sum();
126 let octaves = (risen / 12.0).floor() + 1.0;
127 Interval::from_semitones((octaves * 12.0 - risen).round() as IntegerType)?
128 }
129 };
130 if closing.short_name() != "P1" {
132 steps.push(closing);
133 }
134 Ok(Self {
135 tonic,
136 steps,
137 closed: true,
138 })
139 }
140
141 pub fn sieve(tonic: Pitch, expression: &str) -> Result<Self> {
150 Self::sieve_by(tonic, expression, 1.0)
151 }
152
153 pub fn sieve_by(tonic: Pitch, expression: &str, eld: FloatType) -> Result<Self> {
158 let widths = Sieve::parse(expression)?.interval_widths()?;
159 let steps = widths
160 .into_iter()
161 .map(|width| {
162 Interval::from_chromatic(ChromaticInterval::new(FloatType::from(width) * eld)?)
163 })
164 .collect::<Result<Vec<_>>>()?;
165 Ok(Self {
166 tonic,
167 steps,
168 closed: false,
169 })
170 }
171
172 pub fn tonic(&self) -> &Pitch {
174 &self.tonic
175 }
176
177 pub fn steps(&self) -> &[Interval] {
179 &self.steps
180 }
181
182 pub fn degree_count(&self) -> usize {
184 self.steps.len()
185 }
186
187 pub fn scale(&self) -> Result<Scale> {
191 if self.closed {
192 Scale::from_steps(self.tonic.clone(), self.steps.clone())
193 } else {
194 Scale::from_cycle(self.tonic.clone(), self.steps.clone())
195 }
196 }
197
198 pub fn pitches(&self) -> Result<Vec<Pitch>> {
200 let mut pitches = Vec::with_capacity(self.steps.len() + 1);
201 pitches.push(self.tonic.clone());
202
203 let mut current = self.tonic.clone();
204 for step in &self.steps {
205 current = step.transpose_pitch_with_options(¤t, false, Some(1))?;
209 pitches.push(current.clone());
210 }
211 Ok(pitches)
212 }
213}
214
215fn interval_sum(reference: &Pitch, steps: &[Interval]) -> Result<Interval> {
221 let mut current = reference.clone();
222 for step in steps {
223 current = step.transpose_pitch_with_options(¤t, false, None)?;
224 }
225 Interval::between_pitches(reference, ¤t)
226}
227
228fn or_default(steps: Vec<Interval>) -> Vec<Interval> {
230 if steps.is_empty() {
231 vec![DEFAULT_STEP.clone()]
232 } else {
233 steps
234 }
235}
236
237fn parse_steps(steps: &[&str]) -> Result<Vec<Interval>> {
238 steps
239 .iter()
240 .map(|name| Interval::from_name(*name))
241 .collect()
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn a_step_scale_reports_its_tonic_and_steps() {
250 let tonic = Pitch::from_name("C4").unwrap();
251 let scale = StepScale::cyclical(tonic.clone(), &["M2", "M2"]).unwrap();
252 assert_eq!(scale.tonic(), &tonic);
253 assert_eq!(scale.steps().len(), 2);
254 assert_eq!(scale.degree_count(), 2);
255 }
256
257 #[test]
258 fn steps_reaching_the_octave_close_with_no_unison() {
259 let scale = StepScale::octave_repeating(
260 Pitch::from_name("C4").unwrap(),
261 &["m3", "M2", "m3", "M2", "M2"],
262 )
263 .unwrap();
264 assert_eq!(names(&scale), ["C", "Eb", "F", "Ab", "Bb", "C"]);
265 }
266
267 fn names(scale: &StepScale) -> Vec<String> {
268 scale
269 .pitches()
270 .expect("scale realizes")
271 .iter()
272 .map(|pitch| pitch.name())
273 .collect()
274 }
275
276 fn tonic(name: &str) -> Pitch {
277 Pitch::from_name(name).expect("valid tonic")
278 }
279
280 #[test]
281 fn a_step_scale_answers_as_a_scale() {
282 let scale = StepScale::octave_repeating(tonic("C4"), &["m3", "M3"])
286 .unwrap()
287 .scale()
288 .unwrap();
289 let spelled = |pitches: Vec<Pitch>| -> Vec<String> {
290 pitches.iter().map(Pitch::name_with_octave).collect()
291 };
292 assert_eq!(
293 spelled(scale.pitches_between(&tonic("G2"), &tonic("G4")).unwrap()),
294 ["G2", "C3", "Eb3", "G3", "C4", "Eb4", "G4"]
295 );
296 assert_eq!(scale.degree_count(), 3);
298 assert_eq!(scale.degree_of(&tonic("C4")).unwrap(), Some(1));
299 assert_eq!(scale.degree_of(&tonic("E-")).unwrap(), Some(2));
300 assert_eq!(scale.degree_of(&tonic("G5")).unwrap(), Some(3));
301 assert_eq!(scale.degree_of(&tonic("D4")).unwrap(), None);
302 let next = |origin: &str, steps: usize| {
303 scale
304 .next_pitch_above(&tonic(origin), steps)
305 .unwrap()
306 .name_with_octave()
307 };
308 assert_eq!(next("C4", 1), "Eb4");
309 assert_eq!(next("G3", 2), "Eb4");
310 assert_eq!(
311 scale
312 .next_pitch_below(&tonic("Eb5"), 1)
313 .unwrap()
314 .name_with_octave(),
315 "C5"
316 );
317 }
318
319 #[test]
320 fn a_respelling_scale_comes_down_by_its_own_walk() {
321 let scale = StepScale::octave_repeating(tonic("D"), &["m2", "m2", "m2"])
325 .unwrap()
326 .scale()
327 .unwrap();
328 let spelled = |pitches: Vec<Pitch>| -> Vec<String> {
329 pitches.iter().map(Pitch::name_with_octave).collect()
330 };
331 assert_eq!(
332 spelled(scale.pitches_between(&tonic("D4"), &tonic("D5")).unwrap()),
333 ["D4", "Eb4", "Fb4", "F4", "D5"]
334 );
335 assert_eq!(
336 spelled(
337 scale
338 .pitches_between_descending(&tonic("D4"), &tonic("D5"))
339 .unwrap()
340 ),
341 ["D5", "F4", "E4", "D#4", "D4"]
342 );
343 }
344
345 #[test]
346 fn a_cycle_that_does_not_close_at_the_octave_is_walked_both_ways() {
347 let spelled = |pitches: Vec<Pitch>| -> Vec<String> {
349 pitches.iter().map(ToString::to_string).collect()
350 };
351 let fifths = StepScale::cyclical(tonic("C4"), &["P5"])
352 .unwrap()
353 .scale()
354 .unwrap();
355 assert_eq!(spelled(fifths.pitches().unwrap()), ["C4", "G4"]);
356 assert_eq!(
357 spelled(fifths.pitches_between(&tonic("G2"), &tonic("G6")).unwrap()),
358 ["Bb2", "F3", "C4", "G4", "D5", "A5", "E6"]
359 );
360 assert_eq!(fifths.degree_of(&tonic("G4")).unwrap(), Some(1));
362 assert_eq!(fifths.degree_of(&tonic("Bb2")).unwrap(), Some(1));
363 assert_eq!(fifths.degree_of(&tonic("F#3")).unwrap(), None);
366 assert_eq!(fifths.degree_of(&tonic("F3")).unwrap(), Some(1));
367
368 let mixed = StepScale::cyclical(tonic("C4"), &["M2", "m3"])
371 .unwrap()
372 .scale()
373 .unwrap();
374 assert_eq!(
375 mixed
376 .degrees_of_by(&tonic("G4"), crate::scale::DegreeComparison::Name)
377 .unwrap(),
378 [1, 2]
379 );
380 assert_eq!(
381 fifths
382 .next_pitch_above(&tonic("G4"), 2)
383 .unwrap()
384 .to_string(),
385 "A5"
386 );
387
388 let seconds = StepScale::cyclical(tonic("C4"), &["m2", "m2"])
389 .unwrap()
390 .scale()
391 .unwrap();
392 assert_eq!(
393 spelled(seconds.pitches_between(&tonic("G4"), &tonic("G5")).unwrap()),
394 [
395 "G4", "Ab4", "A4", "Bb4", "Cb5", "C5", "Db5", "D5", "Eb5", "Fb5", "F5", "Gb5", "G5"
396 ]
397 );
398
399 let whole = StepScale::sieve_by(tonic("D4"), "1@0", 2.0)
402 .unwrap()
403 .scale()
404 .unwrap();
405 assert_eq!(
406 spelled(whole.pitches_between(&tonic("C2"), &tonic("C3")).unwrap()),
407 ["C2", "D2", "Fb2", "Gb2", "Ab2", "Bb2", "C3"]
408 );
409 let quarter = StepScale::sieve_by(tonic("D4"), "1@0", 0.5)
410 .unwrap()
411 .scale()
412 .unwrap();
413 assert_eq!(
414 spelled(quarter.pitches_between(&tonic("C2"), &tonic("D2")).unwrap()),
415 ["C2", "C~2", "Db2", "D`2", "D2"]
416 );
417 }
418
419 #[test]
420 fn a_cycle_whose_sum_cannot_be_spelled_still_closes() {
421 let steps = ["m2"; 11];
422 let scale = StepScale::octave_repeating(tonic("C4"), &steps).unwrap();
423 let pitches = scale.pitches().unwrap();
424 assert_eq!(pitches.len(), 13);
425 assert_eq!(pitches.last().unwrap().ps(), 72.0);
426 }
427
428 #[test]
429 fn cyclical_walks_the_cycle_once() {
430 let scale = StepScale::cyclical(tonic("C4"), &["P5"]).unwrap();
431 assert_eq!(names(&scale), ["C", "G"]);
432
433 let scale = StepScale::cyclical(tonic("C4"), &["m3", "M3"]).unwrap();
434 assert_eq!(names(&scale), ["C", "Eb", "G"]);
435 }
436
437 #[test]
438 fn octave_repeating_closes_on_the_octave() {
439 let scale = StepScale::octave_repeating(tonic("C4"), &["m3", "M3"]).unwrap();
440 assert_eq!(names(&scale), ["C", "Eb", "G", "C"]);
441 let pitches = scale.pitches().unwrap();
442 assert_eq!(pitches.first().unwrap().octave(), Some(4));
443 assert_eq!(pitches.last().unwrap().octave(), Some(5));
444 }
445
446 #[test]
447 fn an_empty_interval_list_defaults_to_a_minor_second() {
448 assert_eq!(
451 names(&StepScale::cyclical(tonic("C4"), &[]).unwrap()),
452 ["C", "Db"]
453 );
454 assert_eq!(
455 names(&StepScale::octave_repeating(tonic("C4"), &[]).unwrap()),
456 ["C", "Db", "C"]
457 );
458 }
459
460 #[test]
461 fn the_closing_interval_comes_from_the_interval_sum_not_the_pitches() {
462 let scale = StepScale::octave_repeating(tonic("C4"), &["m2", "m2", "m2"]).unwrap();
467 assert_eq!(names(&scale), ["C", "Db", "D", "Eb", "B#"]);
468
469 let scale = StepScale::octave_repeating(tonic("C4"), &["M2", "M2", "m2"]).unwrap();
472 assert_eq!(names(&scale), ["C", "D", "E", "F", "C"]);
473 let scale = StepScale::octave_repeating(tonic("F#4"), &["M2", "M2", "m2"]).unwrap();
474 assert_eq!(names(&scale), ["F#", "G#", "A#", "B", "F#"]);
475 }
476
477 #[test]
478 fn a_cycle_wider_than_an_octave_closes_above_it() {
479 let scale = StepScale::octave_repeating(tonic("C4"), &["P5", "P5"]).unwrap();
483 assert_eq!(names(&scale), ["C", "G", "D", "C"]);
484 let pitches = scale.pitches().unwrap();
485 assert_eq!(pitches.first().unwrap().octave(), Some(4));
486 assert_eq!(pitches.last().unwrap().octave(), Some(6));
487 }
488
489 #[test]
490 fn sieve_scales_match_music21() {
491 let cases: [(&str, &str, &[&str]); 6] = [
492 ("C4", "3@0", &["C", "Eb"]),
493 ("D4", "3@0", &["D", "F"]),
494 ("E-4", "2@0", &["Eb", "F"]),
495 (
496 "C2",
497 "(-3@2 & 4) | (-3@1 & 4@1) | (3@2 & 4@2) | (-3 & 4@3)",
498 &["C", "D", "E", "F", "G", "A", "B", "C"],
499 ),
500 (
501 "C4",
502 "3@0|7@0",
503 &["C", "Eb", "F#", "G", "A", "C", "D", "Eb", "F#", "A"],
504 ),
505 ("C4", "{3@0|4@0}", &["C", "Eb", "E", "F#", "G#", "A", "C"]),
506 ];
507
508 for (tonic_name, expression, expected) in cases {
509 let scale = StepScale::sieve(tonic(tonic_name), expression).expect("sieve realizes");
510 assert_eq!(names(&scale), expected, "{tonic_name} {expression}");
511 }
512 }
513
514 #[test]
515 fn a_sieve_with_no_intervals_errors() {
516 assert!(StepScale::sieve(tonic("C4"), "3@1").is_err());
517 assert!(StepScale::sieve(tonic("C4"), "not a sieve").is_err());
518 }
519
520 #[test]
521 fn malformed_interval_names_error_instead_of_panicking() {
522 assert!(StepScale::cyclical(tonic("C4"), &["nonsense"]).is_err());
523 }
524}