1use realfft::RealFftPlanner;
5use rustfft::num_complex::Complex;
6use std::f64::consts::PI;
7
8#[inline]
10pub fn pow_db(p: f64) -> f64 {
11 if p <= 1e-24 {
12 -240.0
13 } else {
14 10.0 * p.log10()
15 }
16}
17
18#[inline]
20pub fn amp_db(a: f64) -> f64 {
21 ferraus_core::lin_to_db(a)
22}
23
24pub fn hann(n: usize) -> Vec<f32> {
26 (0..n).map(|i| (0.5 - 0.5 * (2.0 * PI * i as f64 / n as f64).cos()) as f32).collect()
27}
28
29pub fn next_pow2(n: usize) -> usize {
31 n.max(1).next_power_of_two()
32}
33
34pub fn stft_complex(x: &[f32], n_fft: usize, hop: usize) -> Vec<Vec<Complex<f32>>> {
37 let mut planner = RealFftPlanner::<f32>::new();
38 let fft = planner.plan_fft_forward(n_fft);
39 let win = hann(n_fft);
40 let half = n_fft / 2;
41 let hop = hop.max(1);
42 let n_frames = if x.is_empty() { 0 } else { x.len() / hop + 1 };
43 let mut input = fft.make_input_vec();
44 let mut out = fft.make_output_vec();
45 let mut scratch = fft.make_scratch_vec();
46 let mut frames = Vec::with_capacity(n_frames);
47 for f in 0..n_frames {
48 let start = (f * hop) as isize - half as isize;
49 for (k, slot) in input.iter_mut().enumerate() {
50 let idx = start + k as isize;
51 *slot = if idx >= 0 && (idx as usize) < x.len() { x[idx as usize] * win[k] } else { 0.0 };
52 }
53 fft.process_with_scratch(&mut input, &mut out, &mut scratch).expect("fft sizes match");
54 frames.push(out.clone());
55 }
56 frames
57}
58
59pub fn stft_mag(x: &[f32], n_fft: usize, hop: usize) -> Vec<Vec<f32>> {
61 stft_complex(x, n_fft, hop).into_iter().map(|fr| fr.iter().map(|c| c.norm()).collect()).collect()
62}
63
64pub fn welch_power(channels: &[&[f32]], n_fft: usize) -> (Vec<f64>, f64) {
70 let mut planner = RealFftPlanner::<f32>::new();
71 let fft = planner.plan_fft_forward(n_fft);
72 let win = hann(n_fft);
73 let wss: f64 = win.iter().map(|w| (*w as f64).powi(2)).sum();
74 let hop = n_fft / 2;
75 let nb = n_fft / 2 + 1;
76 let mut acc = vec![0.0f64; nb];
77 let mut count = 0usize;
78 let mut input = fft.make_input_vec();
79 let mut out = fft.make_output_vec();
80 let mut scratch = fft.make_scratch_vec();
81 for ch in channels {
82 let n = ch.len();
83 if n == 0 {
84 continue;
85 }
86 let starts: Vec<usize> = if n < n_fft { vec![0] } else { (0..=(n - n_fft) / hop).map(|i| i * hop).collect() };
87 for start in starts {
88 let end = (start + n_fft).min(n);
89 let len = end - start;
90 let mean: f64 = ch[start..end].iter().map(|v| *v as f64).sum::<f64>() / len as f64;
91 for k in 0..n_fft {
92 input[k] = if k < len { ((ch[start + k] as f64 - mean) as f32) * win[k] } else { 0.0 };
93 }
94 fft.process_with_scratch(&mut input, &mut out, &mut scratch).expect("fft sizes match");
95 for (a, c) in acc.iter_mut().zip(out.iter()) {
96 *a += c.norm_sqr() as f64;
97 }
98 count += 1;
99 }
100 }
101 if count > 0 {
102 for a in acc.iter_mut() {
103 *a /= count as f64;
104 }
105 }
106 let scale = 2.0 / (n_fft as f64 * wss);
108 (acc, scale)
109}
110
111#[derive(Clone, Copy, Debug)]
113pub struct Biquad {
114 pub b0: f64,
115 pub b1: f64,
116 pub b2: f64,
117 pub a1: f64,
118 pub a2: f64,
119}
120
121impl Biquad {
122 pub fn lowpass(sr: f64, f0: f64, q: f64) -> Biquad {
124 let w = 2.0 * PI * (f0 / sr).min(0.49);
125 let (s, c) = w.sin_cos();
126 let alpha = s / (2.0 * q);
127 let a0 = 1.0 + alpha;
128 Biquad {
129 b0: (1.0 - c) / 2.0 / a0,
130 b1: (1.0 - c) / a0,
131 b2: (1.0 - c) / 2.0 / a0,
132 a1: -2.0 * c / a0,
133 a2: (1.0 - alpha) / a0,
134 }
135 }
136
137 pub fn highpass(sr: f64, f0: f64, q: f64) -> Biquad {
139 let w = 2.0 * PI * (f0 / sr).min(0.49);
140 let (s, c) = w.sin_cos();
141 let alpha = s / (2.0 * q);
142 let a0 = 1.0 + alpha;
143 Biquad {
144 b0: (1.0 + c) / 2.0 / a0,
145 b1: -(1.0 + c) / a0,
146 b2: (1.0 + c) / 2.0 / a0,
147 a1: -2.0 * c / a0,
148 a2: (1.0 - alpha) / a0,
149 }
150 }
151
152 pub fn run(&self, x: &[f32]) -> Vec<f32> {
154 let (mut x1, mut x2, mut y1, mut y2) = (0.0f64, 0.0, 0.0, 0.0);
155 x.iter()
156 .map(|&v| {
157 let v = v as f64;
158 let y = self.b0 * v + self.b1 * x1 + self.b2 * x2 - self.a1 * y1 - self.a2 * y2;
159 x2 = x1;
160 x1 = v;
161 y2 = y1;
162 y1 = y;
163 y as f32
164 })
165 .collect()
166 }
167
168 pub fn run_f64(&self, x: &[f64]) -> Vec<f64> {
170 let (mut x1, mut x2, mut y1, mut y2) = (0.0f64, 0.0, 0.0, 0.0);
171 x.iter()
172 .map(|&v| {
173 let y = self.b0 * v + self.b1 * x1 + self.b2 * x2 - self.a1 * y1 - self.a2 * y2;
174 x2 = x1;
175 x1 = v;
176 y2 = y1;
177 y1 = y;
178 y
179 })
180 .collect()
181 }
182}
183
184pub fn lowpass4(x: &[f32], sr: f64, f0: f64) -> Vec<f32> {
186 let a = Biquad::lowpass(sr, f0, 0.541_196_100_146_197);
187 let b = Biquad::lowpass(sr, f0, 1.306_562_964_876_376_4);
188 b.run(&a.run(x))
189}
190
191pub fn highpass4(x: &[f32], sr: f64, f0: f64) -> Vec<f32> {
193 let a = Biquad::highpass(sr, f0, 0.541_196_100_146_197);
194 let b = Biquad::highpass(sr, f0, 1.306_562_964_876_376_4);
195 b.run(&a.run(x))
196}
197
198pub fn hz_to_mel(f: f64) -> f64 {
200 2595.0 * (1.0 + f / 700.0).log10()
201}
202
203pub fn mel_to_hz(m: f64) -> f64 {
205 700.0 * (10f64.powf(m / 2595.0) - 1.0)
206}
207
208pub fn mel_filterbank(sr: f64, n_fft: usize, n_mels: usize, fmin: f64, fmax: f64) -> Vec<Vec<(usize, f32)>> {
211 let nb = n_fft / 2 + 1;
212 let df = sr / n_fft as f64;
213 let fmax = fmax.min(sr / 2.0);
214 let (m0, m1) = (hz_to_mel(fmin), hz_to_mel(fmax));
215 let pts: Vec<f64> = (0..n_mels + 2).map(|i| mel_to_hz(m0 + (m1 - m0) * i as f64 / (n_mels + 1) as f64)).collect();
216 (0..n_mels)
217 .map(|m| {
218 let (lo, c, hi) = (pts[m], pts[m + 1], pts[m + 2]);
219 let mut f = Vec::new();
220 for k in 0..nb {
221 let fk = k as f64 * df;
222 let w = if fk > lo && fk <= c {
223 (fk - lo) / (c - lo)
224 } else if fk > c && fk < hi {
225 (hi - fk) / (hi - c)
226 } else {
227 0.0
228 };
229 if w > 0.0 {
230 f.push((k, w as f32));
231 }
232 }
233 if f.is_empty() {
234 let k = ((c / df).round() as usize).min(nb - 1);
235 f.push((k, 1.0));
236 }
237 f
238 })
239 .collect()
240}
241
242pub fn apply_bank(bank: &[Vec<(usize, f32)>], frame: &[f32]) -> Vec<f32> {
244 bank.iter().map(|f| f.iter().map(|(k, w)| frame[*k] * w).sum()).collect()
245}
246
247pub fn dct2(x: &[f32], n_out: usize) -> Vec<f32> {
249 let n = x.len();
250 (0..n_out)
251 .map(|k| {
252 let s: f64 = x
253 .iter()
254 .enumerate()
255 .map(|(i, v)| *v as f64 * (PI * k as f64 * (i as f64 + 0.5) / n as f64).cos())
256 .sum();
257 let norm = if k == 0 { (1.0 / n as f64).sqrt() } else { (2.0 / n as f64).sqrt() };
258 (s * norm) as f32
259 })
260 .collect()
261}
262
263pub fn xcorr_peak(a: &[f32], b: &[f32], max_lag: usize) -> (isize, f64, f64) {
268 if a.is_empty() || b.is_empty() {
269 return (0, 0.0, 0.0);
270 }
271 let n = next_pow2(a.len() + b.len());
272 let mut planner = RealFftPlanner::<f64>::new();
273 let fwd = planner.plan_fft_forward(n);
274 let inv = planner.plan_fft_inverse(n);
275 let mut ia = fwd.make_input_vec();
276 let mut ib = fwd.make_input_vec();
277 for (s, v) in ia.iter_mut().zip(a) {
278 *s = *v as f64;
279 }
280 for (s, v) in ib.iter_mut().zip(b) {
281 *s = *v as f64;
282 }
283 let mut fa = fwd.make_output_vec();
284 let mut fb = fwd.make_output_vec();
285 fwd.process(&mut ia, &mut fa).expect("fft");
286 fwd.process(&mut ib, &mut fb).expect("fft");
287 let mut prod: Vec<Complex<f64>> = fa.iter().zip(&fb).map(|(x, y)| x.conj() * y).collect();
288 if let Some(f) = prod.first_mut() {
290 f.im = 0.0;
291 }
292 if let Some(l) = prod.last_mut() {
293 l.im = 0.0;
294 }
295 let mut c = inv.make_output_vec();
296 inv.process(&mut prod, &mut c).expect("ifft");
297 let scale = 1.0 / n as f64;
298 let max_pos = max_lag.min(b.len().saturating_sub(1));
299 let max_neg = max_lag.min(a.len().saturating_sub(1));
300 let mut best = (0isize, 0.0f64);
301 for (lag, cv) in c.iter().enumerate().take(max_pos + 1) {
302 let v = cv * scale;
303 if v.abs() > best.1.abs() {
304 best = (lag as isize, v);
305 }
306 }
307 for lag in 1..=max_neg {
308 let v = c[n - lag] * scale;
309 if v.abs() > best.1.abs() {
310 best = (-(lag as isize), v);
311 }
312 }
313 let ea: f64 = a.iter().map(|v| (*v as f64).powi(2)).sum();
314 let eb: f64 = b.iter().map(|v| (*v as f64).powi(2)).sum();
315 let norm = (ea * eb).sqrt();
316 (best.0, best.1, if norm > 0.0 { best.1 / norm } else { 0.0 })
317}
318
319pub fn percentile(x: &[f64], p: f64) -> f64 {
321 if x.is_empty() {
322 return f64::NAN;
323 }
324 let mut v: Vec<f64> = x.to_vec();
325 v.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
326 let pos = (p / 100.0).clamp(0.0, 1.0) * (v.len() - 1) as f64;
327 let lo = pos.floor() as usize;
328 let hi = pos.ceil() as usize;
329 v[lo] + (v[hi] - v[lo]) * (pos - lo as f64)
330}
331
332pub fn median(x: &[f64]) -> f64 {
334 percentile(x, 50.0)
335}
336
337pub fn mean(x: &[f64]) -> f64 {
339 if x.is_empty() {
340 0.0
341 } else {
342 x.iter().sum::<f64>() / x.len() as f64
343 }
344}
345
346pub fn std_dev(x: &[f64]) -> f64 {
348 if x.len() < 2 {
349 return 0.0;
350 }
351 let m = mean(x);
352 (x.iter().map(|v| (v - m).powi(2)).sum::<f64>() / x.len() as f64).sqrt()
353}
354
355pub fn pearson(a: &[f64], b: &[f64]) -> f64 {
357 let n = a.len().min(b.len());
358 if n < 2 {
359 return 0.0;
360 }
361 let (ma, mb) = (mean(&a[..n]), mean(&b[..n]));
362 let (mut sab, mut saa, mut sbb) = (0.0, 0.0, 0.0);
363 for i in 0..n {
364 let (x, y) = (a[i] - ma, b[i] - mb);
365 sab += x * y;
366 saa += x * x;
367 sbb += y * y;
368 }
369 if saa <= 0.0 || sbb <= 0.0 {
370 return if saa <= 0.0 && sbb <= 0.0 { 1.0 } else { 0.0 };
371 }
372 sab / (saa * sbb).sqrt()
373}
374
375pub fn tidy(x: f64) -> f64 {
379 if !x.is_finite() {
380 return x;
381 }
382 if x == 0.0 {
383 return 0.0;
384 }
385 let mag = x.abs().log10().floor() as i32;
386 let decimals = (3 - mag).clamp(4, 7);
387 let f = 10f64.powi(decimals);
388 let r = (x * f).round() / f;
389 if r == 0.0 {
390 0.0
391 } else {
392 r
393 }
394}
395
396pub fn tidy_json(v: &mut serde_json::Value) {
398 match v {
399 serde_json::Value::Number(n) => {
400 if n.is_f64() {
401 if let Some(f) = n.as_f64() {
402 if let Some(t) = serde_json::Number::from_f64(tidy(f)) {
403 *n = t;
404 }
405 }
406 }
407 }
408 serde_json::Value::Array(a) => a.iter_mut().for_each(tidy_json),
409 serde_json::Value::Object(o) => o.values_mut().for_each(tidy_json),
410 _ => {}
411 }
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417
418 #[test]
419 fn xcorr_finds_delay() {
420 let a: Vec<f32> = (0..2000).map(|i| ((i * 7919) % 101) as f32 / 50.0 - 1.0).collect();
421 let mut b = vec![0.0f32; 37];
422 b.extend_from_slice(&a[..2000 - 37]);
423 let (lag, _, norm) = xcorr_peak(&a, &b, 200);
424 assert_eq!(lag, 37);
425 assert!(norm > 0.9);
426 let (lag, _, _) = xcorr_peak(&b, &a, 200);
427 assert_eq!(lag, -37);
428 }
429
430 #[test]
431 fn welch_parseval_sine() {
432 let sr = 48000.0;
433 let x: Vec<f32> = (0..48000).map(|i| (0.5 * (2.0 * PI * 1000.0 * i as f64 / sr).sin()) as f32).collect();
434 let (p, scale) = welch_power(&[&x], 4096);
435 let ms: f64 = p.iter().map(|v| v * scale).sum();
436 assert!((ms - 0.125).abs() < 0.005, "{ms}");
437 }
438
439 #[test]
440 fn tidy_rounds() {
441 assert_eq!(tidy(-23.012345678), -23.0123);
442 assert_eq!(tidy(0.000123456789), 0.0001235);
443 }
444}