use crate::transient::TransientDetector;
use crate::{AnalysisConfig, Result};
pub struct RhythmAnalyzer {
#[allow(dead_code)]
config: AnalysisConfig,
transient_detector: TransientDetector,
}
impl RhythmAnalyzer {
#[must_use]
pub fn new(config: AnalysisConfig) -> Self {
let transient_detector = TransientDetector::new(config.clone());
Self {
config,
transient_detector,
}
}
pub fn analyze(&self, samples: &[f32], sample_rate: f32) -> Result<RhythmFeatures> {
let transients = self.transient_detector.detect(samples, sample_rate)?;
let iois = self.compute_inter_onset_intervals(&transients.transient_times);
let tempo = self.estimate_tempo(&iois, sample_rate);
let regularity = self.compute_regularity(&iois);
let syncopation = self.compute_syncopation(&transients.transient_times, tempo);
Ok(RhythmFeatures {
tempo,
num_onsets: transients.num_transients,
inter_onset_intervals: iois,
rhythm_regularity: regularity,
syncopation,
})
}
#[allow(clippy::unused_self)]
fn compute_inter_onset_intervals(&self, onset_times: &[f32]) -> Vec<f32> {
if onset_times.len() < 2 {
return vec![];
}
onset_times.windows(2).map(|w| w[1] - w[0]).collect()
}
#[allow(clippy::unused_self)]
fn estimate_tempo(&self, iois: &[f32], _sample_rate: f32) -> f32 {
if iois.is_empty() {
return 0.0;
}
let mut sorted_iois = iois.to_vec();
sorted_iois.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let median_ioi = sorted_iois[sorted_iois.len() / 2];
if median_ioi > 0.0 {
60.0 / median_ioi
} else {
0.0
}
}
#[allow(clippy::unused_self)]
fn compute_regularity(&self, iois: &[f32]) -> f32 {
if iois.len() < 2 {
return 0.0;
}
let mean = iois.iter().sum::<f32>() / iois.len() as f32;
let variance = iois.iter().map(|&x| (x - mean).powi(2)).sum::<f32>() / iois.len() as f32;
let std_dev = variance.sqrt();
if mean > 0.0 {
1.0 / (1.0 + std_dev / mean)
} else {
0.0
}
}
#[allow(clippy::unused_self)]
fn compute_syncopation(&self, onset_times: &[f32], tempo: f32) -> f32 {
if onset_times.is_empty() || tempo <= 0.0 {
return 0.0;
}
let beat_period = 60.0 / tempo;
let mut syncopation_score = 0.0;
for &onset in onset_times {
let beat_phase = (onset % beat_period) / beat_period;
let dist_to_beat = (beat_phase - 0.5).abs() * 2.0;
syncopation_score += dist_to_beat;
}
syncopation_score / onset_times.len() as f32
}
}
#[derive(Debug, Clone)]
pub struct RhythmFeatures {
pub tempo: f32,
pub num_onsets: usize,
pub inter_onset_intervals: Vec<f32>,
pub rhythm_regularity: f32,
pub syncopation: f32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rhythm_analyzer() {
let config = AnalysisConfig::default();
let analyzer = RhythmAnalyzer::new(config);
let sample_rate = 44100.0;
let beat_interval = 60.0 / 120.0; let mut samples = vec![0.0; (sample_rate * 4.0) as usize];
for i in 0..8 {
let pos = (i as f32 * beat_interval * sample_rate) as usize;
if pos < samples.len() {
samples[pos] = 1.0;
}
}
let result = analyzer.analyze(&samples, sample_rate);
assert!(result.is_ok());
}
}