use crate::core::{Accidental, Pitch, Step};
use super::KeyMode;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Scale {
tonic: Pitch,
mode: KeyMode,
}
impl Scale {
pub fn new(tonic: Pitch, mode: KeyMode) -> Self {
Self { tonic, mode }
}
pub fn major(tonic: Pitch) -> Self {
Self::new(tonic, KeyMode::Major)
}
pub fn natural_minor(tonic: Pitch) -> Self {
Self::new(tonic, KeyMode::Minor)
}
pub fn tonic(&self) -> &Pitch {
&self.tonic
}
pub fn mode(&self) -> KeyMode {
self.mode
}
pub fn intervals_for_mode(mode: KeyMode) -> [i32; 7] {
match mode {
KeyMode::Major => [0, 2, 4, 5, 7, 9, 11],
KeyMode::Dorian => [0, 2, 3, 5, 7, 9, 10],
KeyMode::Phrygian => [0, 1, 3, 5, 7, 8, 10],
KeyMode::Lydian => [0, 2, 4, 6, 7, 9, 11],
KeyMode::Mixolydian => [0, 2, 4, 5, 7, 9, 10],
KeyMode::Minor | KeyMode::Aeolian => [0, 2, 3, 5, 7, 8, 10],
KeyMode::Locrian => [0, 1, 3, 5, 6, 8, 10],
}
}
pub fn pitches(&self) -> Vec<Pitch> {
let intervals = Self::intervals_for_mode(self.mode);
let tonic_step_index = self.tonic.step().index();
let tonic_pc = self.tonic.pitch_class() as i32;
(0..7)
.map(|i| {
let letter = Step::from_index(tonic_step_index + i as i32);
let target_pc = (tonic_pc + intervals[i as usize]).rem_euclid(12);
let natural_pc = letter.pitch_class() as i32;
let alter = shortest_alter(natural_pc, target_pc);
Pitch::from_parts(letter, None, accidental_from_alter(alter))
})
.collect()
}
pub fn pitch_for_degree(&self, degree: u8) -> Option<Pitch> {
if degree == 0 || degree > 7 {
return None;
}
self.pitches().into_iter().nth(degree as usize - 1)
}
pub fn degree_of(&self, pitch: &Pitch) -> Option<u8> {
self.pitches()
.iter()
.position(|p| p.step() == pitch.step() && p.accidental() == pitch.accidental())
.map(|i| i as u8 + 1)
}
}
fn shortest_alter(natural_pc: i32, target_pc: i32) -> i32 {
let mut diff = (target_pc - natural_pc).rem_euclid(12);
if diff > 6 {
diff -= 12;
}
diff
}
fn accidental_from_alter(alter: i32) -> Option<Accidental> {
match alter {
-3 => Some(Accidental::TripleFlat),
-2 => Some(Accidental::DoubleFlat),
-1 => Some(Accidental::Flat),
0 => None,
1 => Some(Accidental::Sharp),
2 => Some(Accidental::DoubleSharp),
3 => Some(Accidental::TripleSharp),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn c(step: Step) -> Pitch {
Pitch::from_parts(step, None, None)
}
#[test]
fn test_major_scale_pitches() {
let scale = Scale::major(c(Step::C));
let pitches: Vec<String> = scale.pitches().iter().map(|p| p.name()).collect();
assert_eq!(pitches, vec!["C", "D", "E", "F", "G", "A", "B"]);
}
#[test]
fn test_major_scale_with_sharps() {
let scale = Scale::major(c(Step::G));
let pitches: Vec<String> = scale.pitches().iter().map(|p| p.name()).collect();
assert_eq!(pitches, vec!["G", "A", "B", "C", "D", "E", "F#"]);
}
#[test]
fn test_natural_minor_scale() {
let scale = Scale::natural_minor(c(Step::A));
let pitches: Vec<String> = scale.pitches().iter().map(|p| p.name()).collect();
assert_eq!(pitches, vec!["A", "B", "C", "D", "E", "F", "G"]);
}
#[test]
fn test_pitch_for_degree_and_degree_of() {
let scale = Scale::major(c(Step::G));
let fifth = scale.pitch_for_degree(5).unwrap();
assert_eq!(fifth.name(), "D");
let leading_tone = scale.pitch_for_degree(7).unwrap();
assert_eq!(leading_tone.name(), "F#");
assert_eq!(scale.degree_of(&leading_tone), Some(7));
assert_eq!(scale.pitch_for_degree(0), None);
assert_eq!(scale.pitch_for_degree(8), None);
}
#[test]
fn test_dorian_mode() {
let scale = Scale::new(c(Step::D), KeyMode::Dorian);
let pitches: Vec<String> = scale.pitches().iter().map(|p| p.name()).collect();
assert_eq!(pitches, vec!["D", "E", "F", "G", "A", "B", "C"]);
}
}