use crate::stream::Part;
use super::discrete::{find_key, pitch_class_distribution, KeyAnalysisResult, KeyFindingAlgorithm};
#[derive(Debug, Clone, PartialEq)]
pub struct WindowedKeyResult {
pub start_measure: usize,
pub window_size: usize,
pub result: KeyAnalysisResult,
}
pub fn analyze_floating_key(
part: &Part,
window_size: usize,
algorithm: KeyFindingAlgorithm,
) -> Vec<WindowedKeyResult> {
let window_size = window_size.max(1);
let num_measures = part.num_measures();
let mut results = Vec::new();
let mut start = 0;
while start < num_measures {
let end = (start + window_size).min(num_measures);
let notes: Vec<_> = (start..end)
.filter_map(|i| part.measure(i))
.flat_map(|m| m.notes().cloned())
.collect();
let distribution = pitch_class_distribution(¬es);
let result = find_key(&distribution, algorithm);
results.push(WindowedKeyResult {
start_measure: start,
window_size: end - start,
result,
});
start += 1;
}
results
}
pub fn detect_modulations(windowed: &[WindowedKeyResult]) -> Vec<usize> {
windowed
.windows(2)
.filter(|pair| pair[0].result.key != pair[1].result.key)
.map(|pair| pair[1].start_measure)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{Duration, Fraction, Note, Pitch, Step};
use crate::notation::KeyMode;
use crate::stream::{Measure, MusicElement, Part};
fn measure_with_phrase(number: u32, phrase: &[(Step, Duration)]) -> Measure {
let mut measure = Measure::new(number);
let mut offset = Fraction::new(0, 1);
for (step, duration) in phrase.iter().cloned() {
let quarter_length = duration.quarter_length();
measure.insert(
offset,
MusicElement::Note(Note::new(Pitch::from_parts(step, Some(4), None), duration)),
);
offset += quarter_length;
}
measure
}
fn c_major_measure(number: u32) -> Measure {
measure_with_phrase(
number,
&[
(Step::C, Duration::half()),
(Step::G, Duration::quarter()),
(Step::C, Duration::quarter()),
],
)
}
fn g_major_measure(number: u32) -> Measure {
measure_with_phrase(
number,
&[
(Step::G, Duration::half()),
(Step::D, Duration::quarter()),
(Step::G, Duration::quarter()),
],
)
}
#[test]
fn test_analyze_floating_key_produces_one_result_per_window_position() {
let mut part = Part::new();
for i in 1..=4 {
part.add_measure(c_major_measure(i));
}
let windowed = analyze_floating_key(&part, 2, KeyFindingAlgorithm::KrumhanslSchmuckler);
assert_eq!(windowed.len(), 4);
assert_eq!(windowed[0].start_measure, 0);
assert_eq!(windowed[0].window_size, 2);
assert_eq!(windowed[3].start_measure, 3);
assert_eq!(windowed[3].window_size, 1);
}
#[test]
fn test_analyze_floating_key_detects_modulation() {
let mut part = Part::new();
for i in 1..=4 {
part.add_measure(c_major_measure(i));
}
for i in 5..=8 {
part.add_measure(g_major_measure(i));
}
let windowed = analyze_floating_key(&part, 2, KeyFindingAlgorithm::KrumhanslSchmuckler);
assert_eq!(windowed[0].result.key.tonic().step(), Step::C);
assert_eq!(windowed[0].result.key.mode(), KeyMode::Major);
let last = windowed.last().unwrap();
assert_eq!(last.result.key.tonic().step(), Step::G);
assert_eq!(last.result.key.mode(), KeyMode::Major);
let modulations = detect_modulations(&windowed);
assert!(!modulations.is_empty());
}
}