rust_music/composition/
scale.rs

1use crate::num::u7;
2
3mod intervals {
4    pub static IONIAN: [u8; 6] = [2, 4, 5, 7, 9, 11];
5    pub static AEOLIAN: [u8; 6] = [2, 3, 5, 7, 8, 10];
6    pub static DORIAN: [u8; 6] = [2, 3, 5, 7, 9, 10];
7    pub static LYDIAN: [u8; 6] = [2, 4, 6, 7, 9, 11];
8    pub static MIXOLYDIAN: [u8; 6] = [2, 4, 5, 7, 9, 10];
9    pub static PHRYGIAN: [u8; 6] = [1, 3, 5, 7, 8, 10];
10    pub static LOCRIAN: [u8; 6] = [1, 3, 5, 6, 8, 10];
11    pub static HARMONIC_MINOR: [u8; 6] = [2, 3, 5, 7, 8, 11];
12    pub static MELODIC_MINOR: [u8; 6] = [2, 3, 5, 7, 9, 11];
13}
14
15// The mode/type of a Scale
16#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum ScaleMode {
18    #[default]
19    Ionian, // Major
20    Aeolian, // Natural Minor
21    Dorian,
22    Lydian,
23    Mixolydian,
24    Phrygian,
25    Locrian,
26    HarmonicMinor,
27    MelodicMinor,
28}
29
30impl ScaleMode {
31    pub const MAJOR: Self = Self::Ionian;
32    pub const NATURAL_MINOR: Self = Self::Aeolian;
33
34    // Returns the list of intervals for this mode.
35    pub fn intervals(&self) -> &'static [u8] {
36        match *self {
37            Self::Ionian => &intervals::IONIAN,
38            Self::Aeolian => &intervals::AEOLIAN,
39            Self::Dorian => &intervals::DORIAN,
40            Self::Lydian => &intervals::LYDIAN,
41            Self::Mixolydian => &intervals::MIXOLYDIAN,
42            Self::Phrygian => &intervals::PHRYGIAN,
43            Self::Locrian => &intervals::LOCRIAN,
44            Self::HarmonicMinor => &intervals::HARMONIC_MINOR,
45            Self::MelodicMinor => &intervals::MELODIC_MINOR,
46        }
47    }
48}
49
50// A Scale defined by a starting pitch and a mode
51#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
52pub struct Scale {
53    tonic_pitch: u7,
54    scale_mode: ScaleMode,
55}
56
57impl Scale {
58    /// Creates a new Scale
59    pub fn new(tonic_pitch: u7, scale_mode: ScaleMode) -> Self {
60        Self {
61            tonic_pitch,
62            scale_mode,
63        }
64    }
65
66    /// Returns an iterator that iterates over all pitches in the scale once
67    pub fn pitches(&self) -> ScalePitchesIterator<'static> {
68        let intervals = self.scale_mode.intervals();
69        ScalePitchesIterator::new(self.tonic_pitch, intervals, intervals.len() + 1)
70    }
71
72    /// Returns an iterator that iterates over all pitches in the scale and can continue
73    /// on the next octaves until `length` pitches have been issued or the pitch has reached
74    /// a value too high for MIDI.
75    pub fn n_pitches(&self, num_pitches: usize) -> ScalePitchesIterator<'static> {
76        ScalePitchesIterator::new(self.tonic_pitch, self.scale_mode.intervals(), num_pitches)
77    }
78}
79
80/// Generates a series of pitches from a given series of intervals and a base (tonic) pitch.
81/// If the requested length is longer than the list of intervals, the iterator continues the
82/// scale on the next octave(s).
83/// The first pitch returned is always the tonic. The iterator stops when it has issued `length`
84/// pitches or when the next pitch would be higher than the maximum MIDI pitch (max u7).
85/// This can either be returned by Scale::pitches() for a standard scale or
86/// created with an arbitrary series of pitches.
87#[derive(Debug, Clone)]
88pub struct ScalePitchesIterator<'a> {
89    intervals: &'a [u8],
90    iteration: usize,
91    length: usize,
92    tonic: u7,
93}
94
95impl<'a> ScalePitchesIterator<'a> {
96    /// Creates a new ScalePitchesIterator with the specified fundamental and list of intervals
97    ///
98    /// # Arguments
99    ///
100    /// * `fundamental_pitch` - The pitch of the fundamental (between 0 and 127)
101    /// * `intervals` - The list of intervals of the scale (relative to the fundamental)
102    /// * `length` - The number of pitches to iterate over
103    pub fn new(tonic: u7, intervals: &'a [u8], length: usize) -> Self {
104        Self {
105            intervals,
106            iteration: 0,
107            length,
108            tonic,
109        }
110    }
111}
112
113impl Iterator for ScalePitchesIterator<'_> {
114    type Item = u7;
115    fn next(&mut self) -> Option<Self::Item> {
116        if self.iteration >= self.length {
117            return None;
118        }
119
120        let pos = self.iteration % (self.intervals.len() + 1);
121        self.iteration += 1;
122
123        if self.iteration == 1 {
124            return Some(self.tonic);
125        }
126        if pos == 0 {
127            if u7::max_value().as_int() - 12 < self.tonic {
128                return None;
129            }
130            self.tonic += 12.into();
131            return Some(self.tonic);
132        }
133        let interval = self.intervals[pos - 1];
134        if u7::max_value().as_int() - interval < self.tonic {
135            return None;
136        }
137        Some(self.tonic + u7::new(interval))
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::ScalePitchesIterator;
144    use crate::*;
145
146    #[test]
147    fn scale_pitches_iterator() -> Result<()> {
148        let pitches = vec![3, 5, 7];
149        let iter = ScalePitchesIterator::new(5.into(), &pitches, 6);
150
151        let s =
152            Note::new_sequence(rhythm::CROTCHET, dynamic::MF, iter).collect::<Result<Vec<_>>>()?;
153        let expected = vec![
154            Note::new(5.into(), rhythm::CROTCHET, dynamic::MF)?,
155            Note::new(8.into(), rhythm::CROTCHET, dynamic::MF)?,
156            Note::new(10.into(), rhythm::CROTCHET, dynamic::MF)?,
157            Note::new(12.into(), rhythm::CROTCHET, dynamic::MF)?,
158            Note::new(17.into(), rhythm::CROTCHET, dynamic::MF)?,
159            Note::new(20.into(), rhythm::CROTCHET, dynamic::MF)?,
160        ];
161        assert_eq!(expected, s);
162        Ok(())
163    }
164}