diff --git a/src/music/interval.rs b/src/music/interval.rs index 09ef86b..2bfffa4 100644 --- a/src/music/interval.rs +++ b/src/music/interval.rs @@ -3,7 +3,7 @@ use std::{ ops::{Add, AddAssign, Neg, Sub}, }; -use super::pitch::PitchClass; +use super::{pitch::PitchClass, KeySig}; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] // 0..8 on piano @@ -71,6 +71,32 @@ impl Interval { pub const fn get_inner(self) -> i8 { self.0 } + + pub const fn major_scale() -> [Self; 8] { + [ + Self::zero(), + Self::tone(), + Self::tone(), + Self::semi_tone(), + Self::tone(), + Self::tone(), + Self::tone(), + Self::semi_tone(), + ] + } + + pub const fn natural_minor_scale() -> [Self; 8] { + [ + Self::zero(), + Self::tone(), + Self::semi_tone(), + Self::tone(), + Self::tone(), + Self::semi_tone(), + Self::tone(), + Self::tone(), + ] + } } impl From for Interval { @@ -220,3 +246,212 @@ impl From for AbsPitch { Self(int.0) } } + +impl AbsPitch { + pub fn diatonic_trans(self, key: KeySig, degrees: i8) -> Self { + if degrees == 0 { + return self; + } + + const DIATONIC_SIZE: i8 = 7; + let oct_size = Octave::semitones_number().0; + + let scale: Vec<_> = key + .get_intervals_scale() + .map(Self::from) + .take(7) // ignore the last one, it is an Octave higher than tonic + .collect(); + + let closest_index = scale + .iter() + .enumerate() + .min_by_key(|(_, x)| (self - **x).0.rem_euclid(oct_size)) + .map(|(i, _)| i) + .expect("Scale is non-empty"); + + let positive_shift = degrees.rem_euclid(DIATONIC_SIZE); + let whole_octaves = (degrees - positive_shift) / DIATONIC_SIZE; + + let interval = scale + .into_iter() + .cycle() + .nth(closest_index + positive_shift as usize) + .expect("Cycled non-empty scale has infinite items") + .0; + let shift = (interval + oct_size) + .checked_sub(self.0 % oct_size) + .unwrap() + % oct_size; + + self + Self(shift + (whole_octaves * oct_size)) + } +} + +#[cfg(test)] +mod tests { + use super::{ + super::{pitch::Pitch, KeySig}, + *, + }; + + #[test] + fn diatonic_trans_c_major() { + let oc4 = Octave::ONE_LINED; + let key = KeySig::Major(PitchClass::C); + + let pitches = [ + Pitch::new(PitchClass::C, oc4), + Pitch::new(PitchClass::D, oc4), + Pitch::new(PitchClass::E, oc4), + ]; + + let transposed: Vec<_> = pitches + .into_iter() + .map(|p| Pitch::from(p.abs().diatonic_trans(key, 2))) + .collect(); + + assert_eq!( + transposed, + [ + Pitch::new(PitchClass::E, oc4), + Pitch::new(PitchClass::F, oc4), + Pitch::new(PitchClass::G, oc4), + ] + ); + } + + #[test] + fn diatonic_trans_g_major() { + let oc4 = Octave::ONE_LINED; + let key = KeySig::Major(PitchClass::G); + + let pitches = [ + Pitch::new(PitchClass::C, oc4), + Pitch::new(PitchClass::D, oc4), + Pitch::new(PitchClass::E, oc4), + ]; + + let transposed: Vec<_> = pitches + .into_iter() + .map(|p| Pitch::from(p.abs().diatonic_trans(key, 2))) + .collect(); + + assert_eq!( + transposed, + [ + Pitch::new(PitchClass::E, oc4), + Pitch::new(PitchClass::Fs, oc4), + Pitch::new(PitchClass::G, oc4), + ] + ); + } + + #[test] + fn diatonic_trans_not_matching() { + let oc4 = Octave::ONE_LINED; + let key = KeySig::Major(PitchClass::C); + + let pitches = [ + Pitch::new(PitchClass::C, oc4), + Pitch::new(PitchClass::Ds, oc4), // this Pitch is not from the C-Major scale + Pitch::new(PitchClass::E, oc4), + ]; + + let transposed: Vec<_> = pitches + .into_iter() + .map(|p| Pitch::from(p.abs().diatonic_trans(key, 2))) + .collect(); + + assert_eq!( + transposed, + [ + Pitch::new(PitchClass::E, oc4), + Pitch::new(PitchClass::F, oc4), + Pitch::new(PitchClass::G, oc4), + ] + ); + } + + #[test] + fn diatonic_trans_wrapping_around_octave() { + let oc4 = Octave::ONE_LINED; + let key = KeySig::Major(PitchClass::C); + + let pitches = [ + Pitch::new(PitchClass::C, oc4), + Pitch::new(PitchClass::D, oc4), + Pitch::new(PitchClass::A, oc4), + ]; + + let transposed: Vec<_> = pitches + .into_iter() + .map(|p| Pitch::from(p.abs().diatonic_trans(key, 3))) + .collect(); + + assert_eq!( + transposed, + [ + Pitch::new(PitchClass::F, oc4), + Pitch::new(PitchClass::G, oc4), + Pitch::new(PitchClass::D, Octave::from(5)), + ] + ); + } + + #[test] + fn diatonic_trans_more_than_an_octave() { + let oc4 = Octave::ONE_LINED; + let key = KeySig::Major(PitchClass::C); + + let pitches = [ + Pitch::new(PitchClass::C, oc4), + Pitch::new(PitchClass::D, oc4), + Pitch::new(PitchClass::A, oc4), + ]; + + let transposed: Vec<_> = pitches + .into_iter() + // single octave is exactly 7 diatonic notes long + // so we should transpose one octave and 3 notes more + .map(|p| Pitch::from(p.abs().diatonic_trans(key, 10))) + .collect(); + + let oc5 = Octave::from(5); + assert_eq!( + transposed, + [ + Pitch::new(PitchClass::F, oc5), + Pitch::new(PitchClass::G, oc5), + Pitch::new(PitchClass::D, Octave::from(6)), + ] + ); + } + + #[test] + fn diatonic_trans_back_more_than_two_octaves() { + let oc4 = Octave::ONE_LINED; + let key = KeySig::Major(PitchClass::C); + + let pitches = [ + Pitch::new(PitchClass::C, oc4), + Pitch::new(PitchClass::Ds, oc4), + Pitch::new(PitchClass::A, oc4), + ]; + + let transposed: Vec<_> = pitches + .into_iter() + // shift 3 octaves back and two notes forward (7 * -3 + 2 = -19) + .map(|p| Pitch::from(p.abs().diatonic_trans(key, -19))) + .collect(); + + let oc1 = Octave::from(1); + assert_eq!( + transposed, + [ + Pitch::new(PitchClass::E, oc1), + Pitch::new(PitchClass::F, oc1), + Pitch::new(PitchClass::C, Octave::from(2)), + ] + ); + } +} diff --git a/src/music/mod.rs b/src/music/mod.rs index 98374a5..7fd4f28 100644 --- a/src/music/mod.rs +++ b/src/music/mod.rs @@ -61,6 +61,24 @@ impl KeySig { }; with_octave.map(Pitch::class) } + + const fn pitch_class(self) -> PitchClass { + match self { + Self::Major(pc) | Self::Minor(pc) => pc, + } + } + + pub fn get_intervals_scale(self) -> impl Iterator { + let scale = match self { + Self::Major(_) => Interval::major_scale(), + Self::Minor(_) => Interval::natural_minor_scale(), + }; + let tonic = self.pitch_class().into(); + scale.into_iter().scan(tonic, |state, p| { + *state += p; + Some(*state) + }) + } } #[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] @@ -410,6 +428,21 @@ mod tests { PitchClass::C, ] ); + + let i_scale: Vec<_> = KeySig::Major(PitchClass::C).get_intervals_scale().collect(); + assert_eq!( + i_scale, + [ + Interval::from(0), + Interval::from(2), + Interval::from(4), + Interval::from(5), + Interval::from(7), + Interval::from(9), + Interval::from(11), + Interval::from(12), + ] + ); } #[test] @@ -428,5 +461,20 @@ mod tests { PitchClass::G, ] ); + + let i_scale: Vec<_> = KeySig::Major(PitchClass::G).get_intervals_scale().collect(); + assert_eq!( + i_scale, + [ + Interval::from(7), + Interval::from(9), + Interval::from(11), + Interval::from(12), + Interval::from(14), + Interval::from(16), + Interval::from(18), + Interval::from(19), + ] + ); } } diff --git a/src/music/pitch.rs b/src/music/pitch.rs index 097e658..2009525 100644 --- a/src/music/pitch.rs +++ b/src/music/pitch.rs @@ -129,13 +129,13 @@ impl Pitch { self.trans(Interval::from(-1)) } - pub fn get_scale(self, intervals: &[I]) -> impl Iterator + '_ + pub fn get_scale(self, intervals: I) -> impl Iterator + 'static where - I: Copy + Into, + I: Iterator + 'static, + Int: Copy + Into, { intervals - .iter() - .scan(Interval::zero(), |tonic_distance, &interval| { + .scan(Interval::zero(), |tonic_distance, interval| { *tonic_distance += interval.into(); Some(*tonic_distance) }) @@ -143,11 +143,11 @@ impl Pitch { } pub fn major_scale(self) -> impl Iterator { - self.get_scale(&[0, 2, 2, 1, 2, 2, 2, 1]) + self.get_scale(Interval::major_scale().into_iter()) } pub fn natural_minor_scale(self) -> impl Iterator { - self.get_scale(&[0, 2, 1, 2, 2, 1, 2, 2]) + self.get_scale(Interval::natural_minor_scale().into_iter()) } }