use crate::utils::Normalize;
use crate::BlissError;
use bliss_audio_aubio_rs::{OnsetMode, Tempo};
use log::warn;
use ndarray::arr1;
use ndarray_stats::interpolate::Midpoint;
use ndarray_stats::Quantile1dExt;
use noisy_float::prelude::*;
pub(crate) struct BPMDesc {
aubio_obj: Tempo,
bpms: Vec<f32>,
}
impl BPMDesc {
pub const WINDOW_SIZE: usize = 512;
pub const HOP_SIZE: usize = BPMDesc::WINDOW_SIZE / 2;
pub fn new(sample_rate: u32) -> Result<Self, BlissError> {
Ok(BPMDesc {
aubio_obj: Tempo::new(
OnsetMode::SpecFlux,
BPMDesc::WINDOW_SIZE,
BPMDesc::HOP_SIZE,
sample_rate,
)
.map_err(|e| {
BlissError::AnalysisError(format!(
"error while loading aubio tempo object: {}",
e.to_string()
))
})?,
bpms: Vec::new(),
})
}
pub fn do_(&mut self, chunk: &[f32]) -> Result<(), BlissError> {
let result = self.aubio_obj.do_result(chunk).map_err(|e| {
BlissError::AnalysisError(format!(
"aubio error while computing tempo {}",
e.to_string()
))
})?;
if result > 0. {
self.bpms.push(self.aubio_obj.get_bpm());
}
Ok(())
}
pub fn get_value(&mut self) -> f32 {
if self.bpms.is_empty() {
warn!("Set tempo value to zero because no beats were found.");
return -1.;
}
let median = arr1(&self.bpms)
.mapv(n32)
.quantile_mut(n64(0.5), &Midpoint)
.unwrap();
self.normalize(median.into())
}
}
impl Normalize for BPMDesc {
const MAX_VALUE: f32 = 206.;
const MIN_VALUE: f32 = 0.;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Song, SAMPLE_RATE};
#[test]
fn test_tempo_real() {
let song = Song::decode("data/s16_mono_22_5kHz.flac").unwrap();
let mut tempo_desc = BPMDesc::new(SAMPLE_RATE).unwrap();
for chunk in song.sample_array.chunks_exact(BPMDesc::HOP_SIZE) {
tempo_desc.do_(&chunk).unwrap();
}
assert!(0.01 > (0.378605 - tempo_desc.get_value()).abs());
}
#[test]
fn test_tempo_artificial() {
let mut tempo_desc = BPMDesc::new(22050).unwrap();
let mut one_chunk = vec![0.; 22000];
one_chunk.append(&mut vec![1.; 100]);
let chunks = std::iter::repeat(one_chunk.iter())
.take(100)
.flatten()
.cloned()
.collect::<Vec<f32>>();
for chunk in chunks.chunks_exact(BPMDesc::HOP_SIZE) {
tempo_desc.do_(&chunk).unwrap();
}
assert!(0.01 > (-0.416853 - tempo_desc.get_value()).abs());
}
#[test]
fn test_tempo_boundaries() {
let mut tempo_desc = BPMDesc::new(10).unwrap();
let silence_chunk = vec![0.; 1024];
tempo_desc.do_(&silence_chunk).unwrap();
assert_eq!(-1., tempo_desc.get_value());
let mut tempo_desc = BPMDesc::new(22050).unwrap();
let mut one_chunk = vec![0.; 6989];
one_chunk.append(&mut vec![1.; 20]);
let chunks = std::iter::repeat(one_chunk.iter())
.take(500)
.flatten()
.cloned()
.collect::<Vec<f32>>();
for chunk in chunks.chunks_exact(BPMDesc::HOP_SIZE) {
tempo_desc.do_(&chunk).unwrap();
}
assert!(0.01 > (0.86 - tempo_desc.get_value()).abs());
}
}