mecomp_analysis/
temporal.rsuse crate::Feature;
use super::errors::{AnalysisError, AnalysisResult};
use super::utils::Normalize;
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 struct BPMDesc {
aubio_obj: Tempo,
bpms: Vec<f32>,
}
impl BPMDesc {
pub const WINDOW_SIZE: usize = 512;
pub const HOP_SIZE: usize = Self::WINDOW_SIZE / 2;
#[allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
pub fn new(sample_rate: u32) -> AnalysisResult<Self> {
Ok(Self {
aubio_obj: Tempo::new(
OnsetMode::SpecFlux,
Self::WINDOW_SIZE,
Self::HOP_SIZE,
sample_rate,
)
.map_err(|e| {
AnalysisError::AnalysisError(format!("error while loading aubio tempo object: {e}"))
})?,
bpms: Vec::new(),
})
}
#[allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
pub fn do_(&mut self, chunk: &[f32]) -> AnalysisResult<()> {
let result = self.aubio_obj.do_result(chunk).map_err(|e| {
AnalysisError::AnalysisError(format!("aubio error while computing tempo {e}"))
})?;
if result > 0. {
self.bpms.push(self.aubio_obj.get_bpm());
}
Ok(())
}
#[allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
pub fn get_value(&mut self) -> Feature {
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: Feature = 206.;
const MIN_VALUE: Feature = 0.;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
decoder::{Decoder as DecoderTrait, MecompDecoder as Decoder},
SAMPLE_RATE,
};
use std::path::Path;
#[test]
fn test_tempo_real() {
let song = Decoder::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap();
let mut tempo_desc = BPMDesc::new(SAMPLE_RATE).unwrap();
for chunk in song.samples.chunks_exact(BPMDesc::HOP_SIZE) {
tempo_desc.do_(chunk).unwrap();
}
assert!(
0.01 > (0.378_605 - tempo_desc.get_value()).abs(),
"{} !~= 0.378605",
tempo_desc.get_value()
);
}
#[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()
.copied()
.collect::<Vec<f32>>();
for chunk in chunks.chunks_exact(BPMDesc::HOP_SIZE) {
tempo_desc.do_(chunk).unwrap();
}
assert!(
0.01 > (-0.416_853 - tempo_desc.get_value()).abs(),
"{} !~= -0.416853",
tempo_desc.get_value()
);
}
#[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()
.copied()
.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(),
"{} !~= 0.86",
tempo_desc.get_value()
);
}
}