Skip to content

Commit 7cff5d6

Browse files
authored
Merge pull request #17 from henryksloan/chord-inversion
Chord inversions
2 parents 6d972be + 252dfe8 commit 7cff5d6

File tree

4 files changed

+164
-42
lines changed

4 files changed

+164
-42
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,6 @@ Notes:
157157
- [ ] Add support for arbitrary accidentals
158158
- [ ] Add support for the alternative names of the modes to regex parser
159159
- [ ] Properly display enharmonic spelling
160-
- [ ] Add inversion support for chords
160+
- [x] Add inversion support for chords
161161
- [ ] Add support for [cadence][1]s
162162
- [ ] Add a mechanism to find the chord from the given notes

src/bin/rustmt.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ fn main() {
113113
.subcommand(App::new("list").about("Prints out the available chords"))
114114
.arg(
115115
Arg::with_name("args")
116-
.help("chord args, examples:\nC minor\nAb augmented major seventh")
116+
.help("chord args, examples:\nC minor\nAb augmented major seventh\nF# dominant seventh / C#\nC/1")
117117
.multiple(true),
118118
),
119119
)

src/chord/chord.rs

Lines changed: 94 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::chord::errors::ChordError;
22
use crate::chord::number::Number::Triad;
33
use crate::chord::{Number, Quality};
44
use crate::interval::Interval;
5-
use crate::note::{Note, Notes, PitchClass};
5+
use crate::note::{Note, NoteError, Notes, PitchClass};
66

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

2224
impl Chord {
2325
/// Create a new chord.
2426
pub fn new(root: PitchClass, quality: Quality, number: Number) -> Self {
27+
Self::with_inversion(root, quality, number, 0)
28+
}
29+
30+
/// Create a new chord with a given inversion.
31+
pub fn with_inversion(
32+
root: PitchClass,
33+
quality: Quality,
34+
number: Number,
35+
inversion: u8,
36+
) -> Self {
37+
let intervals = Self::chord_intervals(quality, number);
38+
let inversion = inversion % (intervals.len() + 1) as u8;
39+
Chord {
40+
root,
41+
octave: 4,
42+
intervals,
43+
quality,
44+
number,
45+
inversion,
46+
}
47+
}
48+
49+
pub fn chord_intervals(quality: Quality, number: Number) -> Vec<Interval> {
2550
use Number::*;
2651
use Quality::*;
27-
let intervals = match (&quality, &number) {
52+
match (&quality, &number) {
2853
(Major, Triad) => Interval::from_semitones(&[4, 3]),
2954
(Minor, Triad) => Interval::from_semitones(&[3, 4]),
3055
(Suspended2, Triad) => Interval::from_semitones(&[2, 5]),
@@ -49,36 +74,62 @@ impl Chord {
4974
(Minor, Thirteenth) => Interval::from_semitones(&[3, 4, 3, 4, 3, 4]),
5075
_ => Interval::from_semitones(&[4, 3]),
5176
}
52-
.unwrap();
53-
54-
Chord {
55-
root,
56-
octave: 4,
57-
intervals,
58-
quality,
59-
number,
60-
}
77+
.unwrap()
6178
}
6279

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

67-
let (quality, quality_match_option) =
68-
Quality::from_regex(&string[pitch_match.end()..].trim())?;
84+
let slash_option = string.find('/');
85+
let bass_note_result = if let Some(slash) = slash_option {
86+
PitchClass::from_regex(&string[slash + 1..].trim())
87+
} else {
88+
Err(NoteError::InvalidPitch)
89+
};
90+
let inversion_num_option = if let Some(slash) = slash_option {
91+
string[slash + 1..].trim().parse::<u8>().ok()
92+
} else {
93+
None
94+
};
95+
96+
let (quality, quality_match_option) = Quality::from_regex(
97+
&string[pitch_match.end()..slash_option.unwrap_or_else(|| string.len())].trim(),
98+
)?;
99+
100+
let number = if let Some(quality_match) = quality_match_option {
101+
Number::from_regex(&string[quality_match.end()..])
102+
.unwrap_or((Triad, None))
103+
.0
104+
} else {
105+
Triad
106+
};
107+
108+
let chord = Chord::with_inversion(
109+
pitch_class,
110+
quality,
111+
number,
112+
inversion_num_option.unwrap_or(0),
113+
);
69114

70-
Ok(match quality_match_option {
71-
// there is
72-
Some(quality_match) => {
73-
let (number, _) =
74-
Number::from_regex(&string[quality_match.end()..]).unwrap_or((Triad, None));
115+
if let Ok((bass_note, _)) = bass_note_result {
116+
let inversion = chord
117+
.notes()
118+
.iter()
119+
.position(|note| note.pitch_class == bass_note)
120+
.unwrap_or(0);
75121

76-
Chord::new(pitch_class, quality, number)
122+
if inversion != 0 {
123+
return Ok(Chord::with_inversion(
124+
pitch_class,
125+
quality,
126+
number,
127+
inversion as u8,
128+
));
77129
}
130+
}
78131

79-
// return a Triad by default
80-
None => Chord::new(pitch_class, quality, Triad),
81-
})
132+
Ok(chord)
82133
}
83134
}
84135

@@ -88,7 +139,24 @@ impl Notes for Chord {
88139
octave: self.octave,
89140
pitch_class: self.root,
90141
};
91-
Interval::to_notes(root_note, self.intervals.clone())
142+
let mut notes = Interval::to_notes(root_note, self.intervals.clone());
143+
notes.rotate_left(self.inversion as usize);
144+
145+
// Normalize to the correct octave
146+
if notes[0].octave > self.octave {
147+
let diff = notes[0].octave - self.octave;
148+
notes.iter_mut().for_each(|note| note.octave -= diff);
149+
}
150+
151+
// Ensure that octave increments at the right notes
152+
for i in 1..notes.len() {
153+
if notes[i].pitch_class as u8 <= notes[i - 1].pitch_class as u8 {
154+
notes[i].octave = notes[i - 1].octave + 1;
155+
} else if notes[i].octave < notes[i - 1].octave {
156+
notes[i].octave = notes[i - 1].octave;
157+
}
158+
}
159+
notes
92160
}
93161
}
94162

