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