Skip to content

Commit

Permalink
Merge pull request #17 from henryksloan/chord-inversion
Browse files Browse the repository at this point in the history
Chord inversions
  • Loading branch information
ozankasikci authored Aug 19, 2020
2 parents 6d972be + 252dfe8 commit 7cff5d6
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 42 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,6 @@ Notes:
- [ ] Add support for arbitrary accidentals
- [ ] Add support for the alternative names of the modes to regex parser
- [ ] Properly display enharmonic spelling
- [ ] Add inversion support for chords
- [x] Add inversion support for chords
- [ ] Add support for [cadence][1]s
- [ ] Add a mechanism to find the chord from the given notes
2 changes: 1 addition & 1 deletion src/bin/rustmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ fn main() {
.subcommand(App::new("list").about("Prints out the available chords"))
.arg(
Arg::with_name("args")
.help("chord args, examples:\nC minor\nAb augmented major seventh")
.help("chord args, examples:\nC minor\nAb augmented major seventh\nF# dominant seventh / C#\nC/1")
.multiple(true),
),
)
Expand Down
119 changes: 94 additions & 25 deletions src/chord/chord.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::chord::errors::ChordError;
use crate::chord::number::Number::Triad;
use crate::chord::{Number, Quality};
use crate::interval::Interval;
use crate::note::{Note, Notes, PitchClass};
use crate::note::{Note, NoteError, Notes, PitchClass};

/// A chord.
#[derive(Debug, Clone)]
Expand All @@ -13,18 +13,43 @@ pub struct Chord {
pub octave: u8,
/// The intervals within the chord.
pub intervals: Vec<Interval>,
/// The quiality of the chord; major, minor, diminished, etc.
/// The quality of the chord: major, minor, diminished, etc.
pub quality: Quality,
/// The superscript number of the chord (3, 7, maj7, etc).
/// The superscript number of the chord: 3, 7, maj7, etc.
pub number: Number,
/// The inversion of the chord: 0=root position, 1=first inversion, etc.
pub inversion: u8,
}

