Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support CLI notes -> chord, support some inversions in Chord::from_string #36

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions src/bin/rustmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,23 @@ fn chord_command(chord_matches: &ArgMatches) {
}
}
_ => {
let chord_args = chord_matches
.values_of("args")
.unwrap()
.collect::<Vec<_>>()
.join(" ");
let chord_args_vec = chord_matches.values_of("args").unwrap().collect::<Vec<_>>();
for &arg in &chord_args_vec {
// User entered chord name, is asking for notes
if arg.len() > 2 {
let chord_args = chord_args_vec.join(" ");

let chord = Chord::from_regex(&chord_args).unwrap();
chord.print_notes();
let chord = Chord::from_regex(&chord_args).unwrap();
return chord.print_notes();
}
}

// User entered notes, is asking for chord name
let chord_args = chord_args_vec.join(" ");
match Chord::from_string(&chord_args) {
Ok(chord) => println!("{}", chord.to_string()),
Err(e) => println!("{}", e.to_string()),
}
}
}
}
Expand Down
192 changes: 136 additions & 56 deletions src/chord/chord.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use std::fmt;

use crate::chord::errors::ChordError;
use crate::chord::number::Number::Triad;
use crate::chord::{Number, Quality};
use crate::interval::Interval;
use crate::note::{Note, NoteError, Notes, Pitch, NoteLetter};
use crate::note::{Note, NoteError, NoteLetter, Notes, Pitch};

/// A chord.
#[derive(Debug, Clone)]
Expand All @@ -28,12 +30,7 @@ impl Chord {
}

