1#![no_std]
2#![allow(incomplete_features)]
3#![feature(generic_const_exprs)]
4
5use core::num::NonZeroUsize;
6
7use microfft::Complex32;
8
9pub use crate::window::Window;
10use crate::{
11 utils::{detrend_constant, overlapping_windows, Assert, IsTrue},
12 window::get_window,
13};
14
15mod fft;
16mod utils;
17mod window;
18
19pub struct Spectrogram<const N: usize> {
20 fs: usize,
21 win: Window,
22}
23
24impl<const N: usize> Spectrogram<N>
25where
26 Assert<{ (N == 4) | (N == 8) | (N == 16) }>: IsTrue,
27{
28 pub fn new(fs: usize, win: Window) -> Self {
29 Self { fs, win }
30 }
31
32 pub fn compute<const X: usize>(&self, data: &[f32; X]) -> [[f32; X / (N / 2) - 1]; 1 + N / 2]
33 where
34 [(); N + 1]: Sized,
35 {
36 let overlap = N / 2;
37 let wins = overlapping_windows(
38 data,
39 NonZeroUsize::new(N).unwrap(),
40 NonZeroUsize::new(overlap).unwrap(),
41 );
42
43 let mut seg = [0.0; N];
44 let mut col = [0.0; 1 + N / 2];
45 let mut out = [[0.0; X / (N / 2) - 1]; 1 + N / 2];
46
47 for (i, data) in wins.enumerate() {
48 seg.copy_from_slice(data);
49 process_segment::<N>(&mut seg, &mut col, self.fs, self.win);
50 col.into_iter().enumerate().for_each(|(j, v)| {
51 out[j][i] = v;
52 });
53 }
54 out
55 }
56}
57
58fn process_segment<const N: usize>(
59 arr: &mut [f32; N],
60 result: &mut [f32; 1 + N / 2],
61 fs: usize,
62 win: Window,
63) where
64 [(); N + 1]: Sized,
65{
66 let win = get_window::<N>(win, false);
67 detrend_constant(arr);
68 arr.iter_mut().zip(win).for_each(|(x, w)| {
69 *x *= w;
70 });
71
72 let mut result_c = [Complex32::new(0.0, 0.0); 1 + N / 2];
74
75 unsafe { fft::fft(arr, &mut result_c) };
78
79 let mut win_sq_sum = 0.0;
80 win.iter().for_each(|x| {
81 win_sq_sum += x * x;
82 });
83
84 let scale = 1.0 / (fs as f32 * win_sq_sum);
86
87 result_c.iter_mut().for_each(|x| {
88 let conj = Complex32::new(x.re, -x.im);
89 *x *= conj * scale * 2.0;
90 });
91
92 result_c[0] /= 2.0;
93 result_c[result_c.len() - 1] /= 2.0;
94
95 result.iter_mut().zip(result_c).for_each(|(r, rc)| {
96 *r = rc.re;
97 });
98}
99
100#[cfg(test)]
101mod test {
102 use core::ops;
103
104 use num_traits::ToPrimitive;
105
106 use super::*;
107
108 fn all_close<T, const N: usize>(a: &[T; N], b: &[T; N]) -> bool
109 where
110 T: ops::Sub<Output = T> + ToPrimitive + Copy,
111 {
112 const EPSILON: f32 = 1e-5;
113 for i in 0..N {
114 if (a[i] - b[i]).to_f32().unwrap() > EPSILON {
115 return false;
116 }
117 }
118 true
119 }
120
121 fn all_close2d<T, const N: usize, const M: usize>(a: &[[T; N]; M], b: &[[T; N]; M]) -> bool
122 where
123 T: ops::Sub<Output = T> + ToPrimitive + Copy,
124 {
125 const EPSILON: f32 = 1e-5;
126 for i in 0..M {
127 for j in 0..N {
128 if (a[i][j] - b[i][j]).to_f32().unwrap() > EPSILON {
129 return false;
130 }
131 }
132 }
133 true
134 }
135
136 #[test]
137 #[allow(clippy::excessive_precision)]
138 fn test_segment() {
139 let mut input = [
140 0.65600173, 0.63951888, 0.62348176, 0.60798841, 0.59066761, 0.57568833, 0.56063477,
141 0.54556932, 0.53043256, 0.51324547, 0.4984534, 0.48411299, 0.46861073, 0.45568773,
142 0.4428301, 0.42829952,
143 ];
144 let expected_scipy = [
145 8.22254870e-04,
146 1.87558778e-02,
147 9.88275690e-04,
148 4.09807662e-05,
149 1.52053553e-05,
150 8.80124399e-07,
151 1.94954969e-06,
152 4.08357001e-07,
153 6.61331734e-10,
154 ];
155 let mut result = Default::default();
156 process_segment(&mut input, &mut result, 1, Window::Hann);
157 assert!(all_close(&result, &expected_scipy));
158 }
159
160 #[test]
161 #[allow(clippy::excessive_precision)]
162 fn test_spectrogram() {
163 let x = [
164 0.65600173, 0.63951888, 0.62348176, 0.60798841, 0.59066761, 0.57568833, 0.56063477,
165 0.54556932, 0.53043256, 0.51324547, 0.4984534, 0.48411299, 0.46861073, 0.45568773,
166 0.4428301, 0.42829952, 0.41565583, 0.40287546, 0.38830922, 0.37554074, 0.36229681,
167 0.34613488, 0.32802069, 0.30968066,
168 ];
169 let expected = [
170 [8.22254870e-04, 4.46185932e-04],
171 [1.87558778e-02, 1.44202233e-02],
172 [9.88275690e-04, 6.00894689e-04],
173 [4.09807662e-05, 3.58241931e-05],
174 [1.52053553e-05, 5.18071551e-06],
175 [8.80124399e-07, 8.84743592e-06],
176 [1.94954969e-06, 1.82243933e-07],
177 [4.08357001e-07, 2.45994606e-07],
178 [6.61331734e-10, 8.14788254e-09],
179 ];
180
181 let spec = Spectrogram::<16>::new(1, Window::Hann);
182 let out = spec.compute(&x);
183 assert!(all_close2d(&out, &expected));
184 }
185}