use crate::aubio::Tempo;
use crate::utils::Normalize;
use crate::BlissResult;
use log::warn;
use ndarray::arr1;
use ndarray_stats::interpolate::Midpoint;
use ndarray_stats::Quantile1dExt;
use noisy_float::prelude::*;
#[doc(hidden)]
pub struct BPMDesc {
tempo: 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) -> BlissResult<Self> {
Ok(BPMDesc {
tempo: Tempo::new(BPMDesc::WINDOW_SIZE, BPMDesc::HOP_SIZE, sample_rate)?,
bpms: Vec::new(),
})
}
pub fn do_(&mut self, chunk: &[f32]) -> BlissResult<()> {
let result = self.tempo.do_(chunk)?;
if result > 0.0 {
let bpm = self.tempo.get_bpm();
self.bpms.push(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::*;
#[cfg(feature = "ffmpeg")]
use crate::song::decoder::ffmpeg::FFmpegDecoder as Decoder;
#[cfg(feature = "ffmpeg")]
use crate::song::decoder::Decoder as DecoderTrait;
use crate::BlissError;
#[cfg(feature = "ffmpeg")]
use crate::SAMPLE_RATE;
#[cfg(feature = "ffmpeg")]
use std::path::Path;
#[test]
#[cfg(feature = "ffmpeg")]
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.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_error_creating_aubio_tempo() {
assert_eq!(
BPMDesc::new(0).err().unwrap(),
BlissError::AnalysisError(String::from(
"error while loading aubio tempo object: creation error"
))
)
}
#[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());
}
}