1use std::f32::consts::PI;
5use super::fft::{Spectrum, Stft, StftConfig, Autocorrelation, Complex32};
6use super::{WindowFunction, next_power_of_two, linear_to_db, freq_to_midi};
7
8pub struct OnsetDetector;
14
15impl OnsetDetector {
16 pub fn spectral_flux(signal: &[f32], sample_rate: f32, threshold: f32) -> Vec<f32> {
18 let stft = Stft::new(StftConfig {
19 fft_size: 1024,
20 hop_size: 256,
21 window: WindowFunction::Hann,
22 });
23 let frames = stft.analyze(signal);
24 let mut onsets = Vec::new();
25 for i in 1..frames.len() {
26 let flux = frames[i - 1].spectral_flux(&frames[i]);
27 if flux > threshold {
28 let time = i as f32 * 256.0 / sample_rate;
29 onsets.push(time);
30 }
31 }
32 onsets
33 }
34}
35
36pub struct SpectralFluxOnset {
38 pub threshold_factor: f32,
40 pub hop_size: usize,
42 pub fft_size: usize,
44}
45
46impl SpectralFluxOnset {
47 pub fn new(threshold_factor: f32, fft_size: usize, hop_size: usize) -> Self {
48 Self { threshold_factor, hop_size, fft_size }
49 }
50
51 pub fn detect(&self, signal: &[f32], sample_rate: f32) -> Vec<f32> {
53 let stft = Stft::new(StftConfig {
54 fft_size: self.fft_size,
55 hop_size: self.hop_size,
56 window: WindowFunction::Hann,
57 });
58 let frames = stft.analyze(signal);
59 if frames.len() < 2 { return Vec::new(); }
60
61 let mut flux: Vec<f32> = vec![0.0];
63 for i in 1..frames.len() {
64 flux.push(frames[i - 1].spectral_flux(&frames[i]));
65 }
66
67 let window = 8usize;
69 let threshold: Vec<f32> = flux.iter().enumerate().map(|(i, _)| {
70 let lo = i.saturating_sub(window);
71 let hi = (i + window + 1).min(flux.len());
72 let mut local: Vec<f32> = flux[lo..hi].to_vec();
73 local.sort_by(|a, b| a.partial_cmp(b).unwrap());
74 local[local.len() / 2] * self.threshold_factor
75 }).collect();
76
77 let mut onsets = Vec::new();
79 for i in 1..flux.len().saturating_sub(1) {
80 if flux[i] > threshold[i]
81 && flux[i] > flux[i - 1]
82 && flux[i] > flux.get(i + 1).copied().unwrap_or(0.0)
83 {
84 onsets.push(i as f32 * self.hop_size as f32 / sample_rate);
85 }
86 }
87 onsets
88 }
89}
90
91pub struct HfcOnset {
93 pub threshold: f32,
94 pub fft_size: usize,
95 pub hop_size: usize,
96}
97
98impl HfcOnset {
99 pub fn new(threshold: f32, fft_size: usize, hop_size: usize) -> Self {
100 Self { threshold, fft_size, hop_size }
101 }
102
103 pub fn detect(&self, signal: &[f32], sample_rate: f32) -> Vec<f32> {
105 let stft = Stft::new(StftConfig {
106 fft_size: self.fft_size,
107 hop_size: self.hop_size,
108 window: WindowFunction::Hann,
109 });
110 let frames = stft.analyze(signal);
111 let mut hfc_curve: Vec<f32> = frames.iter().map(|f| {
112 f.bins.iter().enumerate().map(|(k, c)| {
113 (k as f32) * c.norm_sq()
114 }).sum::<f32>()
115 }).collect();
116
117 let max_hfc = hfc_curve.iter().cloned().fold(0.0f32, f32::max);
119 if max_hfc > 1e-10 {
120 for h in hfc_curve.iter_mut() { *h /= max_hfc; }
121 }
122
123 let mut onsets = Vec::new();
125 for i in 1..hfc_curve.len().saturating_sub(1) {
126 let prev = hfc_curve[i - 1];
127 let curr = hfc_curve[i];
128 let next = hfc_curve.get(i + 1).copied().unwrap_or(0.0);
129 if curr > self.threshold && curr > prev && curr > next {
130 onsets.push(i as f32 * self.hop_size as f32 / sample_rate);
131 }
132 }
133 onsets
134 }
135}
136
137pub struct ComplexDomainOnset {
139 pub threshold: f32,
140 pub fft_size: usize,
141 pub hop_size: usize,
142}
143
144impl ComplexDomainOnset {
145 pub fn new(threshold: f32, fft_size: usize, hop_size: usize) -> Self {
146 Self { threshold, fft_size, hop_size }
147 }
148
149 pub fn detect(&self, signal: &[f32], sample_rate: f32) -> Vec<f32> {
151 let stft = Stft::new(StftConfig {
152 fft_size: self.fft_size,
153 hop_size: self.hop_size,
154 window: WindowFunction::Hann,
155 });
156 let frames = stft.analyze(signal);
157 if frames.len() < 3 { return Vec::new(); }
158
159 let mut cd: Vec<f32> = vec![0.0, 0.0];
161 for t in 2..frames.len() {
162 let frame_t = &frames[t];
163 let frame_t1 = &frames[t - 1];
164 let frame_t2 = &frames[t - 2];
165 let n_bins = frame_t.num_bins();
166 let mut sum = 0.0f32;
167 for k in 0..n_bins {
168 let mag = frame_t.magnitude(k);
169 let mag_t1 = frame_t1.magnitude(k);
170 let ph = frame_t.phase(k);
171 let ph_t1 = frame_t1.phase(k);
172 let ph_t2 = frame_t2.phase(k);
173 let predicted_ph = 2.0 * ph_t1 - ph_t2;
175 let target_re = mag_t1 * predicted_ph.cos();
177 let target_im = mag_t1 * predicted_ph.sin();
178 let actual_re = mag * ph.cos();
179 let actual_im = mag * ph.sin();
180 let diff_re = actual_re - target_re;
181 let diff_im = actual_im - target_im;
182 sum += (diff_re * diff_re + diff_im * diff_im).sqrt();
183 }
184 cd.push(sum / n_bins as f32);
185 }
186
187 let max_cd = cd.iter().cloned().fold(0.0f32, f32::max);
188 if max_cd > 1e-10 {
189 for v in cd.iter_mut() { *v /= max_cd; }
190 }
191
192 let mut onsets = Vec::new();
193 for i in 1..cd.len().saturating_sub(1) {
194 if cd[i] > self.threshold && cd[i] > cd[i - 1] && cd[i] > cd.get(i + 1).copied().unwrap_or(0.0) {
195 onsets.push(i as f32 * self.hop_size as f32 / sample_rate);
196 }
197 }
198 onsets
199 }
200}
201
202pub struct BeatTracker;
208
209impl BeatTracker {
210 pub fn estimate_tempo(signal: &[f32], sample_rate: f32) -> f32 {
212 let onset_env = Self::onset_envelope(signal, sample_rate);
213 let autocorr = Autocorrelation::compute_fft(&onset_env);
214 let hop_size = 512usize;
216 let env_sr = sample_rate / hop_size as f32;
217 let min_period = (60.0 * env_sr / 200.0) as usize;
219 let max_period = (60.0 * env_sr / 60.0) as usize;
220 let max_period = max_period.min(autocorr.len() - 1);
221 if min_period >= max_period { return 120.0; }
222 let (best_lag, _) = autocorr[min_period..=max_period].iter().enumerate()
223 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
224 .unwrap_or((0, &0.0));
225 let period_frames = best_lag + min_period;
226 if period_frames == 0 { return 120.0; }
227 60.0 * env_sr / period_frames as f32
228 }
229
230 pub fn detect_beats(signal: &[f32], sample_rate: f32) -> Vec<f32> {
232 let onset_env = Self::onset_envelope(signal, sample_rate);
233 let hop_size = 512usize;
234 let env_sr = sample_rate / hop_size as f32;
235 let bpm = Self::estimate_tempo(signal, sample_rate).max(60.0).min(200.0);
236 let beat_period = 60.0 / bpm * env_sr;
237 if beat_period < 1.0 { return Vec::new(); }
238
239 let mut beats = Vec::new();
241 let mut phase = Self::find_initial_beat_phase(&onset_env, beat_period as usize);
242 while phase < onset_env.len() {
243 beats.push(phase as f32 * hop_size as f32 / sample_rate);
244 phase += beat_period as usize;
245 }
246 beats
247 }
248
249 pub fn onset_envelope(signal: &[f32], _sample_rate: f32) -> Vec<f32> {
251 let hop_size = 512usize;
252 let fft_size = 2048usize;
253 let stft = Stft::new(StftConfig {
254 fft_size,
255 hop_size,
256 window: WindowFunction::Hann,
257 });
258 let frames = stft.analyze(signal);
259 let mut env = vec![0.0f32; frames.len()];
260 for i in 1..frames.len() {
261 env[i] = frames[i - 1].spectral_flux(&frames[i]);
262 }
263 for v in env.iter_mut() { if *v < 0.0 { *v = 0.0; } }
265 let alpha = 0.9f32;
267 let mut prev = 0.0f32;
268 for v in env.iter_mut() {
269 *v = (1.0 - alpha) * *v + alpha * prev;
270 prev = *v;
271 }
272 env
273 }
274
275 fn find_initial_beat_phase(env: &[f32], period: usize) -> usize {
276 if period == 0 || env.is_empty() { return 0; }
277 let max_offset = period.min(env.len());
279 let (best_offset, _) = (0..max_offset).map(|offset| {
280 let mut sum = 0.0f32;
281 let mut t = offset;
282 while t < env.len() {
283 sum += env[t];
284 t += period;
285 }
286 (offset, sum)
287 }).max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
288 .unwrap_or((0, 0.0));
289 best_offset
290 }
291
292 pub fn tempogram(signal: &[f32], sample_rate: f32) -> Vec<(f32, f32)> {
294 let env = Self::onset_envelope(signal, sample_rate);
295 let hop_size = 512usize;
296 let env_sr = sample_rate / hop_size as f32;
297 let autocorr = Autocorrelation::compute_fft(&env);
298 let mut tg = Vec::new();
299 for lag in 1..autocorr.len() {
300 let bpm = 60.0 * env_sr / lag as f32;
301 if bpm >= 40.0 && bpm <= 300.0 {
302 tg.push((bpm, autocorr[lag]));
303 }
304 }
305 tg.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); tg
307 }
308}
309
310pub struct PitchDetector;
316
317impl PitchDetector {
318 pub fn yin(signal: &[f32], sample_rate: f32, min_hz: f32, max_hz: f32) -> Option<f32> {
320 let freq = Autocorrelation::pitch_yin(signal, sample_rate)?;
321 if freq >= min_hz && freq <= max_hz { Some(freq) } else { None }
322 }
323
324 pub fn autocorr(signal: &[f32], sample_rate: f32) -> Option<f32> {
326 let n = signal.len();
327 if n < 2 { return None; }
328 let ac = Autocorrelation::compute_fft(signal);
329 let mut found_first_min = false;
331 let mut first_min_idx = 0usize;
332 for i in 1..ac.len().saturating_sub(1) {
333 if !found_first_min {
334 if ac[i] < ac[i - 1] {
335 found_first_min = true;
336 first_min_idx = i;
337 }
338 }
339 }
340 if !found_first_min { return None; }
341 let (peak_idx, _) = ac[first_min_idx..].iter().enumerate()
342 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())?;
343 let tau = peak_idx + first_min_idx;
344 if tau == 0 { return None; }
345 let freq = sample_rate / tau as f32;
346 if freq < 20.0 || freq > 20000.0 { return None; }
347 Some(freq)
348 }
349
350 pub fn harmonic_product_spectrum(spectrum: &[f32], sample_rate: f32) -> f32 {
352 let n = spectrum.len();
353 let n_harmonics = 5usize;
354 let usable = n / n_harmonics;
355 let mut hps: Vec<f32> = spectrum[..usable].to_vec();
356 for h in 2..=n_harmonics {
357 for k in 0..usable {
358 let idx = k * h;
359 if idx < n {
360 hps[k] *= spectrum[idx];
361 }
362 }
363 }
364 let (peak_k, _) = hps.iter().enumerate()
365 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
366 .unwrap_or((0, &0.0));
367 peak_k as f32 * sample_rate / (2 * n) as f32
368 }
369
370 pub fn midi_note(freq: f32) -> f32 {
372 freq_to_midi(freq)
373 }
374
375 pub fn note_name(midi: f32) -> String {
377 let names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
378 let midi_i = midi.round() as i32;
379 let octave = midi_i / 12 - 1;
380 let pitch_class = ((midi_i % 12) + 12) as usize % 12;
381 format!("{}{}", names[pitch_class], octave)
382 }
383
384 pub fn detect_from_signal(signal: &[f32], sample_rate: f32) -> Option<f32> {
386 let fft_size = 2048usize;
387 let mut buf = signal.to_vec();
388 buf.resize(next_power_of_two(signal.len().max(fft_size)), 0.0);
389 WindowFunction::Hann.apply(&mut buf[..fft_size]);
390 let spectrum = Spectrum::from_real(&buf[..fft_size]);
391 let mag_spec = spectrum.magnitude_spectrum();
392 let freq = Self::harmonic_product_spectrum(&mag_spec, sample_rate);
393 if freq > 20.0 && freq < 20000.0 { Some(freq) } else { None }
394 }
395
396 pub fn pitch_confidence(spectrum: &Spectrum, fundamental_hz: f32, sample_rate: f32) -> f32 {
398 let n_bins = spectrum.num_bins();
399 let bin_res = sample_rate / spectrum.fft_size as f32;
400 let n_harmonics = 8;
402 let mut harmonic_energy = 0.0f32;
403 let total_energy: f32 = spectrum.bins.iter().map(|c| c.norm_sq()).sum();
404 for h in 1..=n_harmonics {
405 let target_bin = (fundamental_hz * h as f32 / bin_res).round() as usize;
406 let search_range = 2usize;
407 for k in target_bin.saturating_sub(search_range)..=(target_bin + search_range).min(n_bins - 1) {
408 harmonic_energy += spectrum.power(k);
409 }
410 }
411 if total_energy < 1e-10 { 0.0 } else { harmonic_energy / total_energy }
412 }
413}
414
415pub struct LoudnessMeters;
420
421#[derive(Debug, Clone)]
423pub struct Rms {
424 pub window_ms: f32,
426 sample_rate: f32,
427 buffer: Vec<f32>,
428 sum_sq: f32,
429 write_pos: usize,
430 count: usize,
431}
432
433impl Rms {
434 pub fn new(window_ms: f32, sample_rate: f32) -> Self {
435 let len = ((window_ms / 1000.0) * sample_rate).round() as usize;
436 let len = len.max(1);
437 Self {
438 window_ms,
439 sample_rate,
440 buffer: vec![0.0; len],
441 sum_sq: 0.0,
442 write_pos: 0,
443 count: 0,
444 }
445 }
446
447 pub fn process(&mut self, x: f32) -> f32 {
449 let old = self.buffer[self.write_pos];
450 self.sum_sq -= old * old;
451 self.sum_sq = self.sum_sq.max(0.0); self.buffer[self.write_pos] = x;
453 self.sum_sq += x * x;
454 self.write_pos = (self.write_pos + 1) % self.buffer.len();
455 if self.count < self.buffer.len() { self.count += 1; }
456 (self.sum_sq / self.count as f32).sqrt()
457 }
458
459 pub fn process_buffer(&mut self, buf: &[f32]) -> Vec<f32> {
461 buf.iter().map(|&x| self.process(x)).collect()
462 }
463
464 pub fn level(&self) -> f32 {
466 if self.count == 0 { 0.0 } else { (self.sum_sq / self.count as f32).sqrt() }
467 }
468
469 pub fn level_db(&self) -> f32 { linear_to_db(self.level()) }
471
472 pub fn reset(&mut self) {
474 self.buffer.fill(0.0);
475 self.sum_sq = 0.0;
476 self.write_pos = 0;
477 self.count = 0;
478 }
479}
480
481#[derive(Debug, Clone)]
483pub struct Leq {
484 sum_sq: f64,
485 count: u64,
486}
487
488impl Leq {
489 pub fn new() -> Self { Self { sum_sq: 0.0, count: 0 } }
490
491 pub fn push(&mut self, samples: &[f32]) {
493 for &s in samples {
494 self.sum_sq += (s as f64) * (s as f64);
495 self.count += 1;
496 }
497 }
498
499 pub fn leq_db(&self) -> f32 {
501 if self.count == 0 { return -f32::INFINITY; }
502 let rms = (self.sum_sq / self.count as f64).sqrt() as f32;
503 linear_to_db(rms)
504 }
505
506 pub fn reset(&mut self) { self.sum_sq = 0.0; self.count = 0; }
508}
509
510impl Default for Leq { fn default() -> Self { Self::new() } }
511
512#[derive(Debug, Clone)]
516pub struct Lufs {
517 sample_rate: f32,
518 gated_blocks: Vec<f32>,
520 block_samples: Vec<f32>,
522 block_size: usize,
523 z1_1: f32,
526 z2_1: f32,
527 z1_2: f32,
529 z2_2: f32,
530}
531
532impl Lufs {
533 pub fn new(sample_rate: f32) -> Self {
534 let block_size = (0.4 * sample_rate) as usize; Self {
536 sample_rate,
537 gated_blocks: Vec::new(),
538 block_samples: Vec::with_capacity(block_size),
539 block_size,
540 z1_1: 0.0, z2_1: 0.0, z1_2: 0.0, z2_2: 0.0,
541 }
542 }
543
544 fn kw_coeffs(sample_rate: f32) -> (f32, f32, f32, f32, f32, f32, f32, f32, f32, f32) {
546 let f0 = 1681.974450955533;
548 let g = 3.999843853973347;
549 let q1 = 0.7071752369554196;
550 let k1 = (PI * f0 / sample_rate).tan();
551 let a_lin = 10.0f32.powf(g / 40.0);
552 let b0_1 = a_lin * ((a_lin / q1) * k1 + k1 * k1 + 1.0) / (k1 * k1 + (1.0 / q1) * k1 + 1.0);
553 let b1_1 = 2.0 * a_lin * (k1 * k1 - 1.0) / (k1 * k1 + (1.0 / q1) * k1 + 1.0);
554 let b2_1 = a_lin * (k1 * k1 - (a_lin / q1) * k1 + 1.0) / (k1 * k1 + (1.0 / q1) * k1 + 1.0);
555 let a1_1 = 2.0 * (k1 * k1 - 1.0) / (k1 * k1 + (1.0 / q1) * k1 + 1.0);
556 let a2_1 = (k1 * k1 - (1.0 / q1) * k1 + 1.0) / (k1 * k1 + (1.0 / q1) * k1 + 1.0);
557 let f2 = 38.13547087602444;
559 let q2 = 0.5003270373238773;
560 let k2 = (PI * f2 / sample_rate).tan();
561 let b0_2 = 1.0 / (1.0 + k2 / q2 + k2 * k2);
562 let b1_2 = -2.0 * b0_2;
563 let b2_2 = b0_2;
564 let a1_2 = 2.0 * (k2 * k2 - 1.0) * b0_2;
565 let a2_2 = (1.0 - k2 / q2 + k2 * k2) * b0_2;
566 (b0_1, b1_1, b2_1, a1_1, a2_1, b0_2, b1_2, b2_2, a1_2, a2_2)
567 }
568
569 fn k_weight(&mut self, x: f32) -> f32 {
571 let (b0_1, b1_1, b2_1, a1_1, a2_1, b0_2, b1_2, b2_2, a1_2, a2_2) =
572 Self::kw_coeffs(self.sample_rate);
573 let y1 = b0_1 * x + self.z1_1;
575 self.z1_1 = b1_1 * x - a1_1 * y1 + self.z2_1;
576 self.z2_1 = b2_1 * x - a2_1 * y1;
577 let y2 = b0_2 * y1 + self.z1_2;
579 self.z1_2 = b1_2 * y1 - a1_2 * y2 + self.z2_2;
580 self.z2_2 = b2_2 * y1 - a2_2 * y2;
581 y2
582 }
583
584 pub fn push(&mut self, samples: &[f32]) {
586 for &s in samples {
587 let kw = self.k_weight(s);
588 self.block_samples.push(kw);
589 if self.block_samples.len() >= self.block_size {
590 let power: f32 = self.block_samples.iter().map(|&x| x * x).sum::<f32>()
591 / self.block_size as f32;
592 self.gated_blocks.push(power);
593 self.block_samples.clear();
594 }
595 }
596 }
597
598 pub fn integrated_lufs(&self) -> f32 {
600 if self.gated_blocks.is_empty() { return -f32::INFINITY; }
601 let absolute_gate = 10.0f32.powf((-70.0 + 0.691) / 10.0);
603 let ungated: Vec<f32> = self.gated_blocks.iter().copied()
604 .filter(|&p| p > absolute_gate)
605 .collect();
606 if ungated.is_empty() { return -f32::INFINITY; }
607 let ungated_mean: f32 = ungated.iter().sum::<f32>() / ungated.len() as f32;
609 let relative_gate = ungated_mean * 10.0f32.powf(-10.0 / 10.0);
610 let gated: Vec<f32> = ungated.iter().copied()
611 .filter(|&p| p > relative_gate)
612 .collect();
613 if gated.is_empty() { return -f32::INFINITY; }
614 let mean: f32 = gated.iter().sum::<f32>() / gated.len() as f32;
615 -0.691 + 10.0 * mean.log10()
616 }
617
618 pub fn reset(&mut self) {
620 self.gated_blocks.clear();
621 self.block_samples.clear();
622 self.z1_1 = 0.0; self.z2_1 = 0.0;
623 self.z1_2 = 0.0; self.z2_2 = 0.0;
624 }
625}
626
627#[derive(Debug, Clone)]
629pub struct DynamicRange;
630
631impl DynamicRange {
632 pub fn crest_factor(signal: &[f32]) -> f32 {
634 if signal.is_empty() { return 0.0; }
635 let peak = signal.iter().map(|&x| x.abs()).fold(0.0f32, f32::max);
636 let rms = (signal.iter().map(|&x| x * x).sum::<f32>() / signal.len() as f32).sqrt();
637 if rms < 1e-10 { 0.0 } else { peak / rms }
638 }
639
640 pub fn crest_factor_db(signal: &[f32]) -> f32 {
642 let cf = Self::crest_factor(signal);
643 if cf <= 0.0 { return -f32::INFINITY; }
644 20.0 * cf.log10()
645 }
646
647 pub fn dynamic_range_db(signal: &[f32]) -> f32 {
649 if signal.is_empty() { return 0.0; }
650 let peak_db = {
651 let peak = signal.iter().map(|&x| x.abs()).fold(0.0f32, f32::max);
652 linear_to_db(peak)
653 };
654 let rms_db = {
655 let rms = (signal.iter().map(|&x| x * x).sum::<f32>() / signal.len() as f32).sqrt();
656 linear_to_db(rms)
657 };
658 (peak_db - rms_db).max(0.0)
659 }
660
661 pub fn loudness_range(signal: &[f32], sample_rate: f32) -> f32 {
663 let block_size = (0.3 * sample_rate) as usize;
664 if block_size == 0 || signal.len() < block_size { return 0.0; }
665 let mut levels: Vec<f32> = signal.chunks(block_size).map(|block| {
666 let rms = (block.iter().map(|&x| x * x).sum::<f32>() / block.len() as f32).sqrt();
667 linear_to_db(rms)
668 }).filter(|&db| db > -70.0)
669 .collect();
670 if levels.len() < 3 { return 0.0; }
671 levels.sort_by(|a, b| a.partial_cmp(b).unwrap());
672 let lo = levels[(levels.len() as f32 * 0.10) as usize];
673 let hi = levels[(levels.len() as f32 * 0.95) as usize];
674 (hi - lo).max(0.0)
675 }
676}
677
678pub struct TransientAnalysis;
684
685impl TransientAnalysis {
686 pub fn attack_time(signal: &[f32], sample_rate: f32) -> f32 {
688 if signal.is_empty() { return 0.0; }
689 let peak = signal.iter().map(|&x| x.abs()).fold(0.0f32, f32::max);
690 if peak < 1e-10 { return 0.0; }
691 let target = peak * 0.9;
692 for (i, &s) in signal.iter().enumerate() {
693 if s.abs() >= target {
694 return i as f32 / sample_rate;
695 }
696 }
697 signal.len() as f32 / sample_rate
698 }
699
700 pub fn release_time(signal: &[f32], sample_rate: f32) -> f32 {
702 if signal.is_empty() { return 0.0; }
703 let peak = signal.iter().map(|&x| x.abs()).fold(0.0f32, f32::max);
704 if peak < 1e-10 { return 0.0; }
705 let target = peak * 0.1;
706 let peak_idx = signal.iter().enumerate()
708 .max_by(|(_, a), (_, b)| a.abs().partial_cmp(&b.abs()).unwrap())
709 .map(|(i, _)| i)
710 .unwrap_or(0);
711 for i in peak_idx..signal.len() {
713 if signal[i].abs() <= target {
714 return (i - peak_idx) as f32 / sample_rate;
715 }
716 }
717 (signal.len() - peak_idx) as f32 / sample_rate
718 }
719
720 pub fn transient_to_steady_ratio(signal: &[f32]) -> f32 {
722 if signal.len() < 10 { return 0.0; }
723 let transient_end = signal.len() / 10;
724 let transient_energy: f32 = signal[..transient_end].iter().map(|&x| x * x).sum();
725 let steady_energy: f32 = signal[transient_end..].iter().map(|&x| x * x).sum();
726 if steady_energy < 1e-15 { return f32::INFINITY; }
727 transient_energy / steady_energy
728 }
729
730 pub fn detect_transients(signal: &[f32], sample_rate: f32, sensitivity: f32) -> Vec<f32> {
733 let block_size = 256usize;
734 let mut prev_energy = 0.0f32;
735 let mut transients = Vec::new();
736 for (i, chunk) in signal.chunks(block_size).enumerate() {
737 let energy: f32 = chunk.iter().map(|&x| x * x).sum::<f32>() / chunk.len() as f32;
738 if energy > prev_energy * (1.0 + sensitivity) && energy > 1e-6 {
739 transients.push(i as f32 * block_size as f32 / sample_rate);
740 }
741 prev_energy = energy;
742 }
743 transients
744 }
745
746 pub fn energy_envelope(signal: &[f32], sample_rate: f32, window_ms: f32) -> Vec<f32> {
748 let win_samples = ((window_ms / 1000.0) * sample_rate) as usize;
749 let win_samples = win_samples.max(1);
750 let step = win_samples / 2;
751 let mut env = Vec::new();
752 let mut i = 0;
753 while i + win_samples <= signal.len() {
754 let energy: f32 = signal[i..i + win_samples].iter().map(|&x| x * x).sum::<f32>()
755 / win_samples as f32;
756 env.push(linear_to_db(energy.sqrt()));
757 i += step;
758 }
759 env
760 }
761}
762
763pub struct HarmonicAnalyzer;
769
770impl HarmonicAnalyzer {
771 pub fn fundamental(spectrum: &Spectrum, sample_rate: f32) -> f32 {
773 spectrum.dominant_frequency(sample_rate)
774 }
775
776 pub fn harmonics(spectrum: &Spectrum, fundamental_hz: f32, sample_rate: f32, n: usize) -> Vec<f32> {
778 let bin_res = sample_rate / spectrum.fft_size as f32;
779 let mut result = Vec::with_capacity(n);
780 for h in 1..=n {
781 let target_hz = fundamental_hz * h as f32;
782 let target_bin = (target_hz / bin_res).round() as usize;
783 let search = 3usize;
785 let lo = target_bin.saturating_sub(search);
786 let hi = (target_bin + search + 1).min(spectrum.num_bins());
787 let peak_mag = if lo < hi {
788 spectrum.bins[lo..hi].iter().map(|c| c.norm()).fold(0.0f32, f32::max)
789 } else {
790 0.0
791 };
792 result.push(peak_mag);
793 }
794 result
795 }
796
797 pub fn thd(harmonics: &[f32]) -> f32 {
799 if harmonics.len() < 2 || harmonics[0] < 1e-10 { return 0.0; }
800 let fundamental = harmonics[0];
801 let harmonic_sum_sq: f32 = harmonics[1..].iter().map(|&h| h * h).sum();
802 (harmonic_sum_sq.sqrt() / fundamental).min(100.0)
803 }
804
805 pub fn thd_percent(harmonics: &[f32]) -> f32 {
807 Self::thd(harmonics) * 100.0
808 }
809
810 pub fn inharmonicity(spectrum: &Spectrum, fundamental_hz: f32, sample_rate: f32, n: usize) -> f32 {
812 let bin_res = sample_rate / spectrum.fft_size as f32;
813 if fundamental_hz < 1.0 { return 0.0; }
814 let mut total_deviation = 0.0f32;
815 let mut count = 0usize;
816 for h in 2..=n {
817 let ideal_hz = fundamental_hz * h as f32;
818 let ideal_bin = (ideal_hz / bin_res).round() as usize;
819 let search = 5usize;
820 let lo = ideal_bin.saturating_sub(search);
821 let hi = (ideal_bin + search + 1).min(spectrum.num_bins());
822 if lo >= hi { continue; }
823 let (peak_k, _) = spectrum.bins[lo..hi].iter().enumerate()
825 .max_by(|(_, a), (_, b)| a.norm().partial_cmp(&b.norm()).unwrap())
826 .map(|(k, c)| (k + lo, c))
827 .unwrap_or((ideal_bin, &Complex32::zero()));
828 let actual_hz = spectrum.frequency_of_bin(peak_k, sample_rate);
829 let deviation = ((actual_hz - ideal_hz) / fundamental_hz).abs();
830 total_deviation += deviation;
831 count += 1;
832 }
833 if count == 0 { 0.0 } else { total_deviation / count as f32 }
834 }
835
836 pub fn spectral_centroid(spectrum: &Spectrum, sample_rate: f32) -> f32 {
838 spectrum.spectral_centroid(sample_rate)
839 }
840
841 pub fn odd_even_ratio(harmonics: &[f32]) -> f32 {
843 if harmonics.len() < 2 { return 1.0; }
844 let odd: f32 = harmonics.iter().step_by(2).map(|&h| h * h).sum::<f32>().sqrt();
845 let even: f32 = harmonics.iter().skip(1).step_by(2).map(|&h| h * h).sum::<f32>().sqrt();
846 if even < 1e-10 { f32::INFINITY } else { odd / even }
847 }
848}
849
850pub struct Correlogram {
856 frames: Vec<Vec<f32>>,
857 max_lag: usize,
858}
859
860impl Correlogram {
861 pub fn new(max_lag: usize) -> Self {
862 Self { frames: Vec::new(), max_lag }
863 }
864
865 pub fn push_frame(&mut self, frame: &[f32]) {
867 let ac = Autocorrelation::compute(frame);
868 let row: Vec<f32> = ac[..ac.len().min(self.max_lag + 1)].to_vec();
869 self.frames.push(row);
870 }
871
872 pub fn get_frame(&self, t: usize) -> Option<&Vec<f32>> {
874 self.frames.get(t)
875 }
876
877 pub fn num_frames(&self) -> usize { self.frames.len() }
879
880 pub fn max_lag(&self) -> usize { self.max_lag }
882
883 pub fn average(&self) -> Vec<f32> {
885 if self.frames.is_empty() { return Vec::new(); }
886 let n_lags = self.frames[0].len();
887 let mut avg = vec![0.0f32; n_lags];
888 for frame in &self.frames {
889 for (lag, &v) in frame.iter().enumerate().take(n_lags) {
890 avg[lag] += v;
891 }
892 }
893 let n = self.frames.len() as f32;
894 for a in avg.iter_mut() { *a /= n; }
895 avg
896 }
897
898 pub fn clear(&mut self) { self.frames.clear(); }
900}
901
902pub struct DynamicsAnalyzer;
908
909impl DynamicsAnalyzer {
910 pub fn histogram(signal: &[f32], bins: usize) -> Vec<f32> {
912 let mut hist = vec![0u32; bins];
913 for &s in signal {
914 let clamped = s.max(-1.0).min(1.0);
915 let idx = ((clamped + 1.0) / 2.0 * bins as f32) as usize;
916 let idx = idx.min(bins - 1);
917 hist[idx] += 1;
918 }
919 let total = signal.len().max(1) as f32;
920 hist.iter().map(|&c| c as f32 / total).collect()
921 }
922
923 pub fn percentile(signal: &[f32], p: f32) -> f32 {
925 if signal.is_empty() { return 0.0; }
926 let mut sorted: Vec<f32> = signal.iter().map(|&x| x.abs()).collect();
927 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
928 let idx = (p / 100.0 * (sorted.len() - 1) as f32).round() as usize;
929 sorted[idx.min(sorted.len() - 1)]
930 }
931
932 pub fn lufs_integrated(signal: &[f32], sample_rate: f32) -> f32 {
934 let mut meter = Lufs::new(sample_rate);
935 meter.push(signal);
936 meter.integrated_lufs()
937 }
938
939 pub fn dc_offset(signal: &[f32]) -> f32 {
941 if signal.is_empty() { return 0.0; }
942 signal.iter().sum::<f32>() / signal.len() as f32
943 }
944
945 pub fn remove_dc_offset(signal: &mut [f32]) {
947 let dc = Self::dc_offset(signal);
948 for s in signal.iter_mut() { *s -= dc; }
949 }
950
951 pub fn pdf(signal: &[f32], bins: usize) -> Vec<f32> {
953 let hist = Self::histogram(signal, bins);
954 let mut smoothed = hist.clone();
956 let kernel = [0.25, 0.5, 0.25];
957 for i in 1..hist.len().saturating_sub(1) {
958 smoothed[i] = kernel[0] * hist[i - 1] + kernel[1] * hist[i] + kernel[2] * hist[i + 1];
959 }
960 let sum: f32 = smoothed.iter().sum();
962 if sum > 1e-10 { for s in smoothed.iter_mut() { *s /= sum; } }
963 smoothed
964 }
965
966 pub fn sample_entropy(signal: &[f32], m: usize, r: f32) -> f32 {
968 let n = signal.len();
969 if n < m + 2 { return 0.0; }
970 let mut a = 0u64; let mut b = 0u64; for i in 0..n - m {
973 for j in i + 1..n - m {
974 let match_m = (0..m).all(|k| {
975 (signal[i + k] - signal[j + k]).abs() <= r
976 });
977 if match_m {
978 b += 1;
979 if (signal[i + m] - signal[j + m]).abs() <= r {
980 a += 1;
981 }
982 }
983 }
984 }
985 if b == 0 { return 0.0; }
986 -((a as f32 / b as f32).max(1e-10)).ln()
987 }
988}
989
990pub struct SignalSimilarity;
996
997impl SignalSimilarity {
998 pub fn cross_correlation(a: &[f32], b: &[f32]) -> Vec<f32> {
1000 let b_rev: Vec<f32> = b.iter().rev().copied().collect();
1001 let out_len = a.len() + b.len() - 1;
1003 let n = next_power_of_two(out_len);
1004 let mut fa: Vec<Complex32> = a.iter().map(|&x| Complex32::new(x, 0.0)).collect();
1005 fa.resize(n, Complex32::zero());
1006 let mut fb: Vec<Complex32> = b_rev.iter().map(|&x| Complex32::new(x, 0.0)).collect();
1007 fb.resize(n, Complex32::zero());
1008 use super::fft::Fft;
1009 Fft::forward(&mut fa);
1010 Fft::forward(&mut fb);
1011 for (ai, bi) in fa.iter_mut().zip(fb.iter()) { *ai = *ai * *bi; }
1012 Fft::inverse(&mut fa);
1013 fa[..out_len].iter().map(|c| c.re).collect()
1014 }
1015
1016 pub fn normalized_cross_correlation(a: &[f32], b: &[f32]) -> f32 {
1018 let xcorr = Self::cross_correlation(a, b);
1019 let peak = xcorr.iter().map(|&x| x.abs()).fold(0.0f32, f32::max);
1020 let norm_a: f32 = a.iter().map(|&x| x * x).sum::<f32>().sqrt();
1021 let norm_b: f32 = b.iter().map(|&x| x * x).sum::<f32>().sqrt();
1022 let denom = norm_a * norm_b;
1023 if denom < 1e-10 { 0.0 } else { peak / denom }
1024 }
1025
1026 pub fn dtw_distance(a: &[f32], b: &[f32]) -> f32 {
1028 let n = a.len();
1029 let m = b.len();
1030 if n == 0 || m == 0 { return f32::INFINITY; }
1031 let mut dtw = vec![vec![f32::INFINITY; m]; n];
1032 dtw[0][0] = (a[0] - b[0]).abs();
1033 for i in 1..n {
1034 dtw[i][0] = dtw[i - 1][0] + (a[i] - b[0]).abs();
1035 }
1036 for j in 1..m {
1037 dtw[0][j] = dtw[0][j - 1] + (a[0] - b[j]).abs();
1038 }
1039 for i in 1..n {
1040 for j in 1..m {
1041 let cost = (a[i] - b[j]).abs();
1042 dtw[i][j] = cost + dtw[i - 1][j].min(dtw[i][j - 1]).min(dtw[i - 1][j - 1]);
1043 }
1044 }
1045 dtw[n - 1][m - 1]
1046 }
1047
1048 pub fn dtw_distance_windowed(a: &[f32], b: &[f32], window: usize) -> f32 {
1050 let n = a.len();
1051 let m = b.len();
1052 if n == 0 || m == 0 { return f32::INFINITY; }
1053 let mut dtw = vec![vec![f32::INFINITY; m]; n];
1054 for i in 0..n {
1055 let j_lo = i.saturating_sub(window);
1056 let j_hi = (i + window + 1).min(m);
1057 for j in j_lo..j_hi {
1058 let cost = (a[i] - b[j]).abs();
1059 let prev = if i == 0 && j == 0 { 0.0 }
1060 else if i == 0 { dtw[0][j - 1] }
1061 else if j == 0 { dtw[i - 1][0] }
1062 else { dtw[i - 1][j].min(dtw[i][j - 1]).min(dtw[i - 1][j - 1]) };
1063 dtw[i][j] = cost + prev;
1064 }
1065 }
1066 dtw[n - 1][m - 1]
1067 }
1068
1069 pub fn pearson(a: &[f32], b: &[f32]) -> f32 {
1071 let n = a.len().min(b.len());
1072 if n == 0 { return 0.0; }
1073 let mean_a = a[..n].iter().sum::<f32>() / n as f32;
1074 let mean_b = b[..n].iter().sum::<f32>() / n as f32;
1075 let mut cov = 0.0f32;
1076 let mut var_a = 0.0f32;
1077 let mut var_b = 0.0f32;
1078 for i in 0..n {
1079 let da = a[i] - mean_a;
1080 let db = b[i] - mean_b;
1081 cov += da * db;
1082 var_a += da * da;
1083 var_b += db * db;
1084 }
1085 let denom = (var_a * var_b).sqrt();
1086 if denom < 1e-10 { 0.0 } else { cov / denom }
1087 }
1088
1089 pub fn mse(a: &[f32], b: &[f32]) -> f32 {
1091 let n = a.len().min(b.len());
1092 if n == 0 { return 0.0; }
1093 a[..n].iter().zip(b[..n].iter()).map(|(&x, &y)| (x - y).powi(2)).sum::<f32>() / n as f32
1094 }
1095
1096 pub fn snr_db(signal: &[f32], noise: &[f32]) -> f32 {
1098 let n = signal.len().min(noise.len());
1099 if n == 0 { return 0.0; }
1100 let sig_power: f32 = signal[..n].iter().map(|&x| x * x).sum::<f32>() / n as f32;
1101 let noise_power: f32 = noise[..n].iter().map(|&x| x * x).sum::<f32>() / n as f32;
1102 if noise_power < 1e-30 { return f32::INFINITY; }
1103 10.0 * (sig_power / noise_power).log10()
1104 }
1105}
1106
1107#[cfg(test)]
1112mod tests {
1113 use super::*;
1114 use crate::dsp::SignalGenerator;
1115
1116 fn sine(freq: f32, sr: f32, n: usize) -> Vec<f32> {
1117 (0..n).map(|i| (2.0 * PI * freq * i as f32 / sr).sin()).collect()
1118 }
1119
1120 #[test]
1123 fn test_onset_spectral_flux_finds_onset() {
1124 let sr = 44100.0;
1125 let mut signal = vec![0.0f32; 2048];
1127 signal.extend(sine(440.0, sr, 4096));
1128 let detector = SpectralFluxOnset::new(1.5, 1024, 256);
1129 let onsets = detector.detect(&signal, sr);
1130 assert!(!onsets.is_empty(), "Should detect at least one onset");
1131 }
1132
1133 #[test]
1134 fn test_hfc_onset_basic() {
1135 let sr = 44100.0;
1136 let mut signal = vec![0.0f32; 4096];
1137 signal.extend(sine(440.0, sr, 4096));
1138 let detector = HfcOnset::new(0.3, 1024, 256);
1139 let onsets = detector.detect(&signal, sr);
1140 let _ = onsets;
1142 }
1143
1144 #[test]
1145 fn test_complex_domain_onset_basic() {
1146 let sr = 44100.0;
1147 let mut signal = vec![0.0f32; 4096];
1148 signal.extend(sine(440.0, sr, 4096));
1149 let detector = ComplexDomainOnset::new(0.3, 1024, 256);
1150 let onsets = detector.detect(&signal, sr);
1151 let _ = onsets;
1152 }
1153
1154 #[test]
1157 fn test_beat_tracker_estimate_tempo_basic() {
1158 let sr = 44100.0;
1159 let mut signal = vec![0.0f32; sr as usize * 4];
1161 let bpm = 120.0f32;
1162 let beat_samples = (60.0 * sr / bpm) as usize;
1163 for k in (0..signal.len()).step_by(beat_samples) {
1164 if k < signal.len() { signal[k] = 1.0; }
1165 }
1166 let estimated = BeatTracker::estimate_tempo(&signal, sr);
1167 assert!(estimated > 50.0 && estimated < 250.0, "estimated_bpm={}", estimated);
1169 }
1170
1171 #[test]
1172 fn test_beat_tracker_detect_beats() {
1173 let sr = 44100.0;
1174 let mut signal = vec![0.0f32; sr as usize * 2];
1175 let beat_samples = (60.0 * sr / 120.0) as usize;
1176 for k in (0..signal.len()).step_by(beat_samples) {
1177 if k < signal.len() { signal[k] = 1.0; }
1178 }
1179 let beats = BeatTracker::detect_beats(&signal, sr);
1180 assert!(beats.len() > 0);
1182 }
1183
1184 #[test]
1187 fn test_pitch_detector_yin() {
1188 let sr = 44100.0;
1189 let sig = sine(440.0, sr, 4096);
1190 let pitch = PitchDetector::yin(&sig, sr, 20.0, 2000.0);
1191 assert!(pitch.is_some());
1192 let p = pitch.unwrap();
1193 assert!((p - 440.0).abs() < 20.0, "pitch={}", p);
1194 }
1195
1196 #[test]
1197 fn test_pitch_detector_autocorr() {
1198 let sr = 44100.0;
1199 let sig = sine(220.0, sr, 4096);
1200 let pitch = PitchDetector::autocorr(&sig, sr);
1201 if let Some(p) = pitch {
1202 assert!((p - 220.0).abs() < 40.0, "pitch={}", p);
1203 }
1204 }
1205
1206 #[test]
1207 fn test_note_name_a4() {
1208 let midi = PitchDetector::midi_note(440.0);
1209 let name = PitchDetector::note_name(midi);
1210 assert_eq!(name, "A4");
1211 }
1212
1213 #[test]
1214 fn test_note_name_c4() {
1215 let name = PitchDetector::note_name(60.0);
1216 assert_eq!(name, "C4");
1217 }
1218
1219 #[test]
1222 fn test_rms_meter_converges() {
1223 let sr = 44100.0;
1224 let mut rms = Rms::new(100.0, sr);
1225 let sig = sine(440.0, sr, 44100);
1226 for &s in &sig {
1227 rms.process(s);
1228 }
1229 let level = rms.level();
1230 assert!((level - 0.707).abs() < 0.05, "level={}", level);
1232 }
1233
1234 #[test]
1235 fn test_leq() {
1236 let mut leq = Leq::new();
1237 let sig = vec![0.5f32; 44100];
1238 leq.push(&sig);
1239 let db = leq.leq_db();
1240 assert!((db - (-6.0206)).abs() < 0.1, "db={}", db);
1242 }
1243
1244 #[test]
1245 fn test_lufs_push_no_crash() {
1246 let mut lufs = Lufs::new(44100.0);
1247 let sig: Vec<f32> = (0..44100 * 5).map(|i| {
1248 (2.0 * PI * 1000.0 * i as f32 / 44100.0).sin() * 0.5
1249 }).collect();
1250 lufs.push(&sig);
1251 let db = lufs.integrated_lufs();
1252 assert!(db.is_finite() || db == -f32::INFINITY);
1254 }
1255
1256 #[test]
1259 fn test_crest_factor_sine() {
1260 let sig = sine(440.0, 44100.0, 44100);
1261 let cf = DynamicRange::crest_factor(&sig);
1262 assert!((cf - 2.0f32.sqrt()).abs() < 0.05, "cf={}", cf);
1264 }
1265
1266 #[test]
1269 fn test_attack_time() {
1270 let sr = 44100.0;
1271 let n = sr as usize;
1273 let sig: Vec<f32> = (0..n).map(|i| {
1274 if i < (0.1 * sr) as usize { i as f32 / (0.1 * sr) } else { 1.0 }
1275 }).collect();
1276 let at = TransientAnalysis::attack_time(&sig, sr);
1277 assert!(at >= 0.0 && at <= 0.15, "attack_time={}", at);
1278 }
1279
1280 #[test]
1281 fn test_transient_to_steady_ratio() {
1282 let mut sig = vec![1.0f32; 100];
1284 sig.extend(vec![0.001f32; 900]);
1285 let ratio = TransientAnalysis::transient_to_steady_ratio(&sig);
1286 assert!(ratio > 1.0, "ratio={}", ratio);
1287 }
1288
1289 #[test]
1292 fn test_harmonic_analyzer_thd() {
1293 let harmonics = vec![1.0, 0.0, 0.0, 0.0];
1295 let thd = HarmonicAnalyzer::thd(&harmonics);
1296 assert!(thd.abs() < 1e-5);
1297 }
1298
1299 #[test]
1300 fn test_harmonic_analyzer_thd_nonzero() {
1301 let harmonics = vec![1.0, 0.1, 0.05, 0.02];
1302 let thd = HarmonicAnalyzer::thd(&harmonics);
1303 assert!(thd > 0.0 && thd < 1.0);
1304 }
1305
1306 #[test]
1309 fn test_correlogram_frames() {
1310 let mut corr = Correlogram::new(64);
1311 let frame = sine(440.0, 44100.0, 256);
1312 corr.push_frame(&frame);
1313 corr.push_frame(&frame);
1314 assert_eq!(corr.num_frames(), 2);
1315 let avg = corr.average();
1316 assert!(!avg.is_empty());
1317 }
1318
1319 #[test]
1322 fn test_histogram_sums_to_one() {
1323 let sig = sine(440.0, 44100.0, 4410);
1324 let hist = DynamicsAnalyzer::histogram(&sig, 32);
1325 let sum: f32 = hist.iter().sum();
1326 assert!((sum - 1.0).abs() < 0.01, "sum={}", sum);
1327 }
1328
1329 #[test]
1330 fn test_percentile_50th() {
1331 let sig: Vec<f32> = (0..100).map(|i| i as f32 / 100.0).collect();
1332 let p50 = DynamicsAnalyzer::percentile(&sig, 50.0);
1333 assert!((p50 - 0.5).abs() < 0.1, "p50={}", p50);
1334 }
1335
1336 #[test]
1339 fn test_ncc_identical() {
1340 let sig = sine(440.0, 44100.0, 1024);
1341 let ncc = SignalSimilarity::normalized_cross_correlation(&sig, &sig);
1342 assert!(ncc > 0.9, "ncc={}", ncc);
1343 }
1344
1345 #[test]
1346 fn test_dtw_self_zero() {
1347 let sig: Vec<f32> = vec![0.1, 0.2, 0.3, 0.4];
1348 let d = SignalSimilarity::dtw_distance(&sig, &sig);
1349 assert!(d.abs() < 1e-5, "dtw_self={}", d);
1350 }
1351
1352 #[test]
1353 fn test_dtw_different_lengths() {
1354 let a: Vec<f32> = vec![0.0, 1.0, 0.0];
1355 let b: Vec<f32> = vec![0.0, 1.0, 1.0, 0.0];
1356 let d = SignalSimilarity::dtw_distance(&a, &b);
1357 assert!(d >= 0.0);
1358 }
1359
1360 #[test]
1361 fn test_pearson_perfect_positive() {
1362 let a: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0];
1363 let b: Vec<f32> = vec![2.0, 4.0, 6.0, 8.0]; let p = SignalSimilarity::pearson(&a, &b);
1365 assert!((p - 1.0).abs() < 1e-5, "pearson={}", p);
1366 }
1367
1368 #[test]
1369 fn test_mse_zero_identical() {
1370 let sig: Vec<f32> = vec![1.0, -0.5, 0.3];
1371 let mse = SignalSimilarity::mse(&sig, &sig);
1372 assert!(mse.abs() < 1e-10);
1373 }
1374
1375 #[test]
1376 fn test_snr_basic() {
1377 let signal: Vec<f32> = vec![1.0; 100];
1378 let noise: Vec<f32> = vec![0.1; 100]; let snr = SignalSimilarity::snr_db(&signal, &noise);
1380 assert!((snr - 20.0).abs() < 0.1, "snr={}", snr);
1381 }
1382}