use crate::analysis::result::Key;
use crate::error::AnalysisError;
use super::{detector::detect_key, templates::KeyTemplates};
#[derive(Debug, Clone)]
pub struct KeyChange {
pub timestamp: f32,
pub from_key: Key,
pub to_key: Key,
pub confidence: f32,
}
#[derive(Debug, Clone)]
pub struct KeyChangeResult {
pub primary_key: Key,
pub primary_confidence: f32,
pub key_changes: Vec<KeyChange>,
pub segment_keys: Vec<(f32, Key, f32)>, }
pub fn detect_key_changes(
chroma_vectors: &[Vec<f32>],
sample_rate: u32,
hop_size: usize,
templates: &KeyTemplates,
segment_duration: f32,
segment_overlap: f32,
) -> Result<KeyChangeResult, AnalysisError> {
log::debug!("Detecting key changes: {} chroma vectors, segment_duration={:.1}s, overlap={:.1}s",
chroma_vectors.len(), segment_duration, segment_overlap);
if chroma_vectors.is_empty() {
return Err(AnalysisError::InvalidInput(
"Empty chroma vectors".to_string()
));
}
let frames_per_second = sample_rate as f32 / hop_size as f32;
let segment_frames = (segment_duration * frames_per_second) as usize;
let overlap_frames = (segment_overlap * frames_per_second) as usize;
let hop_frames = segment_frames - overlap_frames;
if segment_frames == 0 || hop_frames == 0 {
return Err(AnalysisError::InvalidInput(
"Invalid segment parameters".to_string()
));
}
let mut segment_keys = Vec::new();
let mut segment_idx = 0;
while segment_idx * hop_frames + segment_frames <= chroma_vectors.len() {
let start_frame = segment_idx * hop_frames;
let end_frame = start_frame + segment_frames;
let segment_chroma: Vec<Vec<f32>> = chroma_vectors[start_frame..end_frame]
.iter()
.cloned()
.collect();
match detect_key(&segment_chroma, templates) {
Ok(key_result) => {
let timestamp = (start_frame as f32) / frames_per_second;
segment_keys.push((timestamp, key_result.key, key_result.confidence));
}
Err(e) => {
log::warn!("Key detection failed for segment {}: {}", segment_idx, e);
}
}
segment_idx += 1;
}
if segment_keys.is_empty() {
return Err(AnalysisError::InvalidInput(
"No valid segments found".to_string()
));
}
let mut key_counts: std::collections::HashMap<Key, (usize, f32)> = std::collections::HashMap::new();
for (_, key, confidence) in &segment_keys {
let entry = key_counts.entry(*key).or_insert((0, 0.0));
entry.0 += 1;
entry.1 += confidence;
}
let (primary_key, (count, total_confidence)) = key_counts.iter()
.max_by_key(|(_, (count, _))| *count)
.unwrap();
let primary_confidence = total_confidence / *count as f32;
let mut key_changes = Vec::new();
for i in 1..segment_keys.len() {
let (_prev_timestamp, prev_key, prev_conf) = &segment_keys[i - 1];
let (curr_timestamp, curr_key, curr_conf) = &segment_keys[i];
if prev_key != curr_key {
let change_confidence = (*prev_conf + *curr_conf) / 2.0;
if change_confidence > 0.2 {
key_changes.push(KeyChange {
timestamp: *curr_timestamp,
from_key: *prev_key,
to_key: *curr_key,
confidence: change_confidence,
});
}
}
}
log::debug!("Detected {} key changes, primary key: {:?} (confidence: {:.3})",
key_changes.len(), primary_key, primary_confidence);
Ok(KeyChangeResult {
primary_key: *primary_key,
primary_confidence,
key_changes,
segment_keys,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_key_changes_empty() {
let templates = KeyTemplates::new();
let result = detect_key_changes(&[], 44100, 512, &templates, 8.0, 2.0);
assert!(result.is_err());
}
#[test]
fn test_detect_key_changes_basic() {
let templates = KeyTemplates::new();
let mut chroma_vectors = Vec::new();
for _ in 0..1000 {
let mut chroma = vec![0.0f32; 12];
chroma[0] = 1.0; chroma[4] = 0.8; chroma[7] = 0.9; chroma[5] = 0.3; chroma[9] = 0.2; let norm: f32 = chroma.iter().map(|&x| x * x).sum::<f32>().sqrt();
if norm > 1e-6 {
for x in &mut chroma {
*x /= norm;
}
}
chroma_vectors.push(chroma);
}
let result = detect_key_changes(&chroma_vectors, 44100, 512, &templates, 4.0, 1.0);
assert!(result.is_ok(), "Key change detection should succeed");
let change_result = result.unwrap();
assert!(matches!(change_result.primary_key, Key::Major(_) | Key::Minor(_)),
"Primary key should be valid, got {:?}", change_result.primary_key);
assert!(change_result.primary_confidence >= 0.0 && change_result.primary_confidence <= 1.0,
"Primary confidence should be in [0, 1], got {}", change_result.primary_confidence);
if change_result.primary_confidence == 0.0 {
assert!(!change_result.segment_keys.is_empty(),
"Should have at least one segment even with 0 confidence");
}
}
}