1use crate::utils::Normalize;
7use crate::BlissError;
8use bliss_audio_aubio_rs::{OnsetMode, Tempo};
9use log::warn;
10use ndarray::arr1;
11use ndarray_stats::interpolate::Midpoint;
12use ndarray_stats::Quantile1dExt;
13use noisy_float::prelude::*;
14
15pub(crate) struct BPMDesc {
32 aubio_obj: Tempo,
33 bpms: Vec<f32>,
34}
35
36impl BPMDesc {
39 pub const WINDOW_SIZE: usize = 512;
40 pub const HOP_SIZE: usize = BPMDesc::WINDOW_SIZE / 2;
41
42 pub fn new(sample_rate: u32) -> Result<Self, BlissError> {
43 Ok(BPMDesc {
44 aubio_obj: Tempo::new(
45 OnsetMode::SpecFlux,
46 BPMDesc::WINDOW_SIZE,
47 BPMDesc::HOP_SIZE,
48 sample_rate,
49 )
50 .map_err(|e| {
51 BlissError::AnalysisError(format!(
52 "error while loading aubio tempo object: {}",
53 e.to_string()
54 ))
55 })?,
56 bpms: Vec::new(),
57 })
58 }
59
60 pub fn do_(&mut self, chunk: &[f32]) -> Result<(), BlissError> {
61 let result = self.aubio_obj.do_result(chunk).map_err(|e| {
62 BlissError::AnalysisError(format!(
63 "aubio error while computing tempo {}",
64 e.to_string()
65 ))
66 })?;
67
68 if result > 0. {
69 self.bpms.push(self.aubio_obj.get_bpm());
70 }
71 Ok(())
72 }
73
74 pub fn get_value(&mut self) -> f32 {
81 if self.bpms.is_empty() {
82 warn!("Set tempo value to zero because no beats were found.");
83 return -1.;
84 }
85 let median = arr1(&self.bpms)
86 .mapv(n32)
87 .quantile_mut(n64(0.5), &Midpoint)
88 .unwrap();
89 self.normalize(median.into())
90 }
91}
92
93impl Normalize for BPMDesc {
94 const MAX_VALUE: f32 = 206.;
97 const MIN_VALUE: f32 = 0.;
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::{Song, SAMPLE_RATE};
104
105 #[test]
106 fn test_tempo_real() {
107 let song = Song::decode("data/s16_mono_22_5kHz.flac").unwrap();
108 let mut tempo_desc = BPMDesc::new(SAMPLE_RATE).unwrap();
109 for chunk in song.sample_array.chunks_exact(BPMDesc::HOP_SIZE) {
110 tempo_desc.do_(&chunk).unwrap();
111 }
112 assert!(0.01 > (0.378605 - tempo_desc.get_value()).abs());
113 }
114
115 #[test]
116 fn test_tempo_artificial() {
117 let mut tempo_desc = BPMDesc::new(22050).unwrap();
118 let mut one_chunk = vec![0.; 22000];
120 one_chunk.append(&mut vec![1.; 100]);
121 let chunks = std::iter::repeat(one_chunk.iter())
122 .take(100)
123 .flatten()
124 .cloned()
125 .collect::<Vec<f32>>();
126 for chunk in chunks.chunks_exact(BPMDesc::HOP_SIZE) {
127 tempo_desc.do_(&chunk).unwrap();
128 }
129
130 assert!(0.01 > (-0.416853 - tempo_desc.get_value()).abs());
132 }
133
134 #[test]
135 fn test_tempo_boundaries() {
136 let mut tempo_desc = BPMDesc::new(10).unwrap();
137 let silence_chunk = vec![0.; 1024];
138 tempo_desc.do_(&silence_chunk).unwrap();
139 assert_eq!(-1., tempo_desc.get_value());
140
141 let mut tempo_desc = BPMDesc::new(22050).unwrap();
142 let mut one_chunk = vec![0.; 6989];
145 one_chunk.append(&mut vec![1.; 20]);
146 let chunks = std::iter::repeat(one_chunk.iter())
147 .take(500)
148 .flatten()
149 .cloned()
150 .collect::<Vec<f32>>();
151 for chunk in chunks.chunks_exact(BPMDesc::HOP_SIZE) {
152 tempo_desc.do_(&chunk).unwrap();
153 }
154 assert!(0.01 > (0.86 - tempo_desc.get_value()).abs());
156 }
157}