impl Chord {
/// Create a new chord.
pub fn new(root: PitchClass, quality: Quality, number: Number) -> Self {
Self::with_inversion(root, quality, number, 0)
}

/// Create a new chord with a given inversion.
pub fn with_inversion(
root: PitchClass,
quality: Quality,
number: Number,
inversion: u8,
) -> Self {
let intervals = Self::chord_intervals(quality, number);
let inversion = inversion % (intervals.len() + 1) as u8;
Chord {
root,
octave: 4,
intervals,
quality,
number,
inversion,
}
}

pub fn chord_intervals(quality: Quality, number: Number) -> Vec<Interval> {
use Number::*;
use Quality::*;
let intervals = match (&quality, &number) {
match (&quality, &number) {
(Major, Triad) => Interval::from_semitones(&[4, 3]),
(Minor, Triad) => Interval::from_semitones(&[3, 4]),
(Suspended2, Triad) => Interval::from_semitones(&[2, 5]),
Expand All @@ -49,36 +74,62 @@ impl Chord {
(Minor, Thirteenth) => Interval::from_semitones(&[3, 4, 3, 4, 3, 4]),
_ => Interval::from_semitones(&[4, 3]),
}
.unwrap();

Chord {
root,
octave: 4,
intervals,
quality,
number,
}
.unwrap()
}

/// Parse a chord using a regex.
pub fn from_regex(string: &str) -> Result<Self, ChordError> {
let (pitch_class, pitch_match) = PitchClass::from_regex(&string)?;

let (quality, quality_match_option) =
Quality::from_regex(&string[pitch_match.end()..].trim())?;
let slash_option = string.find('/');
let bass_note_result = if let Some(slash) = slash_option {
PitchClass::from_regex(&string[slash + 1..].trim())
} else {
Err(NoteError::InvalidPitch)
};
let inversion_num_option = if let Some(slash) = slash_option {
string[slash + 1..].trim().parse::<u8>().ok()
} else {
None
};

let (quality, quality_match_option) = Quality::from_regex(
&string[pitch_match.end()..slash_option.unwrap_or_else(|| string.len())].trim(),
)?;

let number = if let Some(quality_match) = quality_match_option {
Number::from_regex(&string[quality_match.end()..])
.unwrap_or((Triad, None))
.0
} else {
Triad
};

let chord = Chord::with_inversion(
pitch_class,
quality,
number,
inversion_num_option.unwrap_or(0),
);

Ok(match quality_match_option {
// there is
Some(quality_match) => {
let (number, _) =
Number::from_regex(&string[quality_match.end()..]).unwrap_or((Triad, None));
if let Ok((bass_note, _)) = bass_note_result {
let inversion = chord
.notes()
.iter()
.position(|note| note.pitch_class == bass_note)
.unwrap_or(0);

Chord::new(pitch_class, quality, number)
if inversion != 0 {
return Ok(Chord::with_inversion(
pitch_class,
quality,
number,
inversion as u8,
));
}
}

// return a Triad by default
None => Chord::new(pitch_class, quality, Triad),
})
Ok(chord)
}
}

Expand All @@ -88,7 +139,24 @@ impl Notes for Chord {
octave: self.octave,
pitch_class: self.root,
};
Interval::to_notes(root_note, self.intervals.clone())
let mut notes = Interval::to_notes(root_note, self.intervals.clone());
notes.rotate_left(self.inversion as usize);

// Normalize to the correct octave
if notes[0].octave > self.octave {
let diff = notes[0].octave - self.octave;
notes.iter_mut().for_each(|note| note.octave -= diff);
}

// Ensure that octave increments at the right notes
for i in 1..notes.len() {
if notes[i].pitch_class as u8 <= notes[i - 1].pitch_class as u8 {
notes[i].octave = notes[i - 1].octave + 1;
} else if notes[i].octave < notes[i - 1].octave {
notes[i].octave = notes[i - 1].octave;
}
}
notes
}
}

Expand All @@ -100,6 +168,7 @@ impl Default for Chord {
intervals: vec![],
quality: Quality::Major,
number: Number::Triad,
inversion: 0,
}
}
}
83 changes: 68 additions & 15 deletions tests/chord/test_chord.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,76 @@ mod chord_tests {
#[test]
fn test_all_chords_in_c() {
let chord_tuples = [
(Chord::new(C, Major, Triad), vec![C, E, G]),
(Chord::new(C, Minor, Triad), vec![C, Ds, G]),
(Chord::new(C, Augmented, Triad), vec![C, E, Gs]),
(Chord::new(C, Diminished, Triad), vec![C, Ds, Fs]),
(Chord::new(C, Major, Seventh), vec![C, E, G, B]),
(Chord::new(C, Minor, Seventh), vec![C, Ds, G, As]),
(Chord::new(C, Augmented, Seventh), vec![C, E, Gs, As]),
(Chord::new(C, Augmented, MajorSeventh), vec![C, E, Gs, B]),
(Chord::new(C, Diminished, Seventh), vec![C, Ds, Fs, A]),
(Chord::new(C, HalfDiminished, Seventh), vec![C, Ds, Fs, As]),
(Chord::new(C, Minor, MajorSeventh), vec![C, Ds, G, B]),
(Chord::new(C, Dominant, Seventh), vec![C, E, G, As]),
((C, Major, Triad), vec![C, E, G]),
((C, Minor, Triad), vec![C, Ds, G]),
((C, Augmented, Triad), vec![C, E, Gs]),
((C, Diminished, Triad), vec![C, Ds, Fs]),
((C, Major, Seventh), vec![C, E, G, B]),
((C, Minor, Seventh), vec![C, Ds, G, As]),
((C, Augmented, Seventh), vec![C, E, Gs, As]),
((C, Augmented, MajorSeventh), vec![C, E, Gs, B]),
((C, Diminished, Seventh), vec![C, Ds, Fs, A]),
((C, HalfDiminished, Seventh), vec![C, Ds, Fs, As]),
((C, Minor, MajorSeventh), vec![C, Ds, G, B]),
((C, Dominant, Seventh), vec![C, E, G, As]),
];

for chord_tuple in chord_tuples.iter() {
let (chord, pitches) = chord_tuple;
assert_notes(pitches, chord.notes());
for (chord, pitches) in chord_tuples.iter() {
let classes = &mut pitches.clone();
for inversion in 0..pitches.len() {
assert_notes(
&classes,
Chord::with_inversion(chord.0, chord.1, chord.2, inversion as u8).notes(),
);
classes.rotate_left(1);
}
}
}

#[test]
fn test_inversion_octaves() {
let chord_desc = (G, Major, Ninth);
let octaves = [
[4u8, 4, 5, 5, 5],
[4, 5, 5, 5, 6],
[4, 4, 4, 5, 5],
[4, 4, 5, 5, 6],
[4, 5, 5, 6, 6],
];
for inversion in 0..octaves[0].len() {
let notes =
Chord::with_inversion(chord_desc.0, chord_desc.1, chord_desc.2, inversion as u8)
.notes();
assert_eq!(
notes
.into_iter()
.map(|note| note.octave)
.collect::<Vec<u8>>(),
octaves[inversion]
);
}
}

#[test]
fn test_regex() {
let chord = Chord::from_regex("F major");
assert!(chord.is_ok());
let chord = chord.unwrap();
assert_notes(&vec![F, A, C], chord.notes());
assert_eq!(chord.inversion, 0);
}

#[test]
fn test_inversion_regex() {
let chord = Chord::from_regex("F/C");
let chord_num = Chord::from_regex("F/2");
assert!(chord.is_ok());
assert!(chord_num.is_ok());
let chord = chord.unwrap();
let chord_num = chord_num.unwrap();
assert_notes(&vec![C, F, A], chord.notes());
assert_notes(&vec![C, F, A], chord_num.notes());
assert_eq!(chord.inversion, 2);
assert_eq!(chord_num.inversion, 2);
}
}

0 comments on commit 7cff5d6

Please sign in to comment.