#![allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ChordQuality {
Major,
Minor,
Diminished,
Augmented,
Dominant7,
Major7,
Minor7,
}
impl ChordQuality {
#[must_use]
pub fn interval_pattern(self) -> Vec<u8> {
match self {
Self::Major => vec![0, 4, 7],
Self::Minor => vec![0, 3, 7],
Self::Diminished => vec![0, 3, 6],
Self::Augmented => vec![0, 4, 8],
Self::Dominant7 => vec![0, 4, 7, 10],
Self::Major7 => vec![0, 4, 7, 11],
Self::Minor7 => vec![0, 3, 7, 10],
}
}
#[must_use]
pub fn suffix(self) -> &'static str {
match self {
Self::Major => "",
Self::Minor => "m",
Self::Diminished => "dim",
Self::Augmented => "aug",
Self::Dominant7 => "7",
Self::Major7 => "maj7",
Self::Minor7 => "m7",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Chord {
pub root: u8,
pub quality: ChordQuality,
}
impl Chord {
const PC_NAMES: [&'static str; 12] = [
"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
];
#[must_use]
pub fn new(root: u8, quality: ChordQuality) -> Self {
Self {
root: root % 12,
quality,
}
}
#[must_use]
pub fn display(&self, root_names: &[&str]) -> String {
let root_name = if root_names.len() > self.root as usize {
root_names[self.root as usize]
} else {
Self::PC_NAMES[self.root as usize]
};
format!("{}{}", root_name, self.quality.suffix())
}
#[must_use]
pub fn notes(&self) -> Vec<u8> {
self.quality
.interval_pattern()
.iter()
.map(|&i| (self.root + i) % 12)
.collect()
}
}
const ALL_QUALITIES: &[ChordQuality] = &[
ChordQuality::Major,
ChordQuality::Minor,
ChordQuality::Diminished,
ChordQuality::Augmented,
ChordQuality::Dominant7,
ChordQuality::Major7,
ChordQuality::Minor7,
];
fn chord_template(root: u8, quality: ChordQuality) -> [f32; 12] {
let mut t = [0.0_f32; 12];
for &interval in &quality.interval_pattern() {
t[((root + interval) % 12) as usize] = 1.0;
}
t
}
fn dot12(a: &[f32; 12], b: &[f32; 12]) -> f32 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
#[must_use]
pub fn chroma_to_chord(chroma: &[f32; 12]) -> Chord {
let mut best_chord = Chord::new(0, ChordQuality::Major);
let mut best_score = f32::NEG_INFINITY;
for &quality in ALL_QUALITIES {
for root in 0_u8..12 {
let template = chord_template(root, quality);
let score = dot12(chroma, &template);
if score > best_score {
best_score = score;
best_chord = Chord::new(root, quality);
}
}
}
best_chord
}
#[derive(Debug, Clone, Default)]
pub struct ChordProgression {
pub chords: Vec<Chord>,
pub timestamps_ms: Vec<u64>,
}
impl ChordProgression {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, chord: Chord, ts_ms: u64) {
self.chords.push(chord);
self.timestamps_ms.push(ts_ms);
}
#[must_use]
pub fn at_time_ms(&self, ts: u64) -> Option<&Chord> {
if self.chords.is_empty() {
return None;
}
let mut idx = None;
for (i, &t) in self.timestamps_ms.iter().enumerate() {
if t <= ts {
idx = Some(i);
} else {
break;
}
}
idx.map(|i| &self.chords[i])
}
#[must_use]
pub fn chord_count(&self) -> usize {
self.chords.len()
}
}
pub struct HarmonicAnalyzer;
impl HarmonicAnalyzer {
#[must_use]
pub fn analyze_progression(progression: &ChordProgression) -> Vec<String> {
let chords = &progression.chords;
if chords.len() < 2 {
return Vec::new();
}
let mut results = Vec::with_capacity(chords.len() - 1);
for pair in chords.windows(2) {
let prev = &pair[0];
let curr = &pair[1];
let label = Self::classify_cadence(prev, curr);
results.push(label);
}
results
}
fn classify_cadence(prev: &Chord, curr: &Chord) -> String {
#[allow(clippy::cast_possible_wrap)]
let interval = (curr.root as i8 - prev.root as i8).rem_euclid(12) as u8;
let same_quality = prev.quality == curr.quality;
if interval == 5 && same_quality {
return "perfect cadence".to_string();
}
if interval == 7 && same_quality {
return "plagal cadence".to_string();
}
if interval == 7
&& curr.quality == ChordQuality::Major
&& prev.quality == ChordQuality::Major
{
return "half cadence".to_string();
}
if interval == 2 {
return "deceptive cadence".to_string();
}
String::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_major_interval_pattern() {
assert_eq!(ChordQuality::Major.interval_pattern(), vec![0, 4, 7]);
}
#[test]
fn test_minor_interval_pattern() {
assert_eq!(ChordQuality::Minor.interval_pattern(), vec![0, 3, 7]);
}
#[test]
fn test_diminished_interval_pattern() {
assert_eq!(ChordQuality::Diminished.interval_pattern(), vec![0, 3, 6]);
}
#[test]
fn test_augmented_interval_pattern() {
assert_eq!(ChordQuality::Augmented.interval_pattern(), vec![0, 4, 8]);
}
#[test]
fn test_dominant7_interval_pattern() {
assert_eq!(
ChordQuality::Dominant7.interval_pattern(),
vec![0, 4, 7, 10]
);
}
#[test]
fn test_major7_interval_pattern() {
assert_eq!(ChordQuality::Major7.interval_pattern(), vec![0, 4, 7, 11]);
}
#[test]
fn test_minor7_interval_pattern() {
assert_eq!(ChordQuality::Minor7.interval_pattern(), vec![0, 3, 7, 10]);
}
#[test]
fn test_chord_display_c_major() {
let c = Chord::new(0, ChordQuality::Major);
assert_eq!(c.display(&[]), "C");
}
#[test]
fn test_chord_display_a_minor() {
let c = Chord::new(9, ChordQuality::Minor);
assert_eq!(c.display(&[]), "Am");
}
#[test]
fn test_chord_display_custom_names() {
let c = Chord::new(0, ChordQuality::Major7);
let names = ["Do", "Re", "Mi"];
assert_eq!(c.display(&names), "Domaj7");
}
#[test]
fn test_chord_notes_c_major() {
let c = Chord::new(0, ChordQuality::Major);
let notes = c.notes();
assert_eq!(notes, vec![0, 4, 7]); }
#[test]
fn test_chord_notes_wraps_mod12() {
let c = Chord::new(11, ChordQuality::Major);
let notes = c.notes();
assert!(notes.contains(&11)); assert!(notes.contains(&3)); assert!(notes.contains(&6)); }
#[test]
fn test_chroma_to_chord_c_major() {
let mut chroma = [0.0_f32; 12];
chroma[0] = 1.0; chroma[4] = 1.0; chroma[7] = 1.0; let chord = chroma_to_chord(&chroma);
assert_eq!(chord.root, 0);
assert_eq!(chord.quality, ChordQuality::Major);
}
#[test]
fn test_chroma_to_chord_a_minor() {
let mut chroma = [0.0_f32; 12];
chroma[9] = 1.0; chroma[0] = 1.0; chroma[4] = 1.0; let chord = chroma_to_chord(&chroma);
assert_eq!(chord.root, 9);
assert_eq!(chord.quality, ChordQuality::Minor);
}
#[test]
fn test_chord_progression_empty() {
let prog = ChordProgression::new();
assert_eq!(prog.chord_count(), 0);
assert!(prog.at_time_ms(0).is_none());
}
#[test]
fn test_chord_progression_add_and_count() {
let mut prog = ChordProgression::new();
prog.add(Chord::new(0, ChordQuality::Major), 0);
prog.add(Chord::new(7, ChordQuality::Major), 1000);
assert_eq!(prog.chord_count(), 2);
}
#[test]
fn test_chord_progression_at_time_ms_first_chord() {
let mut prog = ChordProgression::new();
let c = Chord::new(0, ChordQuality::Major);
prog.add(c, 0);
assert_eq!(prog.at_time_ms(0), Some(&c));
}
#[test]
fn test_chord_progression_at_time_ms_before_start_returns_none() {
let mut prog = ChordProgression::new();
prog.add(Chord::new(0, ChordQuality::Major), 500);
assert!(prog.at_time_ms(100).is_none());
}
#[test]
fn test_chord_progression_at_time_ms_selects_correct() {
let mut prog = ChordProgression::new();
let c = Chord::new(0, ChordQuality::Major);
let g = Chord::new(7, ChordQuality::Major);
prog.add(c, 0);
prog.add(g, 2000);
assert_eq!(prog.at_time_ms(1500), Some(&c));
assert_eq!(prog.at_time_ms(2000), Some(&g));
assert_eq!(prog.at_time_ms(3000), Some(&g));
}
#[test]
fn test_harmonic_analyzer_empty_progression() {
let prog = ChordProgression::new();
let labels = HarmonicAnalyzer::analyze_progression(&prog);
assert!(labels.is_empty());
}
#[test]
fn test_harmonic_analyzer_single_chord_no_pairs() {
let mut prog = ChordProgression::new();
prog.add(Chord::new(0, ChordQuality::Major), 0);
let labels = HarmonicAnalyzer::analyze_progression(&prog);
assert!(labels.is_empty());
}
#[test]
fn test_harmonic_analyzer_pair_count() {
let mut prog = ChordProgression::new();
for i in 0_u8..4 {
prog.add(Chord::new(i * 2, ChordQuality::Major), u64::from(i) * 1000);
}
let labels = HarmonicAnalyzer::analyze_progression(&prog);
assert_eq!(labels.len(), 3);
}
#[test]
fn test_harmonic_analyzer_deceptive_cadence() {
let mut prog = ChordProgression::new();
prog.add(Chord::new(7, ChordQuality::Major), 0);
prog.add(Chord::new(9, ChordQuality::Minor), 1000);
let labels = HarmonicAnalyzer::analyze_progression(&prog);
assert_eq!(labels[0], "deceptive cadence");
}
}