#![allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TempoRange {
Grave,
Largo,
Moderato,
Allegro,
Presto,
Prestissimo,
}
impl TempoRange {
#[must_use]
pub fn from_bpm(bpm: f32) -> Self {
match bpm as u32 {
0..=59 => Self::Grave,
60..=75 => Self::Largo,
76..=119 => Self::Moderato,
120..=167 => Self::Allegro,
168..=199 => Self::Presto,
_ => Self::Prestissimo,
}
}
#[must_use]
pub fn label(&self) -> &'static str {
match self {
Self::Grave => "Grave",
Self::Largo => "Largo",
Self::Moderato => "Moderato",
Self::Allegro => "Allegro",
Self::Presto => "Presto",
Self::Prestissimo => "Prestissimo",
}
}
}
#[derive(Debug, Clone)]
pub struct TempoBand {
pub low_hz: f32,
pub high_hz: f32,
pub bpm: Option<f32>,
pub confidence: f32,
}
impl TempoBand {
#[must_use]
pub fn new(low_hz: f32, high_hz: f32) -> Self {
Self {
low_hz,
high_hz,
bpm: None,
confidence: 0.0,
}
}
#[must_use]
pub fn is_reliable(&self) -> bool {
self.confidence >= 0.6 && self.bpm.is_some()
}
}
#[derive(Debug, Clone)]
pub struct TempoResult {
pub bpm: f32,
pub confidence: f32,
pub range: TempoRange,
pub bands: Vec<TempoBand>,
pub half_tempo: Option<f32>,
pub double_tempo: Option<f32>,
}
pub struct TempoAnalyzer {
sample_rate: f32,
frame_size: usize,
hop_size: usize,
}
impl TempoAnalyzer {
#[must_use]
pub fn new(sample_rate: f32, frame_size: usize, hop_size: usize) -> Self {
Self {
sample_rate,
frame_size,
hop_size,
}
}
#[must_use]
pub fn detect_bpm(&self, samples: &[f32]) -> TempoResult {
let onset_env = self.compute_onset_envelope(samples);
let bpm = self.autocorr_bpm(&onset_env);
let confidence = self.estimate_confidence(&onset_env, bpm);
let bands = self.analyse_bands(samples);
let half = if bpm > 60.0 { Some(bpm / 2.0) } else { None };
let double = Some(bpm * 2.0);
TempoResult {
bpm,
confidence,
range: TempoRange::from_bpm(bpm),
bands,
half_tempo: half,
double_tempo: double,
}
}
fn compute_onset_envelope(&self, samples: &[f32]) -> Vec<f32> {
let mut envelope = Vec::new();
let mut prev_power = 0.0_f32;
let mut pos = 0;
while pos + self.frame_size <= samples.len() {
let frame = &samples[pos..pos + self.frame_size];
let power: f32 = frame.iter().map(|&x| x * x).sum::<f32>() / self.frame_size as f32;
let flux = (power - prev_power).max(0.0);
envelope.push(flux);
prev_power = power;
pos += self.hop_size;
}
envelope
}
#[allow(clippy::cast_precision_loss)]
fn autocorr_bpm(&self, envelope: &[f32]) -> f32 {
if envelope.is_empty() {
return 120.0;
}
let fps = self.sample_rate / self.hop_size as f32;
let min_lag = (fps * 60.0 / 240.0) as usize; let max_lag = (fps * 60.0 / 40.0) as usize; let max_lag = max_lag.min(envelope.len() - 1);
if min_lag >= max_lag {
return 120.0;
}
let mut best_lag = min_lag;
let mut best_val = f32::NEG_INFINITY;
for lag in min_lag..=max_lag {
let corr: f32 = envelope
.iter()
.take(envelope.len() - lag)
.zip(envelope.iter().skip(lag))
.map(|(&a, &b)| a * b)
.sum();
if corr > best_val {
best_val = corr;
best_lag = lag;
}
}
fps * 60.0 / best_lag as f32
}
#[allow(clippy::cast_precision_loss, clippy::unused_self)]
fn estimate_confidence(&self, envelope: &[f32], _bpm: f32) -> f32 {
if envelope.is_empty() {
return 0.0;
}
let mean = envelope.iter().sum::<f32>() / envelope.len() as f32;
let var = envelope.iter().map(|&x| (x - mean).powi(2)).sum::<f32>() / envelope.len() as f32;
(var.sqrt() / (mean + 1e-6)).clamp(0.0, 1.0)
}
fn analyse_bands(&self, samples: &[f32]) -> Vec<TempoBand> {
let definitions = [(20.0_f32, 250.0_f32), (250.0, 4000.0), (4000.0, 20000.0)];
let mut bands = Vec::with_capacity(definitions.len());
for (lo, hi) in definitions {
let mut band = TempoBand::new(lo, hi);
let filtered = self.band_pass(samples, lo, hi);
let onset = self.compute_onset_envelope(&filtered);
let bpm = self.autocorr_bpm(&onset);
let conf = self.estimate_confidence(&onset, bpm);
band.bpm = Some(bpm);
band.confidence = conf;
bands.push(band);
}
bands
}
#[allow(clippy::unused_self)]
fn band_pass(&self, samples: &[f32], _low: f32, _high: f32) -> Vec<f32> {
samples.to_vec()
}
}
impl Default for TempoAnalyzer {
fn default() -> Self {
Self::new(44100.0, 2048, 512)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tempo_range_from_bpm_grave() {
assert_eq!(TempoRange::from_bpm(40.0), TempoRange::Grave);
}
#[test]
fn test_tempo_range_from_bpm_largo() {
assert_eq!(TempoRange::from_bpm(72.0), TempoRange::Largo);
}
#[test]
fn test_tempo_range_from_bpm_moderato() {
assert_eq!(TempoRange::from_bpm(100.0), TempoRange::Moderato);
}
#[test]
fn test_tempo_range_from_bpm_allegro() {
assert_eq!(TempoRange::from_bpm(140.0), TempoRange::Allegro);
}
#[test]
fn test_tempo_range_from_bpm_presto() {
assert_eq!(TempoRange::from_bpm(180.0), TempoRange::Presto);
}
#[test]
fn test_tempo_range_from_bpm_prestissimo() {
assert_eq!(TempoRange::from_bpm(210.0), TempoRange::Prestissimo);
}
#[test]
fn test_tempo_range_labels() {
assert_eq!(TempoRange::Grave.label(), "Grave");
assert_eq!(TempoRange::Largo.label(), "Largo");
assert_eq!(TempoRange::Moderato.label(), "Moderato");
assert_eq!(TempoRange::Allegro.label(), "Allegro");
assert_eq!(TempoRange::Presto.label(), "Presto");
assert_eq!(TempoRange::Prestissimo.label(), "Prestissimo");
}
#[test]
fn test_tempo_band_new() {
let band = TempoBand::new(20.0, 250.0);
assert_eq!(band.low_hz, 20.0);
assert_eq!(band.high_hz, 250.0);
assert!(band.bpm.is_none());
assert_eq!(band.confidence, 0.0);
}
#[test]
fn test_tempo_band_reliability_low_confidence() {
let mut band = TempoBand::new(20.0, 250.0);
band.bpm = Some(120.0);
band.confidence = 0.3;
assert!(!band.is_reliable());
}
#[test]
fn test_tempo_band_reliability_high_confidence() {
let mut band = TempoBand::new(20.0, 250.0);
band.bpm = Some(120.0);
band.confidence = 0.8;
assert!(band.is_reliable());
}
#[test]
fn test_analyzer_default_construction() {
let analyzer = TempoAnalyzer::default();
assert_eq!(analyzer.sample_rate, 44100.0);
assert_eq!(analyzer.frame_size, 2048);
assert_eq!(analyzer.hop_size, 512);
}
#[test]
fn test_detect_bpm_on_silence() {
let analyzer = TempoAnalyzer::default();
let silence = vec![0.0_f32; 44100];
let result = analyzer.detect_bpm(&silence);
assert!(result.bpm > 0.0);
}
#[test]
fn test_detect_bpm_returns_range() {
let analyzer = TempoAnalyzer::default();
let samples = vec![0.1_f32; 22050];
let result = analyzer.detect_bpm(&samples);
assert_ne!(result.range, TempoRange::Grave); let _ = result.range.label(); }
#[test]
fn test_detect_bpm_half_and_double() {
let analyzer = TempoAnalyzer::default();
let samples = vec![0.05_f32; 44100];
let result = analyzer.detect_bpm(&samples);
if result.bpm > 60.0 {
assert!(result.half_tempo.is_some());
}
assert!(result.double_tempo.is_some());
}
#[test]
fn test_detect_bpm_band_count() {
let analyzer = TempoAnalyzer::default();
let samples = vec![0.02_f32; 44100];
let result = analyzer.detect_bpm(&samples);
assert_eq!(result.bands.len(), 3);
}
#[test]
fn test_detect_bpm_confidence_range() {
let analyzer = TempoAnalyzer::default();
let samples = vec![0.1_f32; 44100];
let result = analyzer.detect_bpm(&samples);
assert!((0.0..=1.0).contains(&result.confidence));
}
#[test]
fn test_detect_bpm_short_signal() {
let analyzer = TempoAnalyzer::default();
let samples = vec![0.1_f32; 1024];
let result = analyzer.detect_bpm(&samples);
assert!(result.bpm > 0.0);
}
}