Skip to content

Commit

Permalink
diatonic scales
Browse files Browse the repository at this point in the history
  • Loading branch information
tsionyx committed Feb 10, 2024
1 parent 37a5c1b commit 3976ac2
Show file tree
Hide file tree
Showing 3 changed files with 254 additions and 7 deletions.
201 changes: 200 additions & 1 deletion src/music/interval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<i8> for Interval {
Expand Down Expand Up @@ -220,3 +246,176 @@ impl From<Interval> for AbsPitch {
Self(int.0)
}
}

impl AbsPitch {
pub fn diatonic_trans(self, key: KeySig, degrees: usize) -> Self {
if degrees == 0 {
return self;
}

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 twelve = Octave::semitones_number().0;
let closest_index = scale
.iter()
.enumerate()
.min_by_key(|(_, x)| (((self - **x).0 % twelve) + twelve) % twelve)
.map(|(i, _)| i)
.expect("Scale is non-empty");

let interval = scale
.into_iter()
.cycle()
.nth(closest_index + degrees)
.expect("Cycled non-empty scale has infinite items")
.0;
let shift = (interval + twelve).checked_sub(self.0 % twelve).unwrap() % twelve;

self + Self(shift)
}
}

#[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)),
]
);
}
}
48 changes: 48 additions & 0 deletions src/music/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,24 @@ impl KeySig {
};
with_octave.map(Pitch::class)
}

fn pitch_class(self) -> PitchClass {

Check failure on line 65 in src/music/mod.rs

View workflow job for this annotation

GitHub Actions / Clippy on rust 'beta'

this could be a `const fn`

Check failure on line 65 in src/music/mod.rs

View workflow job for this annotation

GitHub Actions / Clippy on rust 'stable'

this could be a `const fn`
match self {
KeySig::Major(pc) | KeySig::Minor(pc) => pc,

Check failure on line 67 in src/music/mod.rs

View workflow job for this annotation

GitHub Actions / Clippy on rust 'beta'

unnecessary structure name repetition

Check failure on line 67 in src/music/mod.rs

View workflow job for this annotation

GitHub Actions / Clippy on rust 'beta'

unnecessary structure name repetition

Check failure on line 67 in src/music/mod.rs

View workflow job for this annotation

GitHub Actions / Clippy on rust 'stable'

unnecessary structure name repetition

Check failure on line 67 in src/music/mod.rs

View workflow job for this annotation

GitHub Actions / Clippy on rust 'stable'

unnecessary structure name repetition
}
}

pub fn get_intervals_scale(self) -> impl Iterator<Item = Interval> {
let scale = match self {
KeySig::Major(_) => Interval::major_scale(),

Check failure on line 73 in src/music/mod.rs

View workflow job for this annotation

GitHub Actions / Clippy on rust 'beta'

unnecessary structure name repetition

Check failure on line 73 in src/music/mod.rs

View workflow job for this annotation

GitHub Actions / Clippy on rust 'stable'

unnecessary structure name repetition
KeySig::Minor(_) => Interval::natural_minor_scale(),

Check failure on line 74 in src/music/mod.rs

View workflow job for this annotation

GitHub Actions / Clippy on rust 'beta'

unnecessary structure name repetition

Check failure on line 74 in src/music/mod.rs

View workflow job for this annotation

GitHub Actions / Clippy on rust 'stable'

unnecessary structure name repetition
};
let tonic = self.pitch_class().into();
scale.into_iter().scan(tonic, |state, p| {
*state += p;
Some(*state)
})
}
}

#[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord)]
Expand Down Expand Up @@ -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]
Expand All @@ -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),
]
);
}
}
12 changes: 6 additions & 6 deletions src/music/pitch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,25 +129,25 @@ impl Pitch {
self.trans(Interval::from(-1))
}

pub fn get_scale<I>(self, intervals: &[I]) -> impl Iterator<Item = Self> + '_
pub fn get_scale<I, Int>(self, intervals: I) -> impl Iterator<Item = Self> + 'static
where
I: Copy + Into<Interval>,
I: Iterator<Item = Int> + 'static,
Int: Copy + Into<Interval>,
{
intervals
.iter()
.scan(Interval::zero(), |tonic_distance, &interval| {
.scan(Interval::zero(), |tonic_distance, interval| {
*tonic_distance += interval.into();
Some(*tonic_distance)
})
.map(move |distance| self.trans(distance))
}

pub fn major_scale(self) -> impl Iterator<Item = Self> {
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<Item = Self> {
self.get_scale(&[0, 2, 1, 2, 2, 1, 2, 2])
self.get_scale(Interval::natural_minor_scale().into_iter())
}
}

Expand Down

0 comments on commit 3976ac2

Please sign in to comment.