use crate::error::AnalysisError;
const EPSILON: f32 = 1e-10;
const MIN_SEGMENT_DURATION_S: f32 = 2.0;
const TEMPO_VARIATION_THRESHOLD: f32 = 0.15;
#[derive(Debug, Clone)]
pub struct TempoSegment {
pub start_time: f32,
pub end_time: f32,
pub bpm: f32,
pub confidence: f32,
pub is_variable: bool,
}
pub fn detect_tempo_variations(
beats: &[f32],
nominal_bpm: f32,
) -> Result<Vec<TempoSegment>, AnalysisError> {
if beats.len() < 4 {
return Ok(vec![TempoSegment {
start_time: if beats.is_empty() { 0.0 } else { beats[0] },
end_time: if beats.is_empty() { 0.0 } else { beats[beats.len() - 1] },
bpm: nominal_bpm,
confidence: 0.5,
is_variable: false,
}]);
}
if nominal_bpm <= EPSILON {
return Err(AnalysisError::InvalidInput(
format!("Invalid nominal BPM: {:.2}", nominal_bpm)
));
}
let total_duration = beats[beats.len() - 1] - beats[0];
if total_duration < MIN_SEGMENT_DURATION_S {
return Ok(vec![TempoSegment {
start_time: beats[0],
end_time: beats[beats.len() - 1],
bpm: nominal_bpm,
confidence: 0.8,
is_variable: false,
}]);
}
let segment_duration = (total_duration / 4.0).max(4.0).min(8.0);
let overlap = segment_duration * 0.5;
let mut segments = Vec::new();
let mut current_start = beats[0];
while current_start < beats[beats.len() - 1] {
let segment_end = (current_start + segment_duration).min(beats[beats.len() - 1]);
let segment_beats: Vec<f32> = beats.iter()
.filter(|&&beat| beat >= current_start && beat <= segment_end)
.copied()
.collect();
if segment_beats.len() >= 3 {
let mut intervals = Vec::new();
for i in 1..segment_beats.len() {
let interval = segment_beats[i] - segment_beats[i - 1];
if interval > 0.0 {
intervals.push(interval);
}
}
if !intervals.is_empty() {
let mean_interval: f32 = intervals.iter().sum::<f32>() / intervals.len() as f32;
let variance: f32 = intervals.iter()
.map(|&interval| {
let diff = interval - mean_interval;
diff * diff
})
.sum::<f32>() / intervals.len() as f32;
let std_dev = variance.sqrt();
let cv = if mean_interval > EPSILON {
std_dev / mean_interval
} else {
0.0
};
let segment_bpm = if mean_interval > EPSILON {
60.0 / mean_interval
} else {
nominal_bpm
};
let confidence = (1.0 - (cv / 0.3).min(1.0)).max(0.0);
let is_variable = cv > TEMPO_VARIATION_THRESHOLD;
segments.push(TempoSegment {
start_time: current_start,
end_time: segment_end,
bpm: segment_bpm,
confidence,
is_variable,
});
}
}
current_start += segment_duration - overlap;
}
if segments.is_empty() {
segments.push(TempoSegment {
start_time: beats[0],
end_time: beats[beats.len() - 1],
bpm: nominal_bpm,
confidence: 0.8,
is_variable: false,
});
}
Ok(segments)
}
pub fn has_tempo_variation(segments: &[TempoSegment]) -> bool {
segments.iter().any(|seg| seg.is_variable)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_tempo_variations_constant() {
let beat_interval = 60.0 / 120.0;
let beats: Vec<f32> = (0..20).map(|i| i as f32 * beat_interval).collect();
let segments = detect_tempo_variations(&beats, 120.0).unwrap();
assert!(!segments.is_empty());
assert!(!has_tempo_variation(&segments));
}
#[test]
fn test_detect_tempo_variations_variable() {
let mut beats = Vec::new();
let mut time = 0.0;
for i in 0..20 {
let bpm = 120.0 + (i as f32 * 1.0); let interval = 60.0 / bpm;
time += interval;
beats.push(time);
}
let segments = detect_tempo_variations(&beats, 120.0).unwrap();
assert!(!segments.is_empty());
}
#[test]
fn test_detect_tempo_variations_insufficient_beats() {
let beats = vec![0.0, 0.5, 1.0];
let segments = detect_tempo_variations(&beats, 120.0).unwrap();
assert_eq!(segments.len(), 1);
assert_eq!(segments[0].bpm, 120.0);
}
#[test]
fn test_detect_tempo_variations_empty() {
let segments = detect_tempo_variations(&[], 120.0).unwrap();
assert_eq!(segments.len(), 1);
assert_eq!(segments[0].bpm, 120.0);
}
#[test]
fn test_has_tempo_variation() {
let segments = vec![
TempoSegment {
start_time: 0.0,
end_time: 4.0,
bpm: 120.0,
confidence: 0.8,
is_variable: false,
},
TempoSegment {
start_time: 4.0,
end_time: 8.0,
bpm: 130.0,
confidence: 0.6,
is_variable: true,
},
];
assert!(has_tempo_variation(&segments));
let constant_segments = vec![
TempoSegment {
start_time: 0.0,
end_time: 4.0,
bpm: 120.0,
confidence: 0.8,
is_variable: false,
},
];
assert!(!has_tempo_variation(&constant_segments));
}
}