use crate::error::AnalysisError;
const EPSILON: f32 = 1e-10;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TimeSignature {
FourFour,
ThreeFour,
SixEight,
}
impl TimeSignature {
pub fn beats_per_bar(&self) -> u32 {
match self {
TimeSignature::FourFour => 4,
TimeSignature::ThreeFour => 3,
TimeSignature::SixEight => 6,
}
}
pub fn name(&self) -> &'static str {
match self {
TimeSignature::FourFour => "4/4",
TimeSignature::ThreeFour => "3/4",
TimeSignature::SixEight => "6/8",
}
}
}
pub fn detect_time_signature(
beats: &[f32],
bpm_estimate: f32,
) -> Result<(TimeSignature, f32), AnalysisError> {
if beats.len() < 8 {
return Ok((TimeSignature::FourFour, 0.5));
}
if bpm_estimate <= EPSILON {
return Err(AnalysisError::InvalidInput(
format!("Invalid BPM for time signature detection: {:.2}", bpm_estimate)
));
}
let mut intervals = Vec::new();
for i in 1..beats.len() {
let interval = beats[i] - beats[i - 1];
if interval > 0.0 {
intervals.push(interval);
}
}
if intervals.is_empty() {
return Ok((TimeSignature::FourFour, 0.5));
}
let mean_interval: f32 = intervals.iter().sum::<f32>() / intervals.len() as f32;
let mut scores = Vec::new();
let score_44 = score_time_signature(&intervals, 4, mean_interval);
scores.push((TimeSignature::FourFour, score_44));
let score_34 = score_time_signature(&intervals, 3, mean_interval);
scores.push((TimeSignature::ThreeFour, score_34));
let score_68 = score_time_signature(&intervals, 6, mean_interval);
scores.push((TimeSignature::SixEight, score_68));
let (best_sig, best_score) = scores.iter()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.unwrap();
let confidence = (*best_score).min(1.0).max(0.0);
Ok((*best_sig, confidence))
}
fn score_time_signature(
intervals: &[f32],
beats_per_bar: u32,
mean_interval: f32,
) -> f32 {
if intervals.len() < beats_per_bar as usize {
return 0.0;
}
let lag = beats_per_bar as usize;
let mut autocorr_sum = 0.0;
let mut count = 0;
for i in 0..(intervals.len() - lag) {
let diff = (intervals[i] - intervals[i + lag]).abs();
let similarity = 1.0 / (1.0 + diff / mean_interval);
autocorr_sum += similarity;
count += 1;
}
if count == 0 {
return 0.0;
}
let autocorr = autocorr_sum / count as f32;
let variance: f32 = intervals.iter()
.map(|&interval| {
let diff = interval - mean_interval;
diff * diff
})
.sum::<f32>() / intervals.len() as f32;
let cv = if mean_interval > EPSILON {
variance.sqrt() / mean_interval
} else {
1.0
};
let consistency_score = 1.0 / (1.0 + cv);
(autocorr * 0.7 + consistency_score * 0.3).min(1.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_time_signature_four_four() {
let beat_interval = 60.0 / 120.0; let mut beats = Vec::new();
let mut time = 0.0;
for _ in 0..16 {
beats.push(time);
time += beat_interval;
}
let (time_sig, confidence) = detect_time_signature(&beats, 120.0).unwrap();
assert!(confidence >= 0.0 && confidence <= 1.0);
assert!(matches!(time_sig, TimeSignature::FourFour | TimeSignature::ThreeFour | TimeSignature::SixEight));
}
#[test]
fn test_time_signature_three_four() {
let beat_interval = 60.0 / 120.0; let mut beats = Vec::new();
let mut time = 0.0;
for _ in 0..12 {
beats.push(time);
time += beat_interval;
}
let (_time_sig, confidence) = detect_time_signature(&beats, 120.0).unwrap();
assert!(confidence >= 0.0 && confidence <= 1.0);
}
#[test]
fn test_time_signature_insufficient_beats() {
let beats = vec![0.0, 0.5, 1.0, 1.5];
let (time_sig, confidence) = detect_time_signature(&beats, 120.0).unwrap();
assert_eq!(time_sig, TimeSignature::FourFour);
assert_eq!(confidence, 0.5);
}
#[test]
fn test_time_signature_beats_per_bar() {
assert_eq!(TimeSignature::FourFour.beats_per_bar(), 4);
assert_eq!(TimeSignature::ThreeFour.beats_per_bar(), 3);
assert_eq!(TimeSignature::SixEight.beats_per_bar(), 6);
}
#[test]
fn test_time_signature_name() {
assert_eq!(TimeSignature::FourFour.name(), "4/4");
assert_eq!(TimeSignature::ThreeFour.name(), "3/4");
assert_eq!(TimeSignature::SixEight.name(), "6/8");
}
}