/// Create a new chord with a given inversion.
pub fn with_inversion(
root: Pitch,
quality: Quality,
number: Number,
inversion: u8,
) -> Self {
pub fn with_inversion(root: Pitch, quality: Quality, number: Number, inversion: u8) -> Self {
let intervals = Self::chord_intervals(quality, number);
let inversion = inversion % (intervals.len() + 1) as u8;
Chord {
Expand All @@ -46,51 +43,37 @@ impl Chord {
}
}

pub fn from_string(string: &str) -> Self {
let notes: Vec<Pitch> = string.to_string()
.replace(",", "")
.split_whitespace()
.into_iter()
.map(|x| Pitch::from_str(x).expect(&format!("Invalid note {:?}.", x)))
.collect();

let intervals: Vec<u8> = notes.iter()
.map(|&x| Pitch::into_u8(x) % 12)
.zip(notes[1..].iter().map(|&x| Pitch::into_u8(x)))
.map(|(x, y)| if x < y {y - x} else {y + 12 - x})
.collect();

Chord::from_interval(notes[0], &intervals)
pub fn from_string(string: &str) -> Result<Self, ChordError> {
let notes: Vec<Pitch> = string
.to_string()
.replace(",", "")
.split_whitespace()
.into_iter()
.map(|x| Pitch::from_str(x).expect(&format!("Invalid note {:?}.", x)))
.collect();

let intervals: Vec<u8> = notes
.iter()
.map(|&x| Pitch::into_u8(x) % 12)
.zip(notes[1..].iter().map(|&x| Pitch::into_u8(x)))
.map(|(x, y)| if x < y { y - x } else { y + 12 - x })
.collect();

match unknown_position_interval(&intervals) {
Some(info) => Ok(Self::with_inversion(
notes[info.root_note_index],
info.quality,
info.number,
info.inversion,
)),
None => Err(ChordError::InvalidUnknownChord),
}
}

pub fn from_interval(root: Pitch, interval: &[u8]) -> Self {
use Number::*;
use Quality::*;
let (quality, number) = match interval {
&[4, 3] => (Major, Triad),
&[3, 4] => (Minor, Triad),
&[2, 5] => (Suspended2, Triad),
&[5, 2] => (Suspended4, Triad),
&[4, 4] => (Augmented, Triad),
&[3, 3] => (Diminished, Triad),
&[4, 3, 4] => (Major, Seventh),
&[3, 4, 3] => (Minor, Seventh),
&[4, 4, 2] => (Augmented, Seventh),
&[4, 4, 3] => (Augmented, MajorSeventh),
&[3, 3, 3] => (Diminished, Seventh),
&[3, 3, 4] => (HalfDiminished, Seventh),
&[3, 4, 4] => (Minor, MajorSeventh),
&[4, 3, 3] => (Dominant, Seventh),
&[4, 3, 3, 4] => (Dominant, Ninth),
&[4, 3, 4, 3] => (Major, Ninth),
&[4, 3, 3, 4, 4] => (Dominant, Eleventh),
&[4, 3, 4, 3, 3] => (Major, Eleventh),
&[3, 4, 3, 4, 3] => (Minor, Eleventh),
&[4, 3, 3, 4, 3, 4] => (Dominant, Thirteenth),
&[4, 3, 4, 3, 3, 4] => (Major, Thirteenth),
&[3, 4, 3, 4, 3, 4] => (Minor, Thirteenth),
_ => panic!(format!("Couldn't create chord! {:?}", interval))
};
let (quality, number) = assume_root_position_interval(interval)
.expect(&format!("Couldn't create chord! {:?}", interval));

Self::new(root, quality, number)
}

Expand Down Expand Up @@ -153,12 +136,8 @@ impl Chord {
Triad
};

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

if let Ok((bass_note, _)) = bass_note_result {
let inversion = chord
Expand Down Expand Up @@ -211,7 +190,10 @@ impl Notes for Chord {
impl Default for Chord {
fn default() -> Self {
Chord {
root: Pitch { letter: NoteLetter::C, accidental: 0 },
root: Pitch {
letter: NoteLetter::C,
accidental: 0,
},
octave: 4,
intervals: vec![],
quality: Quality::Major,
Expand All @@ -220,3 +202,101 @@ impl Default for Chord {
}
}
}

impl fmt::Display for Chord {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let summary = format!("{} {} {}", self.root, self.quality, self.number);

match self.inversion {
0 => write!(f, "{}", summary),
1 => write!(f, "{}, 1st Inversion", summary),
2 => write!(f, "{}, 2nd Inversion", summary),
3 => write!(f, "{}, 3rd Inversion", summary),
n => write!(f, "{}, {}th Inversion", summary, n),
}
}
}

struct UnknownPositionInterval {
quality: Quality,
number: Number,
root_note_index: usize,
inversion: u8,
}

impl UnknownPositionInterval {
fn new(quality: Quality, number: Number, root_note_index: usize, inversion: u8) -> Self {
Self {
quality,
number,
root_note_index,
inversion,
}
}
}

/// # Purpose
/// Given a slice of intervals, return the:
/// - Chord Quality
/// - Chord Number
/// - Root Note Index (for the array of notes used to find the input intervals)
/// - Inversion Position
///
/// Returns `None` if the intervals are not a chord
///
/// # Implementation
/// In order, match intervals against known patterns for:
/// - Root position chords
/// - Inverted chords
fn unknown_position_interval(interval: &[u8]) -> Option<UnknownPositionInterval> {
use Number::*;
use Quality::*;

match assume_root_position_interval(interval) {
Some((quality, number)) => Some(UnknownPositionInterval::new(quality, number, 0, 0)),
None => match interval {
&[3, 5] => Some(UnknownPositionInterval::new(Major, Triad, 2, 1)),
&[5, 4] => Some(UnknownPositionInterval::new(Major, Triad, 1, 2)),
&[4, 5] => Some(UnknownPositionInterval::new(Minor, Triad, 2, 1)),
&[5, 3] => Some(UnknownPositionInterval::new(Minor, Triad, 1, 2)),
&[5, 5] => Some(UnknownPositionInterval::new(Suspended2, Triad, 2, 1)),
_ => None,
// Conflicts:
// &[5, 2] => Sus2 Triad 2nd inversion, Sus4 Triad root position
// &[4, 4] => Augmented Triad root position + 1st inversion + 2nd inversion
// &[3, 3] => Diminished Triad root position + 1st inversion + 2nd inversion
},
}
}

/// Determine the chord quality and number assuming that the chord is in root position (tonic is bottom note)
fn assume_root_position_interval(interval: &[u8]) -> Option<(Quality, Number)> {
use Number::*;
use Quality::*;

match interval {
&[4, 3] => Some((Major, Triad)),
&[3, 4] => Some((Minor, Triad)),
&[2, 5] => Some((Suspended2, Triad)),
&[5, 2] => Some((Suspended4, Triad)),
&[4, 4] => Some((Augmented, Triad)),
&[3, 3] => Some((Diminished, Triad)),
&[4, 3, 4] => Some((Major, Seventh)),
&[3, 4, 3] => Some((Minor, Seventh)),
&[4, 4, 2] => Some((Augmented, Seventh)),
&[4, 4, 3] => Some((Augmented, MajorSeventh)),
&[3, 3, 3] => Some((Diminished, Seventh)),
&[3, 3, 4] => Some((HalfDiminished, Seventh)),
&[3, 4, 4] => Some((Minor, MajorSeventh)),
&[4, 3, 3] => Some((Dominant, Seventh)),
&[4, 3, 3, 4] => Some((Dominant, Ninth)),
&[4, 3, 4, 3] => Some((Major, Ninth)),
&[4, 3, 3, 4, 4] => Some((Dominant, Eleventh)),
&[4, 3, 4, 3, 3] => Some((Major, Eleventh)),
&[3, 4, 3, 4, 3] => Some((Minor, Eleventh)),
&[4, 3, 3, 4, 3, 4] => Some((Dominant, Thirteenth)),
&[4, 3, 4, 3, 3, 4] => Some((Major, Thirteenth)),
&[3, 4, 3, 4, 3, 4] => Some((Minor, Thirteenth)),
_ => None,
}
}
6 changes: 5 additions & 1 deletion src/chord/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ use std::fmt;
#[derive(Debug, Clone)]
pub enum ChordError {
InvalidRegex,
InvalidUnknownChord,
}

impl fmt::Display for ChordError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Invalid Regex!")
match self {
ChordError::InvalidRegex => write!(f, "Invalid Regex!"),
ChordError::InvalidUnknownChord => write!(f, "Invalid/Unknown Chord!"),
}
}
}

Expand Down
36 changes: 30 additions & 6 deletions tests/chord/test_chord.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ mod chord_tests {
for inversion in 0..pitches.len() {
assert_notes(
&symbols,
Chord::with_inversion(Pitch::from(chord.0), chord.1, chord.2, inversion as u8).notes(),
Chord::with_inversion(Pitch::from(chord.0), chord.1, chord.2, inversion as u8)
.notes(),
);
symbols.rotate_left(1);
}
Expand All @@ -54,9 +55,13 @@ mod chord_tests {
[4, 5, 5, 6, 6],
];
for inversion in 0..octaves[0].len() {
let notes =
Chord::with_inversion(Pitch::from(chord_desc.0), chord_desc.1, chord_desc.2, inversion as u8)
.notes();
let notes = Chord::with_inversion(
Pitch::from(chord_desc.0),
chord_desc.1,
chord_desc.2,
inversion as u8,
)
.notes();
assert_eq!(
notes
.into_iter()
Expand Down Expand Up @@ -91,7 +96,7 @@ mod chord_tests {
}

#[test]
fn test_chord_from_string() {
fn test_chord_from_string_root_position() {
let c = Pitch::from_str("C").unwrap();
let chord_tuples = [
((c, Major, Triad), "C E G"),
Expand All @@ -111,9 +116,28 @@ mod chord_tests {
];

for chord_pair in chord_tuples.iter() {
let chord = Chord::from_string(chord_pair.1);
let chord = Chord::from_string(chord_pair.1).unwrap();
let (root, quality, number) = (chord.root, chord.quality, chord.number);
assert_eq!((root, quality, number), (chord_pair.0));
}
}

#[test]
fn test_chord_from_string_triad_inversions() {
let c = Pitch::from_str("C").unwrap();
let chord_tuples = [
((c, Major, Triad, 1), "E G C"),
((c, Major, Triad, 2), "G C E"),
((c, Minor, Triad, 1), "Eb G C"),
((c, Minor, Triad, 2), "G C Eb"),
((c, Suspended2, Triad, 1), "D G C"),
];

for chord_pair in chord_tuples.iter() {
let chord = Chord::from_string(chord_pair.1).unwrap();
let (root, quality, number, inversion) =
(chord.root, chord.quality, chord.number, chord.inversion);
assert_eq!((root, quality, number, inversion), (chord_pair.0));
}
}
}