use super::notation::{ChordSymbol, KeySignature};
use super::pitch::Pitch;
const TEMPLATES: &[(&[u8], &str)] = &[
(&[0, 4, 7], "major"),
(&[0, 3, 7], "minor"),
(&[0, 4, 7, 10], "dominant"),
(&[0, 4, 7, 11], "major-seventh"),
(&[0, 3, 7, 10], "minor-seventh"),
(&[0, 3, 6], "diminished"),
(&[0, 3, 6, 9], "diminished-seventh"),
(&[0, 3, 6, 10], "half-diminished"),
(&[0, 4, 8], "augmented"),
(&[0, 2, 7], "suspended-second"),
(&[0, 5, 7], "suspended-fourth"),
(&[0, 4, 7, 9], "major-sixth"),
(&[0, 3, 7, 9], "minor-sixth"),
];
pub fn detect_chord(pitches: &[Pitch]) -> Option<ChordSymbol> {
if pitches.len() < 2 {
return None;
}
let mut pcs: Vec<(u8, &Pitch)> = Vec::new();
for p in pitches {
let pc = (p.to_midi().rem_euclid(12)) as u8;
if !pcs.iter().any(|(c, _)| *c == pc) {
pcs.push((pc, p));
}
}
let bass_pitch = pitches.iter().min_by_key(|p| p.to_midi())?;
let bass_pc = (bass_pitch.to_midi().rem_euclid(12)) as u8;
for &(root_pc, root_pitch) in &pcs {
let mut intervals: Vec<u8> = pcs.iter().map(|(pc, _)| (pc + 12 - root_pc) % 12).collect();
intervals.sort_unstable();
for &(template, kind) in TEMPLATES {
if intervals.as_slice() == template {
let acc = match root_pitch.alter {
1 => "#",
-1 => "b",
_ => "",
};
let root = format!("{}{}", root_pitch.step.to_char(), acc);
let bass = if bass_pc != root_pc {
let b_acc = match bass_pitch.alter {
1 => "#",
-1 => "b",
_ => "",
};
Some(format!("{}{}", bass_pitch.step.to_char(), b_acc))
} else {
None
};
return Some(ChordSymbol {
root,
kind: kind.to_string(),
bass,
});
}
}
}
None
}
fn root_to_pc(root: &str) -> Option<u8> {
let mut chars = root.chars();
let base = match chars.next()? {
'C' => 0u8,
'D' => 2,
'E' => 4,
'F' => 5,
'G' => 7,
'A' => 9,
'B' => 11,
_ => return None,
};
let pc = match chars.next() {
Some('#') => base + 1,
Some('b') => base.wrapping_sub(1),
None => base,
_ => return None,
};
Some(pc % 12)
}
pub fn roman_numeral(chord: &ChordSymbol, key: &KeySignature) -> Option<String> {
const NUMERALS: &[&str] = &["I", "II", "III", "IV", "V", "VI", "VII"];
let (key_step, key_alter) = key.tonic();
let key_root_pc = {
let base: u8 = match key_step {
crate::Step::C => 0,
crate::Step::D => 2,
crate::Step::E => 4,
crate::Step::F => 5,
crate::Step::G => 7,
crate::Step::A => 9,
crate::Step::B => 11,
};
((base as i8 + key_alter).rem_euclid(12)) as u8
};
let scale_intervals: &[u8] = if key.mode == "minor" {
&[0, 2, 3, 5, 7, 8, 10] } else {
&[0, 2, 4, 5, 7, 9, 11] };
let pc_to_degree = |pc: u8| -> Option<usize> {
let interval = ((pc as i16 - key_root_pc as i16).rem_euclid(12)) as u8;
scale_intervals.iter().position(|&i| i == interval)
};
let chord_pc = root_to_pc(&chord.root)?;
let degree = pc_to_degree(chord_pc)?;
let numeral = NUMERALS[degree];
let (upper, suffix) = match chord.kind.as_str() {
"major" => (true, ""),
"dominant" => (true, "7"),
"major-seventh" => (true, "maj7"),
"major-sixth" => (true, "6"),
"augmented" => (true, "+"),
k if k.starts_with("suspended") => (true, ""),
"minor" => (false, ""),
"minor-seventh" => (false, "7"),
"minor-sixth" => (false, "6"),
"diminished" => (false, "o"),
"diminished-seventh" => (false, "o7"),
"half-diminished" => (false, "\u{00f8}7"),
_ => (true, ""),
};
let rn = if upper {
format!("{}{}", numeral, suffix)
} else {
format!("{}{}", numeral.to_lowercase(), suffix)
};
let slash = if let Some(bass) = &chord.bass {
let bass_pc = root_to_pc(bass)?;
let bass_degree = pc_to_degree(bass_pc)?;
let bass_numeral = NUMERALS[bass_degree];
format!("/{}", bass_numeral)
} else {
String::new()
};
Some(format!("{}{}", rn, slash))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Pitch, Step};
fn p(step: Step, octave: i8) -> Pitch {
Pitch::new(step, octave)
}
fn pa(step: Step, octave: i8, alter: i8) -> Pitch {
Pitch::with_alter(step, octave, alter)
}
#[test]
fn detect_chord_c_major_root_pos() {
let pitches = [p(Step::C, 4), p(Step::E, 4), p(Step::G, 4)];
let cs = detect_chord(&pitches).unwrap();
assert_eq!(cs.root, "C");
assert_eq!(cs.kind, "major");
assert_eq!(cs.bass, None);
}
#[test]
fn detect_chord_g_dominant_seventh() {
let pitches = [p(Step::G, 4), p(Step::B, 4), p(Step::D, 4), p(Step::F, 4)];
let cs = detect_chord(&pitches).unwrap();
assert_eq!(cs.root, "G");
assert_eq!(cs.kind, "dominant");
}
#[test]
fn detect_chord_d_minor() {
let pitches = [p(Step::D, 4), p(Step::F, 4), p(Step::A, 4)];
let cs = detect_chord(&pitches).unwrap();
assert_eq!(cs.root, "D");
assert_eq!(cs.kind, "minor");
}
#[test]
fn detect_chord_first_inversion() {
let pitches = [p(Step::E, 4), p(Step::G, 4), p(Step::C, 5)];
let cs = detect_chord(&pitches).unwrap();
assert_eq!(cs.root, "C");
assert_eq!(cs.kind, "major");
assert_eq!(cs.bass, Some("E".to_string()));
}
#[test]
fn detect_chord_slash_chord() {
let pitches = [p(Step::G, 3), p(Step::C, 4), p(Step::E, 4), p(Step::G, 4)];
let cs = detect_chord(&pitches).unwrap();
assert_eq!(cs.root, "C");
assert_eq!(cs.kind, "major");
assert_eq!(cs.bass, Some("G".to_string()));
}
#[test]
fn detect_chord_too_few_notes() {
let pitches = [p(Step::C, 4)];
assert!(detect_chord(&pitches).is_none());
}
#[test]
fn detect_chord_no_template_match() {
let pitches = [p(Step::C, 4), pa(Step::C, 4, 1)];
assert!(detect_chord(&pitches).is_none());
}
#[test]
fn detect_chord_diminished() {
let pitches = [p(Step::B, 3), p(Step::D, 4), p(Step::F, 4)];
let cs = detect_chord(&pitches).unwrap();
assert_eq!(cs.root, "B");
assert_eq!(cs.kind, "diminished");
}
#[test]
fn detect_chord_flat_root() {
let pitches = [pa(Step::B, 3, -1), p(Step::D, 4), p(Step::F, 4)];
let cs = detect_chord(&pitches).unwrap();
assert_eq!(cs.root, "Bb");
assert_eq!(cs.kind, "major");
}
fn c_major_key() -> KeySignature {
KeySignature {
fifths: 0,
mode: "major".to_string(),
}
}
#[test]
fn roman_numeral_i_major() {
let chord = ChordSymbol {
root: "C".to_string(),
kind: "major".to_string(),
bass: None,
};
assert_eq!(roman_numeral(&chord, &c_major_key()), Some("I".to_string()));
}
#[test]
fn roman_numeral_v7() {
let chord = ChordSymbol {
root: "G".to_string(),
kind: "dominant".to_string(),
bass: None,
};
assert_eq!(
roman_numeral(&chord, &c_major_key()),
Some("V7".to_string())
);
}
#[test]
fn roman_numeral_ii_minor() {
let chord = ChordSymbol {
root: "D".to_string(),
kind: "minor".to_string(),
bass: None,
};
assert_eq!(
roman_numeral(&chord, &c_major_key()),
Some("ii".to_string())
);
}
#[test]
fn roman_numeral_vii_diminished() {
let chord = ChordSymbol {
root: "B".to_string(),
kind: "diminished".to_string(),
bass: None,
};
assert_eq!(
roman_numeral(&chord, &c_major_key()),
Some("viio".to_string())
);
}
#[test]
fn roman_numeral_out_of_key_returns_none() {
let chord = ChordSymbol {
root: "F#".to_string(),
kind: "major".to_string(),
bass: None,
};
assert!(roman_numeral(&chord, &c_major_key()).is_none());
}
#[test]
fn roman_numeral_slash_chord() {
let chord = ChordSymbol {
root: "C".to_string(),
kind: "major".to_string(),
bass: Some("G".to_string()),
};
assert_eq!(
roman_numeral(&chord, &c_major_key()),
Some("I/V".to_string())
);
}
}