@@ -100,6 +168,7 @@ impl Default for Chord {
100168
intervals: vec![],
101169
quality: Quality::Major,
102170
number: Number::Triad,
171+
inversion: 0,
103172
}
104173
}
105174
}

tests/chord/test_chord.rs

Lines changed: 68 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,76 @@ mod chord_tests {
1515
#[test]
1616
fn test_all_chords_in_c() {
1717
let chord_tuples = [
18-
(Chord::new(C, Major, Triad), vec![C, E, G]),
19-
(Chord::new(C, Minor, Triad), vec![C, Ds, G]),
20-
(Chord::new(C, Augmented, Triad), vec![C, E, Gs]),
21-
(Chord::new(C, Diminished, Triad), vec![C, Ds, Fs]),
22-
(Chord::new(C, Major, Seventh), vec![C, E, G, B]),
23-
(Chord::new(C, Minor, Seventh), vec![C, Ds, G, As]),
24-
(Chord::new(C, Augmented, Seventh), vec![C, E, Gs, As]),
25-
(Chord::new(C, Augmented, MajorSeventh), vec![C, E, Gs, B]),
26-
(Chord::new(C, Diminished, Seventh), vec![C, Ds, Fs, A]),
27-
(Chord::new(C, HalfDiminished, Seventh), vec![C, Ds, Fs, As]),
28-
(Chord::new(C, Minor, MajorSeventh), vec![C, Ds, G, B]),
29-
(Chord::new(C, Dominant, Seventh), vec![C, E, G, As]),
18+
((C, Major, Triad), vec![C, E, G]),
19+
((C, Minor, Triad), vec![C, Ds, G]),
20+
((C, Augmented, Triad), vec![C, E, Gs]),
21+
((C, Diminished, Triad), vec![C, Ds, Fs]),
22+
((C, Major, Seventh), vec![C, E, G, B]),
23+
((C, Minor, Seventh), vec![C, Ds, G, As]),
24+
((C, Augmented, Seventh), vec![C, E, Gs, As]),
25+
((C, Augmented, MajorSeventh), vec![C, E, Gs, B]),
26+
((C, Diminished, Seventh), vec![C, Ds, Fs, A]),
27+
((C, HalfDiminished, Seventh), vec![C, Ds, Fs, As]),
28+
((C, Minor, MajorSeventh), vec![C, Ds, G, B]),
29+
((C, Dominant, Seventh), vec![C, E, G, As]),
3030
];
3131

32-
for chord_tuple in chord_tuples.iter() {
33-
let (chord, pitches) = chord_tuple;
34-
assert_notes(pitches, chord.notes());
32+
for (chord, pitches) in chord_tuples.iter() {
33+
let classes = &mut pitches.clone();
34+
for inversion in 0..pitches.len() {
35+
assert_notes(
36+
&classes,
37+
Chord::with_inversion(chord.0, chord.1, chord.2, inversion as u8).notes(),
38+
);
39+
classes.rotate_left(1);
40+
}
3541
}
3642
}
43+
44+
#[test]
45+
fn test_inversion_octaves() {
46+
let chord_desc = (G, Major, Ninth);
47+
let octaves = [
48+
[4u8, 4, 5, 5, 5],
49+
[4, 5, 5, 5, 6],
50+
[4, 4, 4, 5, 5],
51+
[4, 4, 5, 5, 6],
52+
[4, 5, 5, 6, 6],
53+
];
54+
for inversion in 0..octaves[0].len() {
55+
let notes =
56+
Chord::with_inversion(chord_desc.0, chord_desc.1, chord_desc.2, inversion as u8)
57+
.notes();
58+
assert_eq!(
59+
notes
60+
.into_iter()
61+
.map(|note| note.octave)
62+
.collect::<Vec<u8>>(),
63+
octaves[inversion]
64+
);
65+
}
66+
}
67+
68+
#[test]
69+
fn test_regex() {
70+
let chord = Chord::from_regex("F major");
71+
assert!(chord.is_ok());
72+
let chord = chord.unwrap();
73+
assert_notes(&vec![F, A, C], chord.notes());
74+
assert_eq!(chord.inversion, 0);
75+
}
76+
77+
#[test]
78+
fn test_inversion_regex() {
79+
let chord = Chord::from_regex("F/C");
80+
let chord_num = Chord::from_regex("F/2");
81+
assert!(chord.is_ok());
82+
assert!(chord_num.is_ok());
83+
let chord = chord.unwrap();
84+
let chord_num = chord_num.unwrap();
85+
assert_notes(&vec![C, F, A], chord.notes());
86+
assert_notes(&vec![C, F, A], chord_num.notes());
87+
assert_eq!(chord.inversion, 2);
88+
assert_eq!(chord_num.inversion, 2);
89+
}
3790
}

0 commit comments

Comments
 (0)