1use crate::{
2 chord::Chord,
3 defaults::{FloatType, IntegerType},
4 error::{Error, Result},
5 key::Key,
6 pitch::Pitch,
7};
8
9const MAJOR_PROFILE: [FloatType; 12] = [
10 6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88,
11];
12const MINOR_PROFILE: [FloatType; 12] = [
13 6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17,
14];
15const TONICS: [&str; 12] = [
16 "C", "C#", "D", "E-", "E", "F", "F#", "G", "A-", "A", "B-", "B",
17];
18
19#[derive(Clone, Debug)]
21pub struct KeyEstimate {
22 key: Key,
23 score: FloatType,
24}
25
26impl KeyEstimate {
27 pub fn key(&self) -> &Key {
29 &self.key
30 }
31
32 pub fn score(&self) -> FloatType {
34 self.score
35 }
36}
37
38pub fn estimate_key_from_pitches(pitches: &[Pitch]) -> Result<Vec<KeyEstimate>> {
40 if pitches.is_empty() {
41 return Err(Error::Analysis(
42 "key estimation needs at least one pitch".to_string(),
43 ));
44 }
45
46 let mut histogram = [0.0; 12];
47 for pitch in pitches {
48 let pc = (pitch.ps().round() as IntegerType).rem_euclid(12) as usize;
49 histogram[pc] += 1.0;
50 }
51
52 estimate_key_from_histogram(&histogram)
53}
54
55pub fn estimate_key_from_chords(chords: &[Chord]) -> Result<Vec<KeyEstimate>> {
57 let pitches = chords.iter().flat_map(Chord::pitches).collect::<Vec<_>>();
58 estimate_key_from_pitches(&pitches)
59}
60
61fn estimate_key_from_histogram(histogram: &[FloatType; 12]) -> Result<Vec<KeyEstimate>> {
62 let mut estimates = Vec::new();
63 for (tonic_pc, tonic) in TONICS.iter().enumerate() {
64 for (mode, profile) in [("major", MAJOR_PROFILE), ("minor", MINOR_PROFILE)] {
65 let key = Key::from_tonic_mode(tonic, mode)?;
66 let rotated = rotate_profile(&profile, tonic_pc);
67 estimates.push(KeyEstimate {
68 key,
69 score: correlation(histogram, &rotated),
70 });
71 }
72 }
73
74 estimates.sort_by(|left, right| {
75 right
76 .score
77 .partial_cmp(&left.score)
78 .unwrap_or(std::cmp::Ordering::Equal)
79 });
80 Ok(estimates)
81}
82
83fn rotate_profile(profile: &[FloatType; 12], tonic_pc: usize) -> [FloatType; 12] {
84 let mut rotated = [0.0; 12];
85 for pc in 0..12 {
86 rotated[pc] = profile[(pc + 12 - tonic_pc) % 12];
87 }
88 rotated
89}
90
91fn correlation(left: &[FloatType; 12], right: &[FloatType; 12]) -> FloatType {
92 let left_mean = left.iter().sum::<FloatType>() / 12.0;
93 let right_mean = right.iter().sum::<FloatType>() / 12.0;
94 let mut numerator = 0.0;
95 let mut left_sum = 0.0;
96 let mut right_sum = 0.0;
97
98 for (left_value, right_value) in left.iter().zip(right) {
99 let left_centered = left_value - left_mean;
100 let right_centered = right_value - right_mean;
101 numerator += left_centered * right_centered;
102 left_sum += left_centered.powi(2);
103 right_sum += right_centered.powi(2);
104 }
105
106 let denominator = left_sum.sqrt() * right_sum.sqrt();
107 if denominator == 0.0 {
108 0.0
109 } else {
110 numerator / denominator
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn estimates_c_major_from_tonic_triad_material() {
120 let pitches = ["C4", "E4", "G4", "C5", "E5", "G5"]
121 .into_iter()
122 .map(Pitch::from_name)
123 .collect::<Result<Vec<_>>>()
124 .unwrap();
125 let estimates = estimate_key_from_pitches(&pitches).unwrap();
126 assert_eq!(estimates[0].key().tonic().name(), "C");
127 assert_eq!(estimates[0].key().mode(), "major");
128 }
129
130 #[test]
131 fn estimates_from_chords() {
132 let chords = [Chord::new("C E G").unwrap(), Chord::new("F A C").unwrap()];
133 let estimates = estimate_key_from_chords(&chords).unwrap();
134 assert!(!estimates.is_empty());
135 }
136}