use crate::core::{Note, Pitch, Step};
use crate::notation::{Key, KeyMode};
use crate::stream::Part;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KeyFindingAlgorithm {
KrumhanslSchmuckler,
AardenEssen,
SimpleWeights,
BellmanBudge,
TemperleyKostkaPayne,
}
impl KeyFindingAlgorithm {
fn profiles(&self) -> ([f64; 12], [f64; 12]) {
match self {
KeyFindingAlgorithm::KrumhanslSchmuckler => (
[6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88],
[6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17],
),
KeyFindingAlgorithm::AardenEssen => (
[
17.7661, 0.145624, 14.9265, 0.160186, 19.8049, 11.3587, 0.291248, 22.062,
0.145624, 8.15494, 0.232998, 4.95122,
],
[
18.2648, 0.737619, 14.0499, 16.8599, 0.702494, 14.4362, 0.702494, 18.6161,
4.56621, 1.93186, 7.37619, 1.75623,
],
),
KeyFindingAlgorithm::SimpleWeights => (
[2.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 2.0, 0.0, 1.0, 0.0, 1.0],
[2.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 2.0, 1.0, 0.0, 0.5, 0.5],
),
KeyFindingAlgorithm::BellmanBudge => (
[16.80, 0.86, 12.95, 1.41, 13.49, 11.93, 1.25, 20.28, 1.80, 8.04, 0.62, 10.57],
[18.16, 0.69, 12.99, 13.34, 1.07, 11.15, 1.38, 21.07, 7.49, 1.53, 0.92, 10.21],
),
KeyFindingAlgorithm::TemperleyKostkaPayne => (
[0.748, 0.060, 0.488, 0.082, 0.670, 0.460, 0.096, 0.715, 0.104, 0.366, 0.057, 0.400],
[0.712, 0.084, 0.474, 0.618, 0.049, 0.460, 0.105, 0.747, 0.404, 0.067, 0.133, 0.330],
),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct KeyAnalysisResult {
pub key: Key,
pub correlation: f64,
}
pub fn pitch_class_distribution(notes: &[Note]) -> [f64; 12] {
let mut histogram = [0.0; 12];
for note in notes {
let pc = note.pitch().pitch_class() as usize;
let quarter_length = note.quarter_length();
let weight = *quarter_length.numer() as f64 / *quarter_length.denom() as f64;
histogram[pc] += weight;
}
histogram
}
fn correlation(a: &[f64; 12], b: &[f64; 12]) -> f64 {
let n = a.len() as f64;
let mean_a = a.iter().sum::<f64>() / n;
let mean_b = b.iter().sum::<f64>() / n;
let mut covariance = 0.0;
let mut variance_a = 0.0;
let mut variance_b = 0.0;
for i in 0..a.len() {
let da = a[i] - mean_a;
let db = b[i] - mean_b;
covariance += da * db;
variance_a += da * da;
variance_b += db * db;
}
if variance_a <= 0.0 || variance_b <= 0.0 {
return 0.0;
}
covariance / (variance_a.sqrt() * variance_b.sqrt())
}
fn rotate(profile: &[f64; 12], tonic_pc: usize) -> [f64; 12] {
let mut rotated = [0.0; 12];
for (pc, slot) in rotated.iter_mut().enumerate() {
let degree = (pc + 12 - tonic_pc) % 12;
*slot = profile[degree];
}
rotated
}
fn pitch_class_to_pitch(pc: usize) -> Pitch {
let (step, accidental) = match pc {
0 => (Step::C, None),
1 => (Step::C, Some(crate::core::Accidental::Sharp)),
2 => (Step::D, None),
3 => (Step::D, Some(crate::core::Accidental::Sharp)),
4 => (Step::E, None),
5 => (Step::F, None),
6 => (Step::F, Some(crate::core::Accidental::Sharp)),
7 => (Step::G, None),
8 => (Step::G, Some(crate::core::Accidental::Sharp)),
9 => (Step::A, None),
10 => (Step::A, Some(crate::core::Accidental::Sharp)),
11 => (Step::B, None),
_ => unreachable!("pitch class is always 0..12"),
};
Pitch::from_parts(step, None, accidental)
}
pub fn find_key(distribution: &[f64; 12], algorithm: KeyFindingAlgorithm) -> KeyAnalysisResult {
let (major_profile, minor_profile) = algorithm.profiles();
let mut best_correlation = f64::NEG_INFINITY;
let mut best_tonic_pc = 0usize;
let mut best_mode = KeyMode::Major;
for tonic_pc in 0..12 {
let major_corr = correlation(distribution, &rotate(&major_profile, tonic_pc));
if major_corr > best_correlation {
best_correlation = major_corr;
best_tonic_pc = tonic_pc;
best_mode = KeyMode::Major;
}
let minor_corr = correlation(distribution, &rotate(&minor_profile, tonic_pc));
if minor_corr > best_correlation {
best_correlation = minor_corr;
best_tonic_pc = tonic_pc;
best_mode = KeyMode::Minor;
}
}
KeyAnalysisResult {
key: Key::new(pitch_class_to_pitch(best_tonic_pc), best_mode),
correlation: best_correlation,
}
}
pub fn analyze_part(part: &Part, algorithm: KeyFindingAlgorithm) -> KeyAnalysisResult {
let notes = part.find_consecutive_notes();
let distribution = pitch_class_distribution(¬es);
find_key(&distribution, algorithm)
}
pub fn tonal_certainty(distribution: &[f64; 12], algorithm: KeyFindingAlgorithm) -> f64 {
let (major_profile, minor_profile) = algorithm.profiles();
let mut correlations: Vec<f64> = Vec::with_capacity(24);
for tonic_pc in 0..12 {
correlations.push(correlation(distribution, &rotate(&major_profile, tonic_pc)));
correlations.push(correlation(distribution, &rotate(&minor_profile, tonic_pc)));
}
correlations.sort_by(|a, b| b.partial_cmp(a).unwrap());
let best = correlations[0];
let second = correlations.get(1).copied().unwrap_or(0.0);
if best <= 0.0 {
return 0.0;
}
((best - second) / best).clamp(0.0, 1.0)
}
pub fn analyze_part_with_certainty(
part: &Part,
algorithm: KeyFindingAlgorithm,
) -> (KeyAnalysisResult, f64) {
let notes = part.find_consecutive_notes();
let distribution = pitch_class_distribution(¬es);
(find_key(&distribution, algorithm), tonal_certainty(&distribution, algorithm))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Duration;
use crate::stream::{Measure, MusicElement};
fn note(step: Step, duration: Duration) -> Note {
Note::new(Pitch::from_parts(step, Some(4), None), duration)
}
fn c_major_scale_part() -> Part {
let mut part = Part::new();
let mut measure = Measure::new(1);
let phrase = [
(Step::C, Duration::half()),
(Step::E, Duration::quarter()),
(Step::G, Duration::quarter()),
(Step::G, Duration::half()),
(Step::D, Duration::quarter()),
(Step::B, Duration::quarter()),
(Step::C, Duration::whole()),
];
let mut offset = crate::core::Fraction::new(0, 1);
for (step, duration) in phrase {
let quarter_length = duration.quarter_length();
measure.insert(offset, MusicElement::Note(note(step, duration)));
offset += quarter_length;
}
part.add_measure(measure);
part
}
#[test]
fn test_krumhansl_schmuckler_identifies_c_major() {
let part = c_major_scale_part();
let result = analyze_part(&part, KeyFindingAlgorithm::KrumhanslSchmuckler);
assert_eq!(result.key.tonic().step(), Step::C);
assert_eq!(result.key.mode(), KeyMode::Major);
assert!(result.correlation > 0.5);
}
#[test]
fn test_all_five_algorithms_identify_c_major() {
let part = c_major_scale_part();
for algorithm in [
KeyFindingAlgorithm::KrumhanslSchmuckler,
KeyFindingAlgorithm::AardenEssen,
KeyFindingAlgorithm::SimpleWeights,
KeyFindingAlgorithm::BellmanBudge,
KeyFindingAlgorithm::TemperleyKostkaPayne,
] {
let result = analyze_part(&part, algorithm);
assert_eq!(result.key.tonic().step(), Step::C, "algorithm {algorithm:?} failed");
assert_eq!(result.key.mode(), KeyMode::Major, "algorithm {algorithm:?} failed");
}
}
#[test]
fn test_pitch_class_distribution_weights_by_duration() {
let notes = vec![
note(Step::C, Duration::whole()),
note(Step::D, Duration::quarter()),
];
let distribution = pitch_class_distribution(¬es);
assert_eq!(distribution[0], 4.0); assert_eq!(distribution[2], 1.0); }
#[test]
fn test_rotate_aligns_tonic_to_absolute_pitch_class() {
let profile = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
let rotated = rotate(&profile, 4); assert_eq!(rotated[4], 1.0); assert_eq!(rotated[11], 8.0); }
#[test]
fn test_correlation_zero_variance_is_zero_not_nan() {
let flat = [1.0; 12];
let other = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0];
assert_eq!(correlation(&flat, &other), 0.0);
}
#[test]
fn test_tonal_certainty_high_for_clearly_tonal_distribution() {
let part = c_major_scale_part();
let notes = part.find_consecutive_notes();
let distribution = pitch_class_distribution(¬es);
let certainty = tonal_certainty(&distribution, KeyFindingAlgorithm::KrumhanslSchmuckler);
assert!(certainty > 0.05, "expected clear tonal certainty, got {certainty}");
}
#[test]
fn test_tonal_certainty_zero_for_flat_distribution() {
let flat_distribution = [1.0; 12];
let certainty = tonal_certainty(&flat_distribution, KeyFindingAlgorithm::KrumhanslSchmuckler);
assert_eq!(certainty, 0.0);
}
#[test]
fn test_analyze_part_with_certainty_matches_separate_calls() {
let part = c_major_scale_part();
let (result, certainty) =
analyze_part_with_certainty(&part, KeyFindingAlgorithm::KrumhanslSchmuckler);
assert_eq!(result.key.tonic().step(), Step::C);
assert!(certainty > 0.0);
}
}