153 lines
4.8 KiB
Rust
153 lines
4.8 KiB
Rust
//! Temporal feature extraction module.
|
|
//!
|
|
//! Contains functions to extract & summarize the temporal aspects
|
|
//! of a given Song.
|
|
|
|
use crate::utils::Normalize;
|
|
use crate::bliss_lib::{BlissError, BlissResult};
|
|
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::*;
|
|
|
|
/**
|
|
* Beats per minutes ([BPM](https://en.wikipedia.org/wiki/Tempo#Measurement))
|
|
* detection object.
|
|
*
|
|
* It indicates the (subjective) "speed" of a music piece. The higher the BPM,
|
|
* the "quicker" the song will feel.
|
|
*
|
|
* It uses `SpecFlux`, a phase-deviation onset detection function to perform
|
|
* onset detection; it proved to be the best for finding out the BPM of a panel
|
|
* of songs I had, but it could very well be replaced by something better in the
|
|
* future.
|
|
*
|
|
* Ranges from 0 (theoretically...) to 206 BPM. (Even though aubio apparently
|
|
* has trouble to identify tempo > 190 BPM - did not investigate too much)
|
|
*
|
|
*/
|
|
pub(crate) struct BPMDesc {
|
|
aubio_obj: Tempo,
|
|
bpms: Vec<f32>,
|
|
}
|
|
|
|
// TODO>1.0 use the confidence value to discard this descriptor if confidence
|
|
// is too low.
|
|
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 {
|
|
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}"))
|
|
})?,
|
|
bpms: Vec::new(),
|
|
})
|
|
}
|
|
|
|
pub fn do_(&mut self, chunk: &[f32]) -> BlissResult<()> {
|
|
let result = self.aubio_obj.do_result(chunk).map_err(|e| {
|
|
BlissError::AnalysisError(format!("aubio error while computing tempo {e}"))
|
|
})?;
|
|
|
|
if result > 0. {
|
|
self.bpms.push(self.aubio_obj.get_bpm());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/**
|
|
* Compute score related to tempo.
|
|
* Right now, basically returns the song's BPM.
|
|
*
|
|
* - `song` Song to compute score from
|
|
*/
|
|
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 {
|
|
// See aubio/src/tempo/beattracking.c:387
|
|
// Should really be 413, needs testing
|
|
const MAX_VALUE: f32 = 206.;
|
|
const MIN_VALUE: f32 = 0.;
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::bliss_lib::{Song, SAMPLE_RATE};
|
|
use std::path::Path;
|
|
|
|
#[test]
|
|
fn test_tempo_real() {
|
|
let song = Song::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_artificial() {
|
|
let mut tempo_desc = BPMDesc::new(22050).unwrap();
|
|
// This gives one beat every second, so 60 BPM
|
|
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();
|
|
}
|
|
|
|
// -0.41 is 60 BPM normalized
|
|
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();
|
|
// The highest value I could obtain was with these params, even though
|
|
// apparently the higher bound is 206 BPM, but here I found ~189 BPM.
|
|
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();
|
|
}
|
|
// 0.86 is 192BPM normalized
|
|
assert!(0.01 > (0.86 - tempo_desc.get_value()).abs());
|
|
}
|
|
}
|