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