#![allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PitchClass {
C,
Cs,
D,
Ds,
E,
F,
Fs,
G,
Gs,
A,
As,
B,
}
impl PitchClass {
#[must_use]
pub fn semitone(self) -> u8 {
match self {
Self::C => 0,
Self::Cs => 1,
Self::D => 2,
Self::Ds => 3,
Self::E => 4,
Self::F => 5,
Self::Fs => 6,
Self::G => 7,
Self::Gs => 8,
Self::A => 9,
Self::As => 10,
Self::B => 11,
}
}
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::C => "C",
Self::Cs => "C#",
Self::D => "D",
Self::Ds => "D#",
Self::E => "E",
Self::F => "F",
Self::Fs => "F#",
Self::G => "G",
Self::Gs => "G#",
Self::A => "A",
Self::As => "A#",
Self::B => "B",
}
}
#[must_use]
pub fn from_semitone(s: u8) -> Self {
match s % 12 {
0 => Self::C,
1 => Self::Cs,
2 => Self::D,
3 => Self::Ds,
4 => Self::E,
5 => Self::F,
6 => Self::Fs,
7 => Self::G,
8 => Self::Gs,
9 => Self::A,
10 => Self::As,
_ => Self::B,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KeyMode {
Major,
Minor,
}
impl KeyMode {
#[must_use]
pub fn name(self) -> &'static str {
match self {
Self::Major => "major",
Self::Minor => "minor",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MusicalKey {
pub root: PitchClass,
pub mode: KeyMode,
}
impl MusicalKey {
#[must_use]
pub fn new(root: PitchClass, mode: KeyMode) -> Self {
Self { root, mode }
}
#[must_use]
pub fn display(&self) -> String {
format!("{} {}", self.root.name(), self.mode.name())
}
#[must_use]
pub fn relative_key(&self) -> MusicalKey {
match self.mode {
KeyMode::Major => {
let semitone = (self.root.semitone() + 9) % 12;
MusicalKey {
root: PitchClass::from_semitone(semitone),
mode: KeyMode::Minor,
}
}
KeyMode::Minor => {
let semitone = (self.root.semitone() + 3) % 12;
MusicalKey {
root: PitchClass::from_semitone(semitone),
mode: KeyMode::Major,
}
}
}
}
}
pub const KEY_PROFILES_MAJOR: [f32; 12] = [
6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88,
];
pub const KEY_PROFILES_MINOR: [f32; 12] = [
6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17,
];
pub struct KrumhanslSchmuckler;
impl KrumhanslSchmuckler {
#[must_use]
pub fn detect(chroma: &[f32; 12]) -> MusicalKey {
let mut best_key = MusicalKey::new(PitchClass::C, KeyMode::Major);
let mut best_corr = f32::NEG_INFINITY;
for root in 0_u8..12 {
let major_corr = pearson_f32(
chroma,
&rotate_profile_f32(&KEY_PROFILES_MAJOR, root as usize),
);
let minor_corr = pearson_f32(
chroma,
&rotate_profile_f32(&KEY_PROFILES_MINOR, root as usize),
);
if major_corr > best_corr {
best_corr = major_corr;
best_key = MusicalKey::new(PitchClass::from_semitone(root), KeyMode::Major);
}
if minor_corr > best_corr {
best_corr = minor_corr;
best_key = MusicalKey::new(PitchClass::from_semitone(root), KeyMode::Minor);
}
}
best_key
}
}
#[must_use]
fn rotate_profile_f32(profile: &[f32; 12], shift: usize) -> [f32; 12] {
let mut out = [0.0_f32; 12];
for i in 0..12 {
out[(i + shift) % 12] = profile[i];
}
out
}
fn pearson_f32(a: &[f32; 12], b: &[f32; 12]) -> f32 {
let mean_a: f32 = a.iter().sum::<f32>() / 12.0;
let mean_b: f32 = b.iter().sum::<f32>() / 12.0;
let mut num = 0.0_f32;
let mut da2 = 0.0_f32;
let mut db2 = 0.0_f32;
for i in 0..12 {
let da = a[i] - mean_a;
let db = b[i] - mean_b;
num += da * db;
da2 += da * da;
db2 += db * db;
}
let denom = (da2 * db2).sqrt();
if denom < 1e-9 {
0.0
} else {
num / denom
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pitch_class_semitones_c() {
assert_eq!(PitchClass::C.semitone(), 0);
}
#[test]
fn test_pitch_class_semitones_b() {
assert_eq!(PitchClass::B.semitone(), 11);
}
#[test]
fn test_pitch_class_name_c_sharp() {
assert_eq!(PitchClass::Cs.name(), "C#");
}
#[test]
fn test_pitch_class_from_semitone_roundtrip() {
for i in 0_u8..12 {
let pc = PitchClass::from_semitone(i);
assert_eq!(pc.semitone(), i);
}
}
#[test]
fn test_pitch_class_from_semitone_mod12() {
assert_eq!(PitchClass::from_semitone(12), PitchClass::C);
assert_eq!(PitchClass::from_semitone(13), PitchClass::Cs);
}
#[test]
fn test_key_mode_names() {
assert_eq!(KeyMode::Major.name(), "major");
assert_eq!(KeyMode::Minor.name(), "minor");
}
#[test]
fn test_musical_key_display_c_major() {
let k = MusicalKey::new(PitchClass::C, KeyMode::Major);
assert_eq!(k.display(), "C major");
}
#[test]
fn test_musical_key_display_a_minor() {
let k = MusicalKey::new(PitchClass::A, KeyMode::Minor);
assert_eq!(k.display(), "A minor");
}
#[test]
fn test_relative_key_c_major_to_a_minor() {
let c_major = MusicalKey::new(PitchClass::C, KeyMode::Major);
let rel = c_major.relative_key();
assert_eq!(rel.root, PitchClass::A);
assert_eq!(rel.mode, KeyMode::Minor);
}
#[test]
fn test_relative_key_a_minor_to_c_major() {
let a_minor = MusicalKey::new(PitchClass::A, KeyMode::Minor);
let rel = a_minor.relative_key();
assert_eq!(rel.root, PitchClass::C);
assert_eq!(rel.mode, KeyMode::Major);
}
#[test]
fn test_relative_key_roundtrip() {
for root in 0_u8..12 {
let k = MusicalKey::new(PitchClass::from_semitone(root), KeyMode::Major);
let rel = k.relative_key().relative_key();
assert_eq!(rel.root, k.root);
assert_eq!(rel.mode, k.mode);
}
}
#[test]
fn test_detect_c_major_from_profile() {
let result = KrumhanslSchmuckler::detect(&KEY_PROFILES_MAJOR);
assert_eq!(result.root, PitchClass::C);
assert_eq!(result.mode, KeyMode::Major);
}
#[test]
fn test_detect_a_minor_from_profile() {
let mut chroma = [0.0_f32; 12];
for i in 0..12 {
chroma[(i + 9) % 12] = KEY_PROFILES_MINOR[i];
}
let result = KrumhanslSchmuckler::detect(&chroma);
assert_eq!(result.root, PitchClass::A);
assert_eq!(result.mode, KeyMode::Minor);
}
#[test]
fn test_detect_returns_valid_key() {
let chroma = [1.0_f32; 12];
let result = KrumhanslSchmuckler::detect(&chroma);
let _ = result.display();
}
}