1use bliss_audio_aubio_rs::vec::CVec;
8use bliss_audio_aubio_rs::{PVoc, SpecDesc, SpecShape, bin_to_freq};
9use ndarray::{Axis, arr1};
10
11use crate::Feature;
12
13use super::SAMPLE_RATE;
14use super::errors::{AnalysisError, AnalysisResult};
15use super::utils::{Normalize, geometric_mean, mean, number_crossings};
16
17pub struct SpectralDesc {
31 phase_vocoder: PVoc,
32 sample_rate: u32,
33
34 centroid_aubio_desc: SpecDesc,
35 rolloff_aubio_desc: SpecDesc,
36 values_centroid: Vec<f32>,
37 values_rolloff: Vec<f32>,
38 values_flatness: Vec<f32>,
39}
40
41impl SpectralDesc {
42 pub const WINDOW_SIZE: usize = 512;
43 pub const HOP_SIZE: usize = Self::WINDOW_SIZE / 4;
44
45 #[inline]
60 pub fn get_centroid(&mut self) -> Vec<Feature> {
61 vec![
62 self.normalize(Feature::from(mean(&self.values_centroid))),
63 self.normalize(Feature::from(
64 arr1(&self.values_centroid)
65 .std_axis(Axis(0), 0.)
66 .into_scalar(),
67 )),
68 ]
69 }
70
71 #[inline]
85 pub fn get_rolloff(&mut self) -> Vec<Feature> {
86 vec![
87 self.normalize(Feature::from(mean(&self.values_rolloff))),
88 self.normalize(Feature::from(
89 arr1(&self.values_rolloff)
90 .std_axis(Axis(0), 0.)
91 .into_scalar(),
92 )),
93 ]
94 }
95
96 #[inline]
113 pub fn get_flatness(&mut self) -> Vec<Feature> {
114 let max_value = 1.;
115 let min_value = 0.;
116 vec![
119 2. * (Feature::from(mean(&self.values_flatness)) - min_value) / (max_value - min_value)
120 - 1.,
121 2. * (Feature::from(
122 arr1(&self.values_flatness)
123 .std_axis(Axis(0), 0.)
124 .into_scalar(),
125 ) - min_value)
126 / (max_value - min_value)
127 - 1.,
128 ]
129 }
130
131 #[inline]
135 pub fn new(sample_rate: u32) -> AnalysisResult<Self> {
136 Ok(Self {
137 centroid_aubio_desc: SpecDesc::new(SpecShape::Centroid, Self::WINDOW_SIZE).map_err(
138 |e| {
139 AnalysisError::AnalysisError(format!(
140 "error while loading aubio centroid object: {e}",
141 ))
142 },
143 )?,
144 rolloff_aubio_desc: SpecDesc::new(SpecShape::Rolloff, Self::WINDOW_SIZE).map_err(
145 |e| {
146 AnalysisError::AnalysisError(format!(
147 "error while loading aubio rolloff object: {e}",
148 ))
149 },
150 )?,
151 phase_vocoder: PVoc::new(Self::WINDOW_SIZE, Self::HOP_SIZE).map_err(|e| {
152 AnalysisError::AnalysisError(format!("error while loading aubio pvoc object: {e}",))
153 })?,
154 values_centroid: Vec::new(),
155 values_rolloff: Vec::new(),
156 values_flatness: Vec::new(),
157 sample_rate,
158 })
159 }
160
161 #[allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
169 #[allow(clippy::missing_inline_in_public_items)]
170 pub fn do_(&mut self, chunk: &[f32]) -> AnalysisResult<()> {
171 let mut fftgrain: Vec<f32> = vec![0.0; Self::WINDOW_SIZE];
172 self.phase_vocoder
173 .do_(chunk, fftgrain.as_mut_slice())
174 .map_err(|e| {
175 AnalysisError::AnalysisError(format!("error while processing aubio pv object: {e}"))
176 })?;
177
178 let bin = self
179 .centroid_aubio_desc
180 .do_result(fftgrain.as_slice())
181 .map_err(|e| {
182 AnalysisError::AnalysisError(format!(
183 "error while processing aubio centroid object: {e}",
184 ))
185 })?;
186
187 #[allow(clippy::cast_precision_loss)]
188 let freq = bin_to_freq(bin, self.sample_rate as f32, Self::WINDOW_SIZE as f32);
189 self.values_centroid.push(freq);
190
191 let mut bin = self
192 .rolloff_aubio_desc
193 .do_result(fftgrain.as_slice())
194 .unwrap();
195
196 #[allow(clippy::cast_precision_loss)]
198 if bin > Self::WINDOW_SIZE as f32 / 2. {
199 bin = Self::WINDOW_SIZE as f32 / 2.;
200 }
201
202 #[allow(clippy::cast_precision_loss)]
203 let freq = bin_to_freq(bin, self.sample_rate as f32, Self::WINDOW_SIZE as f32);
204 self.values_rolloff.push(freq);
205
206 let cvec: CVec = fftgrain.as_slice().into();
207 let geo_mean = geometric_mean(cvec.norm());
208 if geo_mean == 0.0 {
209 self.values_flatness.push(0.0);
210 return Ok(());
211 }
212 let flatness = geo_mean / mean(cvec.norm());
213 self.values_flatness.push(flatness);
214 Ok(())
215 }
216}
217
218impl Normalize for SpectralDesc {
219 #[allow(clippy::cast_precision_loss)]
220 const MAX_VALUE: Feature = SAMPLE_RATE as Feature / 2.;
221 const MIN_VALUE: Feature = 0.;
222}
223
224#[derive(Default, Clone)]
237pub struct ZeroCrossingRateDesc {
238 crossings_sum: u32,
239 samples_checked: usize,
240}
241
242impl ZeroCrossingRateDesc {
243 #[must_use]
244 #[inline]
245 pub fn new(_sample_rate: u32) -> Self {
246 Self::default()
247 }
248
249 #[inline]
251 pub fn do_(&mut self, chunk: &[f32]) {
252 self.crossings_sum += number_crossings(chunk);
253 self.samples_checked += chunk.len();
254 }
255
256 #[allow(clippy::cast_precision_loss)]
259 #[inline]
260 pub fn get_value(&mut self) -> Feature {
261 self.normalize(Feature::from(self.crossings_sum) / self.samples_checked as Feature)
262 }
263}
264
265impl Normalize for ZeroCrossingRateDesc {
266 const MAX_VALUE: Feature = 1.;
267 const MIN_VALUE: Feature = 0.;
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273 use crate::decoder::{Decoder as DecoderTrait, MecompDecoder as Decoder};
274 use std::path::Path;
275
276 #[test]
277 fn test_zcr_boundaries() {
278 let mut zcr_desc = ZeroCrossingRateDesc::default();
279 let chunk = vec![0.; 1024];
280 zcr_desc.do_(&chunk);
281 let value = zcr_desc.get_value();
282 assert!(f64::EPSILON > (-1. - value).abs(), "{value} !~= -1");
283
284 let one_chunk = [-1., 1.];
285 let chunks = std::iter::repeat(one_chunk.iter())
286 .take(512)
287 .flatten()
288 .copied()
289 .collect::<Vec<f32>>();
290 let mut zcr_desc = ZeroCrossingRateDesc::default();
291 zcr_desc.do_(&chunks);
292 let value = zcr_desc.get_value();
293 assert!(0.001 > (0.998_046_9 - value).abs(), "{value} !~= 0.9980469");
294 }
295
296 #[test]
297 fn test_zcr() {
298 let song = Decoder::new()
299 .unwrap()
300 .decode(Path::new("data/s16_mono_22_5kHz.flac"))
301 .unwrap();
302 let mut zcr_desc = ZeroCrossingRateDesc::default();
303 for chunk in song.samples.chunks_exact(SpectralDesc::HOP_SIZE) {
304 zcr_desc.do_(chunk);
305 }
306 let value = zcr_desc.get_value();
307 assert!(0.001 > (-0.85036 - value).abs(), "{value} !~= -0.85036");
308 }
309
310 #[test]
311 fn test_spectral_flatness_boundaries() {
312 let mut spectral_desc = SpectralDesc::new(10).unwrap();
313 let chunk = vec![0.; 1024];
314
315 let expected_values = [-1., -1.];
316 spectral_desc.do_(&chunk).unwrap();
317 for (expected, actual) in expected_values
318 .iter()
319 .zip(spectral_desc.get_flatness().iter())
320 {
321 assert!(
322 0.000_000_1 > (expected - actual).abs(),
323 "{expected} !~= {actual}"
324 );
325 }
326
327 let song = Decoder::new()
328 .unwrap()
329 .decode(Path::new("data/white_noise.mp3"))
330 .unwrap();
331 let mut spectral_desc = SpectralDesc::new(22050).unwrap();
332 for chunk in song.samples.chunks_exact(SpectralDesc::HOP_SIZE) {
333 spectral_desc.do_(chunk).unwrap();
334 }
335 println!("{:?}", spectral_desc.get_flatness());
336 let expected_values = [0.578_530_3, -0.942_630_8];
338 for (expected, actual) in expected_values
339 .iter()
340 .zip(spectral_desc.get_flatness().iter())
341 {
342 let relative_error = (expected - actual).abs() / expected.abs();
345 assert!(
346 relative_error < 0.078,
347 "relative error: {relative_error}, expected: {expected}, actual: {actual}"
348 );
349 }
350 }
351
352 #[test]
353 fn test_spectral_flatness() {
354 let song = Decoder::new()
355 .unwrap()
356 .decode(Path::new("data/s16_mono_22_5kHz.flac"))
357 .unwrap();
358 let mut spectral_desc = SpectralDesc::new(SAMPLE_RATE).unwrap();
359 for chunk in song.samples.chunks_exact(SpectralDesc::HOP_SIZE) {
360 spectral_desc.do_(chunk).unwrap();
361 }
362 let expected_values = [-0.776_100_75, -0.814_817_9];
365 for (expected, actual) in expected_values
366 .iter()
367 .zip(spectral_desc.get_flatness().iter())
368 {
369 assert!(0.01 > (expected - actual).abs(), "{expected} !~= {actual}");
370 }
371 }
372
373 #[test]
374 fn test_spectral_roll_off_boundaries() {
375 let mut spectral_desc = SpectralDesc::new(10).unwrap();
376 let chunk = vec![0.; 512];
377
378 let expected_values = [-1., -1.];
379 spectral_desc.do_(&chunk).unwrap();
380 for (expected, actual) in expected_values
381 .iter()
382 .zip(spectral_desc.get_rolloff().iter())
383 {
384 assert!(
385 0.000_000_1 > (expected - actual).abs(),
386 "{expected} !~= {actual}"
387 );
388 }
389
390 let song = Decoder::new()
391 .unwrap()
392 .decode(Path::new("data/tone_11080Hz.flac"))
393 .unwrap();
394 let mut spectral_desc = SpectralDesc::new(SAMPLE_RATE).unwrap();
395 for chunk in song.samples.chunks_exact(SpectralDesc::HOP_SIZE) {
396 spectral_desc.do_(chunk).unwrap();
397 }
398 let expected_values = [0.996_768_1, -0.996_151_75];
399 for (expected, actual) in expected_values
400 .iter()
401 .zip(spectral_desc.get_rolloff().iter())
402 {
403 assert!(
404 0.0001 > (expected - actual).abs(),
405 "{expected} !~= {actual}"
406 );
407 }
408 }
409
410 #[test]
411 fn test_spectral_roll_off() {
412 let song = Decoder::new()
413 .unwrap()
414 .decode(Path::new("data/s16_mono_22_5kHz.flac"))
415 .unwrap();
416 let mut spectral_desc = SpectralDesc::new(SAMPLE_RATE).unwrap();
417 for chunk in song.samples.chunks_exact(SpectralDesc::HOP_SIZE) {
418 spectral_desc.do_(chunk).unwrap();
419 }
420 let expected_values = [-0.632_648_6, -0.726_093_3];
421 for (expected, actual) in expected_values
424 .iter()
425 .zip(spectral_desc.get_rolloff().iter())
426 {
427 assert!(0.01 > (expected - actual).abs(), "{expected} !~= {actual}");
428 }
429 }
430
431 #[test]
432 fn test_spectral_centroid() {
433 let song = Decoder::new()
434 .unwrap()
435 .decode(Path::new("data/s16_mono_22_5kHz.flac"))
436 .unwrap();
437 let mut spectral_desc = SpectralDesc::new(SAMPLE_RATE).unwrap();
438 for chunk in song.samples.chunks_exact(SpectralDesc::HOP_SIZE) {
439 spectral_desc.do_(chunk).unwrap();
440 }
441 let expected_values = [-0.75483, -0.879_168_87];
444 for (expected, actual) in expected_values
445 .iter()
446 .zip(spectral_desc.get_centroid().iter())
447 {
448 assert!(
449 0.0001 > (expected - actual).abs(),
450 "{expected} !~= {actual}"
451 );
452 }
453 }
454
455 #[test]
456 fn test_spectral_centroid_boundaries() {
457 let mut spectral_desc = SpectralDesc::new(10).unwrap();
458 let chunk = vec![0.; 512];
459
460 spectral_desc.do_(&chunk).unwrap();
461 let expected_values = [-1., -1.];
462 for (expected, actual) in expected_values
463 .iter()
464 .zip(spectral_desc.get_centroid().iter())
465 {
466 assert!(
467 0.000_000_1 > (expected - actual).abs(),
468 "{expected} !~= {actual}"
469 );
470 }
471 let song = Decoder::new()
472 .unwrap()
473 .decode(Path::new("data/tone_11080Hz.flac"))
474 .unwrap();
475 let mut spectral_desc = SpectralDesc::new(SAMPLE_RATE).unwrap();
476 for chunk in song.samples.chunks_exact(SpectralDesc::HOP_SIZE) {
477 spectral_desc.do_(chunk).unwrap();
478 }
479 let expected_values = [0.97266, -0.960_992_6];
480 for (expected, actual) in expected_values
481 .iter()
482 .zip(spectral_desc.get_centroid().iter())
483 {
484 let relative_error = (expected - actual).abs() / expected.abs();
487 assert!(
488 relative_error < 0.039,
489 "relative error: {relative_error}, expected: {expected}, actual: {actual}"
490 );
491 }
492 }
493}