Skip to content

Commit

Permalink
src: composition: add Scale composition utilities
Browse files Browse the repository at this point in the history
  • Loading branch information
paveyry committed Dec 21, 2023
1 parent 6849368 commit 9c077f5
Show file tree
Hide file tree
Showing 10 changed files with 213 additions and 8 deletions.
7 changes: 7 additions & 0 deletions Cargo.toml
Expand Up @@ -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"]
2 changes: 1 addition & 1 deletion examples/praeludium_no1_multi_phrase.rs
Expand Up @@ -53,7 +53,7 @@ fn right_hand() -> Result<Phrase> {
right_hand.add_sequential_notes(Note::new_sequence(
SEMIQUAVER,
MF,
&[pitch1, pitch2, pitch3],
[pitch1, pitch2, pitch3],
))?;
}
}
Expand Down
2 changes: 1 addition & 1 deletion examples/praeludium_no1_organ_piano.rs
Expand Up @@ -51,7 +51,7 @@ fn right_hand() -> Result<Phrase> {
right_hand.add_sequential_notes(Note::new_sequence(
SEMIQUAVER,
MF,
&[pitch1, pitch2, pitch3],
[pitch1, pitch2, pitch3],
))?;
}
}
Expand Down
2 changes: 1 addition & 1 deletion examples/praeludium_no1_single_phrase.rs
Expand Up @@ -48,7 +48,7 @@ fn phrase() -> Result<Phrase> {
phrase.add_sequential_notes(Note::new_sequence(
SEMIQUAVER,
MF,
&[pitch3, pitch4, pitch5],
[pitch3, pitch4, pitch5],
))?;
}
}
Expand Down
31 changes: 31 additions & 0 deletions 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<dyn Error>> {
// 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(())
}
3 changes: 2 additions & 1 deletion src/chord.rs
Expand Up @@ -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<Self> {
let notes: Result<Vec<_>> = Note::new_sequence(rhythm, dynamic, pitches).collect();
let notes: Result<Vec<_>> =
Note::new_sequence(rhythm, dynamic, pitches.iter().copied()).collect();
Self::new(rhythm, notes?)
}

Expand Down
3 changes: 3 additions & 0 deletions src/composition/mod.rs
@@ -0,0 +1,3 @@
mod scale;

pub use scale::*;
158 changes: 158 additions & 0 deletions 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<Self::Item> {
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::<Result<Vec<_>>>()?;
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(())
}
}
3 changes: 3 additions & 0 deletions src/lib.rs
Expand Up @@ -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<T> = core::result::Result<T, Error>;

Expand Down
10 changes: 6 additions & 4 deletions src/note.rs
Expand Up @@ -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<Item = u7> + 'a>(
rhythm: f64,
dynamic: u7,
pitches: &[u7],
) -> impl std::iter::Iterator<Item = Result<Note>> + '_ {
pitches.iter().map(move |p| Note::new(*p, rhythm, dynamic))
pitches: PitchIter,
) -> impl std::iter::Iterator<Item = Result<Note>> + 'a {
pitches
.into_iter()
.map(move |p| Note::new(p, rhythm, dynamic))
}

/// Returns the pitch of the note
Expand Down

0 comments on commit 9c077f5

Please sign in to comment.