1use crate::{
2 chord::Chord,
3 defaults::{FloatType, IntegerType},
4 error::{Error, Result},
5 interval::Interval,
6 key::Key,
7 notation::TieType,
8 note::Note,
9 pitch::Pitch,
10 stream::{Stream, StreamElement},
11};
12
13pub mod correlate;
14pub mod elements;
15pub mod enharmonics;
16pub mod floating_key;
17pub mod harmonic_function;
18pub mod metrical;
19pub mod neoriemannian;
20pub mod patel;
21pub mod pitch_analysis;
22pub mod reduce_chords;
23pub mod reduction;
24pub mod segment_by_rests;
25pub mod transposition;
26pub mod windowed;
27
28#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
34pub enum KeyProfile {
35 KrumhanslSchmuckler,
38 AardenEssen,
41 SimpleWeights,
43 BellmanBudge,
45 TemperleyKostkaPayne,
48}
49
50impl KeyProfile {
51 pub const ALL: [KeyProfile; 5] = [
53 Self::KrumhanslSchmuckler,
54 Self::AardenEssen,
55 Self::SimpleWeights,
56 Self::BellmanBudge,
57 Self::TemperleyKostkaPayne,
58 ];
59
60 pub fn major_weights(self) -> [FloatType; 12] {
62 match self {
63 Self::KrumhanslSchmuckler => [
64 6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88,
65 ],
66 Self::AardenEssen => [
67 17.7661, 0.145624, 14.9265, 0.160186, 19.8049, 11.3587, 0.291248, 22.062, 0.145624,
68 8.15494, 0.232998, 4.95122,
69 ],
70 Self::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],
71 Self::BellmanBudge => [
72 16.80, 0.86, 12.95, 1.41, 13.49, 11.93, 1.25, 20.28, 1.80, 8.04, 0.62, 10.57,
73 ],
74 Self::TemperleyKostkaPayne => [
75 0.748, 0.060, 0.488, 0.082, 0.670, 0.460, 0.096, 0.715, 0.104, 0.366, 0.057, 0.400,
76 ],
77 }
78 }
79
80 pub fn minor_weights(self) -> [FloatType; 12] {
82 match self {
83 Self::KrumhanslSchmuckler => [
84 6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17,
85 ],
86 Self::AardenEssen => [
87 18.2648, 0.737619, 14.0499, 16.8599, 0.702494, 14.4362, 0.702494, 18.6161, 4.56621,
88 1.93186, 7.37619, 1.75623,
89 ],
90 Self::SimpleWeights => [2.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 2.0, 1.0, 0.0, 0.5, 0.5],
91 Self::BellmanBudge => [
92 18.16, 0.69, 12.99, 13.34, 1.07, 11.15, 1.38, 21.07, 7.49, 1.53, 0.92, 10.21,
93 ],
94 Self::TemperleyKostkaPayne => [
95 0.712, 0.084, 0.474, 0.618, 0.049, 0.460, 0.105, 0.747, 0.404, 0.067, 0.133, 0.330,
96 ],
97 }
98 }
99
100 pub fn music21_name(self) -> &'static str {
103 match self {
104 Self::KrumhanslSchmuckler => "Krumhansl Schmuckler/Kessler Key Analysis",
105 Self::AardenEssen => "Aarden Essen Key Analysis",
106 Self::SimpleWeights => "Simple Weight Key Analysis",
107 Self::BellmanBudge => "Bellman Budge Key Analysis",
108 Self::TemperleyKostkaPayne => "Temperley Kostka Payne Key Analysis",
109 }
110 }
111
112 pub fn identifiers(self) -> &'static [&'static str] {
115 match self {
116 Self::KrumhanslSchmuckler => &[
117 "key.krumhansl",
118 "key.schmuckler",
119 "key.krumhansl-schmuckler",
120 "key.krumhanslschmuckler",
121 "krumhansl",
122 "schmuckler",
123 "krumhansl-schmuckler",
124 "krumhanslschmuckler",
125 "key.kessler",
126 "key.krumhansl-kessler",
127 "key.krumhanslkessler",
128 "kessler",
129 "krumhansl-kessler",
130 "krumhanslkessler",
131 ],
132 Self::AardenEssen => &[
133 "key.aarden",
134 "key.essen",
135 "key.aarden-essen",
136 "key.aardenessen",
137 "aarden",
138 "essen",
139 "aarden-essen",
140 "aardenessen",
141 "key",
143 "keyscape",
144 ],
145 Self::SimpleWeights => &[
146 "key.simple",
147 "key.weight",
148 "key.simple-weight",
149 "key.simpleweight",
150 "simple",
151 "weight",
152 "simple-weight",
153 "simpleweight",
154 ],
155 Self::BellmanBudge => &[
156 "key.bellman",
157 "key.budge",
158 "key.bellman-budge",
159 "key.bellmanbudge",
160 "bellman",
161 "budge",
162 "bellman-budge",
163 "bellmanbudge",
164 ],
165 Self::TemperleyKostkaPayne => &[
166 "key.temperley",
167 "key.kostka",
168 "key.payne",
169 "key.temperley-kostka-payne",
170 "key.temperleykostkapayne",
171 "temperley",
172 "kostka",
173 "payne",
174 "temperley-kostka-payne",
175 "temperleykostkapayne",
176 ],
177 }
178 }
179
180 pub fn music21_class_name(self) -> &'static str {
182 match self {
183 Self::KrumhanslSchmuckler => "KrumhanslSchmuckler",
184 Self::AardenEssen => "AardenEssen",
185 Self::SimpleWeights => "SimpleWeights",
186 Self::BellmanBudge => "BellmanBudge",
187 Self::TemperleyKostkaPayne => "TemperleyKostkaPayne",
188 }
189 }
190}
191
192#[derive(Clone, Debug)]
194#[must_use]
195pub struct KeyEstimate {
196 key: Key,
197 score: FloatType,
198}
199
200impl KeyEstimate {
201 pub fn key(&self) -> &Key {
203 &self.key
204 }
205
206 pub fn score(&self) -> FloatType {
208 self.score
209 }
210}
211
212pub fn estimate_key_from_pitches(pitches: &[Pitch]) -> Result<Vec<KeyEstimate>> {
214 estimate_key_from_pitches_with(KeyProfile::KrumhanslSchmuckler, pitches)
215}
216
217pub fn estimate_key_from_pitches_with(
220 profile: KeyProfile,
221 pitches: &[Pitch],
222) -> Result<Vec<KeyEstimate>> {
223 if pitches.is_empty() {
224 return Err(Error::Analysis(
225 "key estimation needs at least one pitch".to_string(),
226 ));
227 }
228 let distribution = pitch_class_distribution(
229 pitches
230 .iter()
231 .map(|pitch| (std::slice::from_ref(pitch), 1.0)),
232 )
233 .expect("there is at least one pitch");
234 Ok(estimate_key_from_distribution(profile, &distribution))
235}
236
237pub fn estimate_key_from_chords(chords: &[Chord]) -> Result<Vec<KeyEstimate>> {
239 estimate_key_from_chords_with(KeyProfile::KrumhanslSchmuckler, chords)
240}
241
242pub fn estimate_key_from_chords_with(
244 profile: KeyProfile,
245 chords: &[Chord],
246) -> Result<Vec<KeyEstimate>> {
247 let pitches = chords.iter().flat_map(Chord::pitches).collect::<Vec<_>>();
248 estimate_key_from_pitches_with(profile, &pitches)
249}
250
251pub fn pitch_class_distribution<'a>(
257 notes: impl IntoIterator<Item = (&'a [Pitch], FloatType)>,
258) -> Option<[FloatType; 12]> {
259 let mut distribution = [0.0; 12];
260 let mut any = false;
261 for (pitches, length) in notes {
262 any = true;
263 for pitch in pitches {
264 let class = (pitch.ps().round_ties_even() as IntegerType).rem_euclid(12) as usize;
265 distribution[class] += length;
266 }
267 }
268 any.then_some(distribution)
269}
270
271pub fn stream_distribution(stream: &Stream) -> Option<[FloatType; 12]> {
278 let notes: Vec<(Vec<Pitch>, FloatType)> = stream
279 .flatten()
280 .events()
281 .iter()
282 .filter_map(|event| {
283 let element = event.element();
284 let pitches = match element {
285 StreamElement::Note(_)
286 | StreamElement::Chord(_)
287 | StreamElement::PercussionChord(_) => element.pitches(),
288 StreamElement::ChordSymbol(symbol) => symbol.pitches().unwrap_or_default(),
289 _ => return None,
290 };
291 Some((pitches, element.quarter_length()))
292 })
293 .collect();
294 pitch_class_distribution(
295 notes
296 .iter()
297 .map(|(pitches, length)| (pitches.as_slice(), *length)),
298 )
299}
300
301pub fn estimate_key_of_stream(profile: KeyProfile, stream: &Stream) -> Option<Vec<KeyEstimate>> {
305 stream_distribution(stream)
306 .map(|distribution| estimate_key_from_distribution(profile, &distribution))
307}
308
309pub fn stream_pitch_span(stream: &Stream) -> Option<(Pitch, Pitch)> {
312 pitch_span(&stream.pitches())
313}
314
315pub fn melodic_lines(stream: &Stream) -> Vec<Vec<Pitch>> {
320 let leaves = stream.leaves();
321 melodic_line_positions(stream)
322 .into_iter()
323 .map(|line| {
324 line.into_iter()
325 .filter_map(|position| match leaves.get(position).map(|leaf| leaf.1) {
326 Some(StreamElement::Note(note)) => Some(note.pitch().clone()),
327 _ => None,
328 })
329 .collect()
330 })
331 .collect()
332}
333
334pub fn melodic_line_positions(stream: &Stream) -> Vec<Vec<usize>> {
363 let leaves = stream.leaves();
364 let tops: Option<Vec<usize>> = stream.has_part_like_streams().then(|| {
365 stream
366 .events()
367 .iter()
368 .enumerate()
369 .filter(|(_, event)| event.element().as_stream().is_some())
370 .map(|(index, _)| index)
371 .collect()
372 });
373 let order = Stream::flatten_order(&leaves);
376 let under = stream.leaves_under_top();
377 let of_line = |top: Option<usize>| -> Vec<usize> {
378 order
379 .iter()
380 .copied()
381 .filter(|position| match (top, under.get(*position)) {
382 (Some(top), Some((held, _, _))) => *held == top,
383 (None, _) => true,
384 _ => false,
385 })
386 .filter(|position| {
387 matches!(
388 leaves.get(*position).map(|leaf| leaf.1),
389 Some(StreamElement::Note(note)) if !continues_tie(note)
390 )
391 })
392 .collect()
393 };
394 match tops {
395 Some(tops) => tops.into_iter().map(|top| of_line(Some(top))).collect(),
396 None => vec![of_line(None)],
397 }
398}
399
400fn continues_tie(note: &Note) -> bool {
403 note.tie().is_some_and(|tie| {
404 matches!(
405 tie.tie_type(),
406 TieType::Stop | TieType::Continue | TieType::ContinueLetRing
407 )
408 })
409}
410
411const VALID_MAJOR: [&str; 15] = [
413 "C", "C#", "Cb", "Db", "D", "Eb", "E", "F", "F#", "Gb", "G", "Ab", "A", "Bb", "B",
414];
415
416const VALID_MINOR: [&str; 15] = [
418 "C", "C#", "D", "D#", "Eb", "E", "F", "F#", "G", "G#", "Ab", "A", "A#", "Bb", "B",
419];
420
421pub fn estimate_key_from_distribution(
434 profile: KeyProfile,
435 distribution: &[FloatType; 12],
436) -> Vec<KeyEstimate> {
437 let histogram_average = crate::statistics::python_sum(distribution) / 12.0;
438 let mut ranked: Vec<(FloatType, usize, &'static str)> = Vec::with_capacity(24);
439 for (mode, weights) in [
440 ("major", profile.major_weights()),
441 ("minor", profile.minor_weights()),
442 ] {
443 let profile_average = crate::statistics::python_sum(&weights) / 12.0;
444 for tonic in 0..12 {
445 let mut top = 0.0;
446 let mut bottom_right = 0.0;
447 let mut bottom_left = 0.0;
448 for (class, value) in distribution.iter().enumerate() {
449 let weight = weights[(class + 12 - tonic) % 12];
450 top += (weight - profile_average) * (value - histogram_average);
451 bottom_right += (weight - profile_average) * (weight - profile_average);
452 bottom_left += (value - histogram_average) * (value - histogram_average);
453 }
454 let score = if bottom_right == 0.0 || bottom_left == 0.0 {
455 0.0
456 } else {
457 top / (bottom_right * bottom_left).sqrt()
458 };
459 ranked.push((score, tonic, mode));
460 }
461 }
462 ranked.sort_by(|a, b| b.0.total_cmp(&a.0).then(b.1.cmp(&a.1)).then(b.2.cmp(a.2)));
463 ranked
464 .into_iter()
465 .map(|(score, tonic, mode)| KeyEstimate {
466 key: key_on(tonic, mode),
467 score,
468 })
469 .collect()
470}
471
472fn key_on(tonic: usize, mode: &str) -> Key {
475 const USUAL: [&str; 12] = [
476 "C", "C#", "D", "Eb", "E", "F", "F#", "G", "G#", "A", "Bb", "B",
477 ];
478 const RESPELLED: [&str; 12] = [
479 "B#", "Db", "C##", "D#", "Fb", "E#", "Gb", "F##", "Ab", "Bbb", "A#", "Cb",
480 ];
481 let valid: &[&str] = if mode == "major" {
482 &VALID_MAJOR
483 } else {
484 &VALID_MINOR
485 };
486 let name = if valid.contains(&USUAL[tonic]) {
487 USUAL[tonic]
488 } else {
489 RESPELLED[tonic]
490 };
491 Key::from_tonic_mode(name, mode).expect("every analysis tonic names a key")
492}
493
494pub fn pitch_span(pitches: &[Pitch]) -> Option<(Pitch, Pitch)> {
498 let first = pitches.first()?;
499 let mut lowest = first;
500 let mut highest = first;
501 for pitch in &pitches[1..] {
502 if pitch.ps() < lowest.ps() {
503 lowest = pitch;
504 }
505 if pitch.ps() > highest.ps() {
506 highest = pitch;
507 }
508 }
509 Some((lowest.clone(), highest.clone()))
510}
511
512pub fn melodic_interval_counts(
523 lines: &[Vec<Pitch>],
524 ignore_direction: bool,
525 ignore_unison: bool,
526) -> Result<Vec<(Interval, usize)>> {
527 let mut found: Vec<(String, Interval, usize)> = Vec::new();
528 for line in lines {
529 for pair in line.windows(2) {
530 let mut interval = Interval::between_pitches(&pair[0], &pair[1])?;
531 let semitones = interval.semitones();
532 if ignore_unison && semitones == 0.0 {
533 continue;
534 }
535 if ignore_direction && semitones < 0.0 {
536 interval = interval.reversed()?;
537 }
538 let name = interval.directed_name();
539 match found.iter_mut().find(|(known, _, _)| *known == name) {
540 Some((_, _, count)) => *count += 1,
541 None => found.push((name, interval, 1)),
542 }
543 }
544 }
545 Ok(found
546 .into_iter()
547 .map(|(_, interval, count)| (interval, count))
548 .collect())
549}
550
551pub fn tonal_certainty(estimates: &[KeyEstimate]) -> FloatType {
556 let scores: Vec<FloatType> = estimates.iter().map(KeyEstimate::score).collect();
557 tonal_certainty_from_scores(&scores)
558}
559
560pub fn tonal_certainty_from_scores(scores: &[FloatType]) -> FloatType {
564 let Some(leader) = scores.first().copied() else {
565 return 0.0;
566 };
567 match scores[1..].iter().copied().find(|score| *score > 0.0) {
568 Some(second) => leader + 2.0 * (leader - second),
569 None => leader.max(0.0),
570 }
571}
572
573#[cfg(test)]
574mod tests {
575
576 #[test]
577 fn a_stream_is_weighed_as_music21_weighs_it() {
578 use super::*;
579 use crate::{Duration, Note, Stream, StreamKind};
580
581 let note = |name: &str, length: FloatType| {
584 let mut note = Note::from_name(name).unwrap();
585 note.set_duration(Duration::new(length).unwrap());
586 note
587 };
588 let mut stream = Stream::new();
589 stream.push(note("C4", 3.0));
590 stream.push(note("F#4", 2.0));
591 assert_eq!(
592 stream_distribution(&stream),
593 Some([3.0, 0.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0])
594 );
595 let mut chord = Chord::new("D4 E4 B-4").unwrap();
596 chord.set_duration(Duration::new(1.5).unwrap());
597 stream.push(chord);
598 assert_eq!(
599 stream_distribution(&stream),
600 Some([3.0, 0.0, 1.5, 0.0, 1.5, 0.0, 2.0, 0.0, 0.0, 0.0, 1.5, 0.0])
601 );
602 assert!(estimate_key_of_stream(KeyProfile::AardenEssen, &stream).is_some());
603 assert_eq!(stream_distribution(&Stream::new()), None);
604
605 let (low, high) = stream_pitch_span(&stream).unwrap();
607 assert_eq!(
608 (low.name_with_octave(), high.name_with_octave()),
609 ("C4".into(), "Bb4".into())
610 );
611
612 let mut upper = Stream::with_kind(StreamKind::Part);
614 let mut tied = note("E4", 1.0);
615 tied.set_tie(Some(crate::Tie::new(crate::TieType::Start)));
616 let mut held = note("E4", 1.0);
617 held.set_tie(Some(crate::Tie::new(crate::TieType::Stop)));
618 for element in [note("C4", 1.0), tied, held, note("G4", 1.0)] {
619 upper.push(element);
620 }
621 let mut lower = Stream::with_kind(StreamKind::Part);
622 lower.push(note("C3", 2.0));
623 lower.push(note("G2", 2.0));
624 let mut score = Stream::with_kind(StreamKind::Score);
625 score.insert(0.0, upper);
626 score.insert(0.0, lower);
627 let lines: Vec<Vec<String>> = melodic_lines(&score)
628 .iter()
629 .map(|line| line.iter().map(Pitch::name_with_octave).collect())
630 .collect();
631 assert_eq!(lines, [vec!["C4", "E4", "G4"], vec!["C3", "G2"]]);
632 }
633
634 #[test]
635 fn a_profile_names_its_music21_class() {
636 assert_eq!(KeyProfile::AardenEssen.music21_class_name(), "AardenEssen");
637 assert_eq!(
638 KeyProfile::KrumhanslSchmuckler.music21_class_name(),
639 "KrumhanslSchmuckler"
640 );
641 }
642
643 #[test]
644 fn tonal_certainty_rewards_a_clear_leader() {
645 let scale: Vec<Pitch> = ["C4", "D4", "E4", "F4", "G4", "A4", "B4", "C5"]
646 .iter()
647 .map(|name| Pitch::from_name(*name).unwrap())
648 .collect();
649 let ranked = estimate_key_from_pitches(&scale).unwrap();
650 let leader = ranked[0].score();
651 let second = ranked[1].score();
652 assert!(second > 0.0);
653 assert!((tonal_certainty(&ranked) - (leader + 2.0 * (leader - second))).abs() < 1e-12);
654 assert_eq!(tonal_certainty(&ranked[..1]), leader.max(0.0));
655 assert_eq!(tonal_certainty(&[]), 0.0);
656 }
657 use super::*;
658
659 #[test]
660 fn estimates_c_major_from_tonic_triad_material() {
661 let pitches = ["C4", "E4", "G4", "C5", "E5", "G5"]
662 .into_iter()
663 .map(Pitch::from_name)
664 .collect::<Result<Vec<_>>>()
665 .unwrap();
666 let estimates = estimate_key_from_pitches(&pitches).unwrap();
667 assert_eq!(estimates[0].key().tonic().name(), "C");
668 assert_eq!(estimates[0].key().mode(), "major");
669 }
670
671 #[test]
672 fn estimates_from_chords() {
673 let chords = [Chord::new("C E G").unwrap(), Chord::new("F A C").unwrap()];
674 let estimates = estimate_key_from_chords(&chords).unwrap();
675 assert!(!estimates.is_empty());
676 }
677
678 #[test]
679 fn every_profile_agrees_on_unambiguous_material() {
680 let pitches = ["C4", "D4", "E4", "F4", "G4", "A4", "B4", "C5", "G4", "C4"]
681 .into_iter()
682 .map(Pitch::from_name)
683 .collect::<Result<Vec<_>>>()
684 .unwrap();
685 for profile in KeyProfile::ALL {
686 let estimates = estimate_key_from_pitches_with(profile, &pitches).unwrap();
687 assert_eq!(estimates[0].key().tonic().name(), "C", "{profile:?}");
688 assert_eq!(estimates[0].key().mode(), "major", "{profile:?}");
689 }
690 }
691}