1use crate::utils::Normalize;
7use crate::{BlissError, BlissResult};
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
15#[doc(hidden)]
32pub struct BPMDesc {
33 aubio_obj: Tempo,
34 bpms: Vec<f32>,
35}
36
37impl BPMDesc {
40 pub const WINDOW_SIZE: usize = 512;
41 pub const HOP_SIZE: usize = BPMDesc::WINDOW_SIZE / 2;
42
43 pub fn new(sample_rate: u32) -> BlissResult<Self> {
44 Ok(BPMDesc {
45 aubio_obj: Tempo::new(
46 OnsetMode::SpecFlux,
47 BPMDesc::WINDOW_SIZE,
48 BPMDesc::HOP_SIZE,
49 sample_rate,
50 )
51 .map_err(|e| {
52 BlissError::AnalysisError(format!("error while loading aubio tempo object: {e}"))
53 })?,
54 bpms: Vec::new(),
55 })
56 }
57
58 pub fn do_(&mut self, chunk: &[f32]) -> BlissResult<()> {
59 let result = self.aubio_obj.do_result(chunk).map_err(|e| {
60 BlissError::AnalysisError(format!("aubio error while computing tempo {e}"))
61 })?;
62
63 if result > 0. {
64 self.bpms.push(self.aubio_obj.get_bpm());
65 }
66 Ok(())
67 }
68
69 pub fn get_value(&mut self) -> f32 {
76 if self.bpms.is_empty() {
77 warn!("Set tempo value to zero because no beats were found.");
78 return -1.;
79 }
80 let median = arr1(&self.bpms)
81 .mapv(n32)
82 .quantile_mut(n64(0.5), &Midpoint)
83 .unwrap();
84 self.normalize(median.into())
85 }
86}
87
88impl Normalize for BPMDesc {
89 const MAX_VALUE: f32 = 206.;
92 const MIN_VALUE: f32 = 0.;
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 #[cfg(feature = "ffmpeg")]
99 use crate::song::decoder::ffmpeg::FFmpegDecoder as Decoder;
100 #[cfg(feature = "ffmpeg")]
101 use crate::song::decoder::Decoder as DecoderTrait;
102 #[cfg(feature = "ffmpeg")]
103 use crate::SAMPLE_RATE;
104 #[cfg(feature = "ffmpeg")]
105 use std::path::Path;
106
107 #[test]
108 #[cfg(feature = "ffmpeg")]
109 fn test_tempo_real() {
110 let song = Decoder::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap();
111 let mut tempo_desc = BPMDesc::new(SAMPLE_RATE).unwrap();
112 for chunk in song.sample_array.chunks_exact(BPMDesc::HOP_SIZE) {
113 tempo_desc.do_(&chunk).unwrap();
114 }
115 assert!(0.01 > (0.378605 - tempo_desc.get_value()).abs());
116 }
117
118 #[test]
119 fn test_tempo_error_creating_aubio_tempo() {
120 assert_eq!(
121 BPMDesc::new(0).err().unwrap(),
122 BlissError::AnalysisError(String::from(
123 "error while loading aubio tempo object: creation error"
124 ))
125 )
126 }
127
128 #[test]
129 fn test_tempo_artificial() {
130 let mut tempo_desc = BPMDesc::new(22050).unwrap();
131 let mut one_chunk = vec![0.; 22000];
133 one_chunk.append(&mut vec![1.; 100]);
134 let chunks = std::iter::repeat(one_chunk.iter())
135 .take(100)
136 .flatten()
137 .cloned()
138 .collect::<Vec<f32>>();
139 for chunk in chunks.chunks_exact(BPMDesc::HOP_SIZE) {
140 tempo_desc.do_(&chunk).unwrap();
141 }
142
143 assert!(0.01 > (-0.416853 - tempo_desc.get_value()).abs());
145 }
146
147 #[test]
148 fn test_tempo_boundaries() {
149 let mut tempo_desc = BPMDesc::new(10).unwrap();
150 let silence_chunk = vec![0.; 1024];
151 tempo_desc.do_(&silence_chunk).unwrap();
152 assert_eq!(-1., tempo_desc.get_value());
153
154 let mut tempo_desc = BPMDesc::new(22050).unwrap();
155 let mut one_chunk = vec![0.; 6989];
158 one_chunk.append(&mut vec![1.; 20]);
159 let chunks = std::iter::repeat(one_chunk.iter())
160 .take(500)
161 .flatten()
162 .cloned()
163 .collect::<Vec<f32>>();
164 for chunk in chunks.chunks_exact(BPMDesc::HOP_SIZE) {
165 tempo_desc.do_(&chunk).unwrap();
166 }
167 assert!(0.01 > (0.86 - tempo_desc.get_value()).abs());
169 }
170}