mecomp_analysis/
utils.rs

1use likely_stable::{if_likely, unlikely};
2use log::warn;
3use ndarray::{Array, Array1, Array2, arr1, s};
4use rustfft::FftPlanner;
5use rustfft::num_complex::Complex;
6use std::f32::consts::PI;
7
8use crate::Feature;
9
10#[must_use]
11#[inline]
12pub fn reflect_pad(array: &[f32], pad: usize) -> Vec<f32> {
13    debug_assert!(pad < array.len(), "Padding is too large");
14    let prefix = array[1..=pad].iter().rev().copied();
15    let suffix = array[(array.len() - 2) - pad + 1..array.len() - 1]
16        .iter()
17        .rev()
18        .copied();
19    let mut output = Vec::with_capacity(prefix.len() + array.len() + suffix.len());
20
21    output.extend(prefix);
22    output.extend(array);
23    output.extend(suffix);
24    output
25}
26
27/// Compute the Short-Time Fourier Transform of a signal
28#[must_use]
29#[allow(clippy::missing_inline_in_public_items)]
30pub fn stft(signal: &[f32], window_length: usize, hop_length: usize) -> Array2<f64> {
31    debug_assert!(
32        window_length.is_multiple_of(2),
33        "Window length must be even"
34    );
35    debug_assert!(window_length < signal.len(), "Signal is too short");
36    debug_assert!(hop_length < window_length, "Hop length is too large");
37    let half_window_length = window_length / 2;
38    // Take advantage of row-major order to have contiguous window for the
39    // `assign`, reversing the axes to have the expected shape at the end only.
40    let mut stft = Array2::zeros((signal.len().div_ceil(hop_length), half_window_length + 1));
41    let signal = reflect_pad(signal, half_window_length);
42
43    // Periodic, so window_size + 1
44    #[allow(clippy::cast_precision_loss)]
45    let mut hann_window = -0.5
46        * Array::from_shape_fn(window_length + 1, |n| {
47            2. * n as f32 * PI / (window_length as f32)
48        })
49        .cos()
50        + 0.5;
51    hann_window = hann_window.slice_move(s![0..window_length]);
52    let mut planner = FftPlanner::new();
53    let fft = planner.plan_fft_forward(window_length);
54
55    #[allow(unused, reason = "it's not unused, but macro confuses poor clippy")]
56    for (window, mut stft_col) in signal
57        .windows(window_length)
58        .step_by(hop_length)
59        .zip(stft.rows_mut())
60    {
61        let mut signal = (arr1(window) * &hann_window).mapv(|x| Complex::new(x, 0.));
62
63        if_likely! {let Some(s) = signal.as_slice_mut() => {
64            fft.process(s);
65        } else {
66            warn!("non-contiguous slice found for stft; expect slow performances.");
67            fft.process(&mut signal.to_vec());
68        }}
69
70        stft_col.assign(
71            &signal
72                .slice(s![..=half_window_length])
73                .mapv(|x| f64::from(x.re.hypot(x.im))),
74        );
75    }
76    stft.permuted_axes((1, 0))
77}
78
79#[allow(clippy::cast_precision_loss)]
80pub(crate) fn mean(input: &[f32]) -> f32 {
81    if unlikely(input.is_empty()) {
82        return 0.;
83    }
84    input.iter().sum::<f32>() / input.len() as f32
85}
86
87pub(crate) trait Normalize {
88    const MAX_VALUE: Feature;
89    const MIN_VALUE: Feature;
90
91    fn normalize(&self, value: Feature) -> Feature {
92        2. * (value - Self::MIN_VALUE) / (Self::MAX_VALUE - Self::MIN_VALUE) - 1.
93    }
94}
95
96// Essentia algorithm
97// https://github.com/MTG/essentia/blob/master/src/algorithms/temporal/zerocrossingrate.cpp
98pub(crate) fn number_crossings(input: &[f32]) -> u32 {
99    if unlikely(input.is_empty()) {
100        return 0;
101    }
102
103    let mut crossings = 0;
104
105    let mut was_positive = input[0] > 0.;
106
107    for &sample in input {
108        let is_positive = sample > 0.;
109        if unlikely(was_positive != is_positive) {
110            crossings += 1;
111            was_positive = is_positive;
112        }
113    }
114
115    crossings
116}
117
118/// Only works for input of size 256 (or at least of size a multiple
119/// of 8), with values belonging to [0; 2^65].
120///
121/// This finely optimized geometric mean courtesy of
122/// Jacques-Henri Jourdan (<https://jhjourdan.mketjh.fr/>)
123#[must_use]
124#[allow(clippy::missing_inline_in_public_items)]
125pub fn geometric_mean(input: &[f32]) -> f32 {
126    debug_assert_eq!(input.len() % 8, 0, "Input size must be a multiple of 8");
127    if unlikely(input.is_empty()) {
128        return 0.;
129    }
130
131    let mut exponents: i32 = 0;
132    let mut mantissas: f64 = 1.;
133    for ch in input.chunks_exact(8) {
134        let mut m = (f64::from(ch[0]) * f64::from(ch[1])) * (f64::from(ch[2]) * f64::from(ch[3]));
135        m *= 3.273_390_607_896_142e150; // 2^500 : avoid underflows and denormals
136        m *= (f64::from(ch[4]) * f64::from(ch[5])) * (f64::from(ch[6]) * f64::from(ch[7]));
137        if unlikely(m == 0.) {
138            return 0.;
139        }
140        exponents += (m.to_bits() >> 52) as i32;
141        mantissas *= f64::from_bits((m.to_bits() & 0x000F_FFFF_FFFF_FFFF) | 0x3FF0_0000_0000_0000);
142    }
143
144    #[allow(clippy::cast_possible_truncation)]
145    let n = input.len() as u32;
146    #[allow(clippy::cast_possible_truncation)]
147    let result = (((mantissas.log2() + f64::from(exponents)) / f64::from(n) - (1023. + 500.) / 8.)
148        .exp2()) as f32;
149    result
150}
151
152pub(crate) fn hz_to_octs_inplace(
153    frequencies: &mut Array1<f64>,
154    tuning: f64,
155    bins_per_octave: u32,
156) -> &mut Array1<f64> {
157    let a440 = 440.0 * (tuning / f64::from(bins_per_octave)).exp2();
158
159    *frequencies /= a440 / 16.;
160    frequencies.mapv_inplace(f64::log2);
161    frequencies
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use crate::decoder::{Decoder as DecoderTrait, MecompDecoder as Decoder};
168    use ndarray::{Array, Array2, arr1};
169    use ndarray_npy::ReadNpyExt;
170    use std::{fs::File, path::Path};
171
172    #[test]
173    fn test_mean() {
174        let numbers = vec![0.0, 1.0, 2.0, 3.0, 4.0];
175        let mean = mean(&numbers);
176        assert!(f32::EPSILON > (2.0 - mean).abs(), "{mean} !~= 2.0");
177    }
178
179    #[test]
180    #[allow(clippy::too_many_lines)]
181    fn test_geometric_mean() {
182        let numbers = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0];
183        let mean = geometric_mean(&numbers);
184        assert!(f32::EPSILON > (0.0 - mean).abs(), "{mean} !~= 0.0");
185
186        let numbers = vec![4.0, 2.0, 1.0, 4.0, 2.0, 1.0, 2.0, 2.0];
187        let mean = geometric_mean(&numbers);
188        assert!(0.0001 > (2.0 - mean).abs(), "{mean} !~= 2.0");
189
190        // never going to happen, but just in case
191        let numbers = vec![256., 4.0, 2.0, 1.0, 4.0, 2.0, 1.0, 2.0];
192        let mean = geometric_mean(&numbers);
193        assert!(
194            0.0001 > (3.668_016_2 - mean).abs(),
195            "{mean} !~= {}",
196            3.668_016_172_818_685
197        );
198
199        let subnormal = vec![4.0, 2.0, 1.0, 4.0, 2.0, 1.0, 2.0, 1.0e-40_f32];
200        let mean = geometric_mean(&subnormal);
201        assert!(
202            0.0001 > (1.834_008e-5 - mean).abs(),
203            "{} !~= {}",
204            mean,
205            1.834_008_086_409_341_7e-5
206        );
207
208        let maximum = vec![2_f32.powi(65); 256];
209        let mean = geometric_mean(&maximum);
210        assert!(
211            0.0001 > (2_f32.powi(65) - mean.abs()),
212            "{} !~= {}",
213            mean,
214            2_f32.powi(65)
215        );
216
217        let input = [
218            0.024_454_033,
219            0.088_096_89,
220            0.445_543_62,
221            0.827_535_03,
222            0.158_220_93,
223            1.444_224_5,
224            3.697_138_5,
225            3.678_955_6,
226            1.598_157_2,
227            1.017_271_8,
228            1.443_609_6,
229            3.145_710_2,
230            2.764_110_8,
231            0.839_523_5,
232            0.248_968_29,
233            0.070_631_73,
234            0.355_419_4,
235            0.352_001_4,
236            0.797_365_1,
237            0.661_970_8,
238            0.784_104,
239            0.876_795_7,
240            0.287_382_66,
241            0.048_841_28,
242            0.322_706_5,
243            0.334_907_47,
244            0.185_888_75,
245            0.135_449_42,
246            0.140_177_46,
247            0.111_815_82,
248            0.152_631_61,
249            0.221_993_12,
250            0.056_798_387,
251            0.083_892_57,
252            0.070_009_65,
253            0.202_903_29,
254            0.370_717_38,
255            0.231_543_18,
256            0.023_348_59,
257            0.013_220_183,
258            0.035_887_096,
259            0.029_505_49,
260            0.090_338_57,
261            0.176_795_04,
262            0.081_421_87,
263            0.003_326_808_6,
264            0.012_269_007,
265            0.016_257_336,
266            0.027_027_424,
267            0.017_253_408,
268            0.017_230_038,
269            0.021_678_915,
270            0.018_645_158,
271            0.005_417_136,
272            0.006_650_174_5,
273            0.020_159_671,
274            0.026_623_515,
275            0.005_166_793_7,
276            0.016_880_387,
277            0.009_935_223_5,
278            0.011_079_361,
279            0.013_200_151,
280            0.005_320_572_3,
281            0.005_070_289_6,
282            0.008_130_498,
283            0.009_006_041,
284            0.003_602_499_8,
285            0.006_440_387_6,
286            0.004_656_151,
287            0.002_513_185_8,
288            0.003_084_559_7,
289            0.008_722_531,
290            0.017_871_628,
291            0.022_656_294,
292            0.017_539_924,
293            0.009_439_588_5,
294            0.003_085_72,
295            0.001_358_616_6,
296            0.002_746_787_2,
297            0.005_413_010_3,
298            0.004_140_312,
299            0.000_143_587_14,
300            0.001_371_840_8,
301            0.004_472_961,
302            0.003_769_122,
303            0.003_259_129_6,
304            0.003_637_24,
305            0.002_445_332_2,
306            0.000_590_368_93,
307            0.000_647_898_65,
308            0.001_745_297,
309            0.000_867_165_5,
310            0.002_156_236_2,
311            0.001_075_606_8,
312            0.002_009_199_5,
313            0.001_537_388_5,
314            0.000_984_620_4,
315            0.000_292_002_49,
316            0.000_921_162_4,
317            0.000_535_111_8,
318            0.001_491_276_5,
319            0.002_065_137_5,
320            0.000_661_122_26,
321            0.000_850_054_26,
322            0.001_900_590_1,
323            0.000_639_584_5,
324            0.002_262_803,
325            0.003_094_018_2,
326            0.002_089_161_7,
327            0.001_215_059,
328            0.001_311_408_4,
329            0.000_470_959,
330            0.000_665_480_7,
331            0.001_430_32,
332            0.001_791_889_3,
333            0.000_863_200_75,
334            0.000_560_445_5,
335            0.000_828_417_54,
336            0.000_669_453_9,
337            0.000_822_765,
338            0.000_616_575_8,
339            0.001_189_319,
340            0.000_730_024_5,
341            0.000_623_748_1,
342            0.001_207_644_4,
343            0.001_474_674_2,
344            0.002_033_916,
345            0.001_500_169_9,
346            0.000_520_51,
347            0.000_445_643_32,
348            0.000_558_462_75,
349            0.000_897_786_64,
350            0.000_805_247_05,
351            0.000_726_536_44,
352            0.000_673_052_6,
353            0.000_994_064_5,
354            0.001_109_393_7,
355            0.001_295_099_7,
356            0.000_982_682_2,
357            0.000_876_651_8,
358            0.001_654_928_7,
359            0.000_929_064_35,
360            0.000_291_306_23,
361            0.000_250_490_47,
362            0.000_228_488_02,
363            0.000_269_673_15,
364            0.000_237_375_09,
365            0.000_969_406_1,
366            0.001_063_811_8,
367            0.000_793_428_86,
368            0.000_590_835_06,
369            0.000_476_389_9,
370            0.000_951_664_1,
371            0.000_692_231_46,
372            0.000_557_113_7,
373            0.000_851_769_7,
374            0.001_071_027_7,
375            0.000_610_243_9,
376            0.000_746_876_23,
377            0.000_849_898_44,
378            0.000_495_806_2,
379            0.000_526_994,
380            0.000_215_249_22,
381            0.000_096_684_314,
382            0.000_654_554_4,
383            0.001_220_697_3,
384            0.001_210_358_3,
385            0.000_920_454_33,
386            0.000_924_843_5,
387            0.000_812_128_4,
388            0.000_239_532_56,
389            0.000_931_822_4,
390            0.001_043_966_3,
391            0.000_483_734_15,
392            0.000_298_952_22,
393            0.000_484_425_4,
394            0.000_666_829_5,
395            0.000_998_398_5,
396            0.000_860_489_7,
397            0.000_183_153_23,
398            0.000_309_180_8,
399            0.000_542_646_2,
400            0.001_040_391_5,
401            0.000_755_456_6,
402            0.000_284_601_7,
403            0.000_600_979_3,
404            0.000_765_056_9,
405            0.000_562_810_46,
406            0.000_346_616_55,
407            0.000_236_224_32,
408            0.000_598_710_6,
409            0.000_295_684_27,
410            0.000_386_978_06,
411            0.000_584_258,
412            0.000_567_097_6,
413            0.000_613_644_4,
414            0.000_564_549_3,
415            0.000_235_384_52,
416            0.000_285_574_6,
417            0.000_385_352_93,
418            0.000_431_935_65,
419            0.000_731_246_5,
420            0.000_603_072_8,
421            0.001_033_130_8,
422            0.001_195_216_2,
423            0.000_824_500_7,
424            0.000_422_183_63,
425            0.000_821_760_16,
426            0.001_132_246,
427            0.000_891_406_73,
428            0.000_635_158_8,
429            0.000_372_681_56,
430            0.000_230_35,
431            0.000_628_649_3,
432            0.000_806_159_9,
433            0.000_661_622_15,
434            0.000_227_139_01,
435            0.000_214_694_96,
436            0.000_665_457_7,
437            0.000_513_901,
438            0.000_391_766_78,
439            0.001_079_094_7,
440            0.000_735_363_7,
441            0.000_171_665_73,
442            0.000_439_648_87,
443            0.000_295_145_3,
444            0.000_177_047_08,
445            0.000_182_958_97,
446            0.000_926_536_04,
447            0.000_832_408_3,
448            0.000_804_168_4,
449            0.001_131_809_3,
450            0.001_187_149_6,
451            0.000_806_948_8,
452            0.000_628_624_75,
453            0.000_591_386_1,
454            0.000_472_182_3,
455            0.000_163_652_31,
456            0.000_177_876_57,
457            0.000_425_363_75,
458            0.000_573_699_3,
459            0.000_434_679_24,
460            0.000_090_282_94,
461            0.000_172_573_55,
462            0.000_501_957_4,
463            0.000_614_716_8,
464            0.000_216_780_5,
465            0.000_148_974_3,
466            0.000_055_081_473,
467            0.000_296_264_13,
468            0.000_378_055_67,
469            0.000_147_361_96,
470            0.000_262_513_64,
471            0.000_162_118_42,
472            0.000_185_347_7,
473            0.000_138_735_4,
474        ];
475        assert!(
476            0.000_000_01 > (0.002_575_059_7 - geometric_mean(&input)).abs(),
477            "{} !~= 0.0025750597",
478            geometric_mean(&input)
479        );
480    }
481
482    #[test]
483    fn test_hz_to_octs_inplace() {
484        let mut frequencies = arr1(&[32., 64., 128., 256.]);
485        let expected = arr1(&[0.168_640_29, 1.168_640_29, 2.168_640_29, 3.168_640_29]);
486
487        hz_to_octs_inplace(&mut frequencies, 0.5, 10)
488            .iter()
489            .zip(expected.iter())
490            .for_each(|(x, y)| assert!(0.0001 > (x - y).abs(), "{x} !~= {y}"));
491    }
492
493    #[test]
494    fn test_compute_stft() {
495        let file = File::open("data/librosa-stft.npy").unwrap();
496        let expected_stft = Array2::<f32>::read_npy(file).unwrap().mapv(f64::from);
497
498        let song = Decoder::new()
499            .unwrap()
500            .decode(Path::new("data/piano.flac"))
501            .unwrap();
502
503        let stft = stft(&song.samples, 2048, 512);
504
505        assert!(!stft.is_empty() && !expected_stft.is_empty(), "Empty STFT");
506        for (expected, actual) in expected_stft.iter().zip(stft.iter()) {
507            // NOTE: can't use relative error here due to division by zero
508            assert!(
509                0.0001 > (expected - actual).abs(),
510                "{expected} !~= {actual}"
511            );
512        }
513    }
514
515    #[test]
516    fn test_reflect_pad() {
517        let array = Array::range(0., 100_000., 1.);
518
519        let output = reflect_pad(array.as_slice().unwrap(), 3);
520        assert_eq!(&output[..4], &[3.0, 2.0, 1.0, 0.]);
521        assert_eq!(&output[3..100_003], array.to_vec());
522        assert_eq!(&output[100_003..100_006], &[99998.0, 99997.0, 99996.0]);
523    }
524}