bliss_audio/
temporal.rs

1//! Temporal feature extraction module.
2//!
3//! Contains functions to extract & summarize the temporal aspects
4//! of a given Song.
5
6use 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/**
16 * Beats per minutes ([BPM](https://en.wikipedia.org/wiki/Tempo#Measurement))
17 * detection object.
18 *
19 * It indicates the (subjective) "speed" of a music piece. The higher the BPM,
20 * the "quicker" the song will feel.
21 *
22 * It uses `SpecFlux`, a phase-deviation onset detection function to perform
23 * onset detection; it proved to be the best for finding out the BPM of a panel
24 * of songs I had, but it could very well be replaced by something better in the
25 * future.
26 *
27 * Ranges from 0 (theoretically...) to 206 BPM. (Even though aubio apparently
28 * has trouble to identify tempo > 190 BPM - did not investigate too much)
29 *
30 */
31#[doc(hidden)]
32pub struct BPMDesc {
33    aubio_obj: Tempo,
34    bpms: Vec<f32>,
35}
36
37// TODO>1.0 use the confidence value to discard this descriptor if confidence
38// is too low.
39impl 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    /**
70     * Compute score related to tempo.
71     * Right now, basically returns the song's BPM.
72     *
73     * - `song` Song to compute score from
74     */
75    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    // See aubio/src/tempo/beattracking.c:387
90    // Should really be 413, needs testing
91    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        // This gives one beat every second, so 60 BPM
132        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        // -0.41 is 60 BPM normalized
144        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        // The highest value I could obtain was with these params, even though
156        // apparently the higher bound is 206 BPM, but here I found ~189 BPM.
157        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        // 0.86 is 192BPM normalized
168        assert!(0.01 > (0.86 - tempo_desc.get_value()).abs());
169    }
170}