diff --git a/Cargo.toml b/Cargo.toml index 3113293..3e8c7ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,10 @@ categories = ["multimedia", "multimedia::audio", "multimedia::encoding"] [dependencies] thiserror = "1.0.37" midly = "0.5.3" + +[features] +composition = [] + +[[example]] +name = "scales_example" +required-features = ["composition"] \ No newline at end of file diff --git a/examples/praeludium_no1_multi_phrase.rs b/examples/praeludium_no1_multi_phrase.rs index bad76dd..a5a254f 100644 --- a/examples/praeludium_no1_multi_phrase.rs +++ b/examples/praeludium_no1_multi_phrase.rs @@ -53,7 +53,7 @@ fn right_hand() -> Result { right_hand.add_sequential_notes(Note::new_sequence( SEMIQUAVER, MF, - &[pitch1, pitch2, pitch3], + [pitch1, pitch2, pitch3], ))?; } } diff --git a/examples/praeludium_no1_organ_piano.rs b/examples/praeludium_no1_organ_piano.rs index 68c0e5c..fc3ca19 100644 --- a/examples/praeludium_no1_organ_piano.rs +++ b/examples/praeludium_no1_organ_piano.rs @@ -51,7 +51,7 @@ fn right_hand() -> Result { right_hand.add_sequential_notes(Note::new_sequence( SEMIQUAVER, MF, - &[pitch1, pitch2, pitch3], + [pitch1, pitch2, pitch3], ))?; } } diff --git a/examples/praeludium_no1_single_phrase.rs b/examples/praeludium_no1_single_phrase.rs index bd98107..c9eee42 100644 --- a/examples/praeludium_no1_single_phrase.rs +++ b/examples/praeludium_no1_single_phrase.rs @@ -48,7 +48,7 @@ fn phrase() -> Result { phrase.add_sequential_notes(Note::new_sequence( SEMIQUAVER, MF, - &[pitch3, pitch4, pitch5], + [pitch3, pitch4, pitch5], ))?; } } diff --git a/examples/scales_example.rs b/examples/scales_example.rs new file mode 100644 index 0000000..557c362 --- /dev/null +++ b/examples/scales_example.rs @@ -0,0 +1,31 @@ +use std::error::Error; +use std::fs::File; +use std::result::Result; + +use rust_music::{ + composition::Scale, composition::ScaleMode, compute_pitch, dynamic::*, rhythm::*, Accidental, + Instrument, Note, NoteName, Part, Phrase, Score, Tempo, +}; + +fn main() -> Result<(), Box> { + // Create a simple C Minor Scale on octave 4 (this requires the `composition` feature) + let s = Scale::new( + compute_pitch(NoteName::Do, Accidental::Natural, 4)?, + ScaleMode::Aeolian, + ); + + // Create a phrase that just plays the scale as a sequence of quavers (half beat) + let phrase = Phrase::from_notes_sequence(Note::new_sequence(QUAVER, MF, s.n_pitches(15)))?; + + // Create a piano part that plays the phrase from beat 0 + let mut piano_part = Part::new(Instrument::AcousticGrandPiano); + piano_part.add_phrase(phrase, 0.); + + // Create a score with a tempo of 60 (one beat per second) and add both parts + let mut score = Score::new("my score", Tempo::new(60)?, None); + score.add_part(piano_part); + + // Write the score to a MIDI file for playback + score.write_midi_file(File::create("scale_example.mid")?)?; + Ok(()) +} diff --git a/src/chord.rs b/src/chord.rs index ea485fb..a54250f 100644 --- a/src/chord.rs +++ b/src/chord.rs @@ -61,7 +61,8 @@ impl Chord { /// * `pitches`: list of the pitches of the notes of the `Chord` /// * `dynamic`: dynamic that each note in the `Chord` will take pub fn from_pitches(rhythm: f64, dynamic: u7, pitches: &[u7]) -> Result { - let notes: Result> = Note::new_sequence(rhythm, dynamic, pitches).collect(); + let notes: Result> = + Note::new_sequence(rhythm, dynamic, pitches.iter().copied()).collect(); Self::new(rhythm, notes?) } diff --git a/src/composition/mod.rs b/src/composition/mod.rs new file mode 100644 index 0000000..b629584 --- /dev/null +++ b/src/composition/mod.rs @@ -0,0 +1,3 @@ +mod scale; + +pub use scale::*; diff --git a/src/composition/scale.rs b/src/composition/scale.rs new file mode 100644 index 0000000..f6bb4de --- /dev/null +++ b/src/composition/scale.rs @@ -0,0 +1,158 @@ +use crate::num::u7; + +mod intervals { + pub static IONIAN: [u8; 6] = [2, 4, 5, 7, 9, 11]; + pub static AEOLIAN: [u8; 6] = [2, 3, 5, 7, 8, 10]; + pub static DORIAN: [u8; 6] = [2, 3, 5, 7, 9, 10]; + pub static LYDIAN: [u8; 6] = [2, 4, 6, 7, 9, 11]; + pub static MIXOLYDIAN: [u8; 6] = [2, 4, 5, 7, 9, 10]; + pub static PHRYGIAN: [u8; 6] = [1, 3, 5, 7, 8, 10]; + pub static LOCRIAN: [u8; 6] = [1, 3, 5, 6, 8, 10]; + pub static HARMONIC_MINOR: [u8; 6] = [2, 3, 5, 7, 8, 11]; + pub static MELODIC_MINOR: [u8; 6] = [2, 3, 5, 7, 9, 11]; +} + +// The mode/type of a Scale +pub enum ScaleMode { + Ionian, // Major + Aeolian, // Natural Minor + Dorian, + Lydian, + Mixolydian, + Phrygian, + Locrian, + HarmonicMinor, + MelodicMinor, +} + +impl ScaleMode { + // Returns the list of intervals for this mode. + pub fn intervals(&self) -> &'static [u8] { + match *self { + Self::Ionian => &intervals::IONIAN, + Self::Aeolian => &intervals::AEOLIAN, + Self::Dorian => &intervals::DORIAN, + Self::Lydian => &intervals::LYDIAN, + Self::Mixolydian => &intervals::MIXOLYDIAN, + Self::Phrygian => &intervals::PHRYGIAN, + Self::Locrian => &intervals::LOCRIAN, + Self::HarmonicMinor => &intervals::HARMONIC_MINOR, + Self::MelodicMinor => &intervals::MELODIC_MINOR, + } + } +} + +// A Scale defined by a starting pitch and a mode +pub struct Scale { + tonic_pitch: u7, + scale_mode: ScaleMode, +} + +impl Scale { + /// Creates a new Scale + pub fn new(tonic_pitch: u7, scale_mode: ScaleMode) -> Self { + Self { + tonic_pitch, + scale_mode, + } + } + + /// Returns an iterator that iterates over all pitches in the scale once + pub fn pitches(&self) -> ScalePitchesIterator<'static> { + let intervals = self.scale_mode.intervals(); + ScalePitchesIterator::new(self.tonic_pitch, intervals, intervals.len() + 1) + } + + /// Returns an iterator that iterates over all pitches in the scale and can continue + /// on the next octaves until `length` pitches have been issued or the pitch has reached + /// a value too high for MIDI. + pub fn n_pitches(&self, num_pitches: usize) -> ScalePitchesIterator<'static> { + ScalePitchesIterator::new(self.tonic_pitch, self.scale_mode.intervals(), num_pitches) + } +} + +/// Generates a series of pitches from a given series of intervals and a base (tonic) pitch. +/// If the requested length is longer than the list of intervals, the iterator continues the +/// scale on the next octave(s). +/// The first pitch returned is always the tonic. The iterator stops when it has issued `length` +/// pitches or when the next pitch would be higher than the maximum MIDI pitch (max u7). +/// This can either be returned by Scale::pitches() for a standard scale or +/// created with an arbitrary series of pitches. +#[derive(Debug, Clone)] +pub struct ScalePitchesIterator<'a> { + intervals: &'a [u8], + iteration: usize, + length: usize, + tonic: u7, +} + +impl<'a> ScalePitchesIterator<'a> { + /// Creates a new ScalePitchesIterator with the specified fundamental and list of intervals + /// + /// # Arguments + /// + /// * `fundamental_pitch` - The pitch of the fundamental (between 0 and 127) + /// * `intervals` - The list of intervals of the scale (relative to the fundamental) + /// * `length` - The number of pitches to iterate over + pub fn new(tonic: u7, intervals: &'a [u8], length: usize) -> Self { + Self { + intervals, + iteration: 0, + length, + tonic, + } + } +} + +impl<'a> Iterator for ScalePitchesIterator<'a> { + type Item = u7; + fn next(&mut self) -> Option { + if self.iteration >= self.length { + return None; + } + + let pos = self.iteration % (self.intervals.len() + 1); + self.iteration += 1; + + if self.iteration == 1 { + return Some(self.tonic); + } + if pos == 0 { + if u7::max_value().as_int() - 12 < self.tonic { + return None; + } + self.tonic += 12.into(); + return Some(self.tonic); + } + let interval = self.intervals[pos - 1]; + if u7::max_value().as_int() - interval < self.tonic { + return None; + } + Some(self.tonic + u7::new(interval)) + } +} + +#[cfg(test)] +mod tests { + use super::ScalePitchesIterator; + use crate::*; + + #[test] + fn scale_pitches_iterator() -> Result<()> { + let pitches = vec![3, 5, 7]; + let iter = ScalePitchesIterator::new(5.into(), &pitches, 6); + + let s = + Note::new_sequence(rhythm::CROTCHET, dynamic::MF, iter).collect::>>()?; + let expected = vec![ + Note::new(5.into(), rhythm::CROTCHET, dynamic::MF)?, + Note::new(8.into(), rhythm::CROTCHET, dynamic::MF)?, + Note::new(10.into(), rhythm::CROTCHET, dynamic::MF)?, + Note::new(12.into(), rhythm::CROTCHET, dynamic::MF)?, + Note::new(17.into(), rhythm::CROTCHET, dynamic::MF)?, + Note::new(20.into(), rhythm::CROTCHET, dynamic::MF)?, + ]; + assert_eq!(expected, s); + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 7b91b62..544f634 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,9 @@ mod part; mod phrase; pub mod score; +#[cfg(feature = "composition")] +pub mod composition; + pub use crate::errors::Error; pub type Result = core::result::Result; diff --git a/src/note.rs b/src/note.rs index 3af4440..1d03fd0 100644 --- a/src/note.rs +++ b/src/note.rs @@ -72,12 +72,14 @@ impl Note { /// Creates an iterator of notes with idential rhythms and dynamic which can be added directly /// to a phrase using `phrase.add_sequential_notes` to be played sequentially or collected as /// a vector to use in a `Chord`. - pub fn new_sequence( + pub fn new_sequence<'a, PitchIter: IntoIterator + 'a>( rhythm: f64, dynamic: u7, - pitches: &[u7], - ) -> impl std::iter::Iterator> + '_ { - pitches.iter().map(move |p| Note::new(*p, rhythm, dynamic)) + pitches: PitchIter, + ) -> impl std::iter::Iterator> + 'a { + pitches + .into_iter() + .map(move |p| Note::new(p, rhythm, dynamic)) } /// Returns the pitch of the note