mecomp_analysis/
temporal.rs1use crate::Feature;
7
8use super::errors::{AnalysisError, AnalysisResult};
9use super::utils::Normalize;
10use bliss_audio_aubio_rs::{OnsetMode, Tempo};
11use likely_stable::{LikelyResult, unlikely};
12use log::warn;
13use ndarray::arr1;
14use ndarray_stats::Quantile1dExt;
15use ndarray_stats::interpolate::Midpoint;
16use noisy_float::prelude::*;
17
18pub struct BPMDesc {
35 aubio_obj: Tempo,
36 bpms: Vec<f32>,
37}
38
39impl BPMDesc {
42 pub const WINDOW_SIZE: usize = 512;
43 pub const HOP_SIZE: usize = Self::WINDOW_SIZE / 2;
44
45 #[allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
46 #[inline]
47 pub fn new(sample_rate: u32) -> AnalysisResult<Self> {
48 Ok(Self {
49 aubio_obj: Tempo::new(
50 OnsetMode::SpecFlux,
51 Self::WINDOW_SIZE,
52 Self::HOP_SIZE,
53 sample_rate,
54 )
55 .map_err_unlikely(|e| {
56 AnalysisError::AnalysisError(format!("error while loading aubio tempo object: {e}"))
57 })?,
58 bpms: Vec::new(),
59 })
60 }
61
62 #[allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
63 #[inline]
64 pub fn do_(&mut self, chunk: &[f32]) -> AnalysisResult<()> {
65 let result = self.aubio_obj.do_result(chunk).map_err_unlikely(|e| {
66 AnalysisError::AnalysisError(format!("aubio error while computing tempo {e}"))
67 })?;
68
69 if result > 0. {
70 self.bpms.push(self.aubio_obj.get_bpm());
71 }
72 Ok(())
73 }
74
75 #[allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
82 #[inline]
83 pub fn get_value(&mut self) -> Feature {
84 if unlikely(self.bpms.is_empty()) {
85 warn!("Set tempo value to zero because no beats were found.");
86 return -1.;
87 }
88 let median = arr1(&self.bpms)
89 .mapv(n32)
90 .quantile_mut(n64(0.5), &Midpoint)
91 .unwrap();
92 self.normalize(median.into())
93 }
94}
95
96impl Normalize for BPMDesc {
97 const MAX_VALUE: Feature = 206.;
100 const MIN_VALUE: Feature = 0.;
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use crate::{
107 SAMPLE_RATE,
108 decoder::{Decoder as DecoderTrait, MecompDecoder as Decoder},
109 };
110 use std::path::Path;
111
112 #[test]
113 fn test_tempo_real() {
114 let song = Decoder::new()
115 .unwrap()
116 .decode(Path::new("data/s16_mono_22_5kHz.flac"))
117 .unwrap();
118 let mut tempo_desc = BPMDesc::new(SAMPLE_RATE).unwrap();
119 for chunk in song.samples.chunks_exact(BPMDesc::HOP_SIZE) {
120 tempo_desc.do_(chunk).unwrap();
121 }
122 assert!(
123 0.01 > (0.378_605 - tempo_desc.get_value()).abs(),
124 "{} !~= 0.378605",
125 tempo_desc.get_value()
126 );
127 }
128
129 #[test]
130 fn test_tempo_artificial() {
131 let mut tempo_desc = BPMDesc::new(22050).unwrap();
132 let mut one_chunk = vec![0.; 22000];
134 one_chunk.append(&mut vec![1.; 100]);
135 let chunks = std::iter::repeat_n(one_chunk.iter(), 100)
136 .flatten()
137 .copied()
138 .collect::<Vec<f32>>();
139 for chunk in chunks.chunks_exact(BPMDesc::HOP_SIZE) {
140 tempo_desc.do_(chunk).unwrap();
141 }
142
143 assert!(
145 0.01 > (-0.416_853 - tempo_desc.get_value()).abs(),
146 "{} !~= -0.416853",
147 tempo_desc.get_value()
148 );
149 }
150
151 #[test]
152 fn test_tempo_boundaries() {
153 let mut tempo_desc = BPMDesc::new(10).unwrap();
154 let silence_chunk = vec![0.; 1024];
155 tempo_desc.do_(&silence_chunk).unwrap();
156 let value = tempo_desc.get_value();
157 assert!(f64::EPSILON > (-1. - value).abs(), "{value} !~= -1");
158
159 let mut tempo_desc = BPMDesc::new(22050).unwrap();
160 let mut one_chunk = vec![0.; 6989];
163 one_chunk.append(&mut vec![1.; 20]);
164 let chunks = std::iter::repeat_n(one_chunk.iter(), 500)
165 .flatten()
166 .copied()
167 .collect::<Vec<f32>>();
168 for chunk in chunks.chunks_exact(BPMDesc::HOP_SIZE) {
169 tempo_desc.do_(chunk).unwrap();
170 }
171 assert!(
173 0.01 > (0.86 - tempo_desc.get_value()).abs(),
174 "{} !~= 0.86",
175 tempo_desc.get_value()
176 );
177 }
178}