Skip to main content

proof_engine/dsp/
mod.rs

1//! Digital Signal Processing — module root
2//!
3//! Provides signal buffers, window functions, envelopes, peak metering,
4//! signal generation, and re-exports the FFT, filter, and analysis sub-modules.
5
6pub mod fft;
7pub mod filters;
8pub mod analysis;
9
10pub use fft::{
11    Complex32, Fft, RealFft, FftPlanner, FftPlan, Spectrum, Stft, StftConfig,
12    Cqt, Autocorrelation, MelFilterbank, Mfcc, Chroma,
13};
14pub use filters::{
15    Biquad, BiquadType, BiquadDesign, FilterChain, Butterworth, Chebyshev1, Bessel,
16    FirFilter, FirDesign, Convolution, OlaConvolver,
17    SvfFilter, SvfMode, CombFilter, CombMode, AllpassDelay,
18    MovingAverage, KalmanFilter1D, PllFilter,
19};
20pub use analysis::{
21    OnsetDetector, SpectralFluxOnset, HfcOnset, ComplexDomainOnset,
22    BeatTracker, PitchDetector, LoudnessMeters, Rms, Leq, Lufs, DynamicRange,
23    TransientAnalysis, HarmonicAnalyzer, Correlogram, DynamicsAnalyzer, SignalSimilarity,
24};
25
26use std::f32::consts::PI;
27
28// ---------------------------------------------------------------------------
29// Signal<T>
30// ---------------------------------------------------------------------------
31
32/// An owned time-series buffer with sample-rate metadata.
33#[derive(Debug, Clone)]
34pub struct Signal<T: Clone> {
35    pub samples: Vec<T>,
36    pub sample_rate: f32,
37}
38
39impl<T: Clone + Default> Signal<T> {
40    /// Create a new signal from a sample buffer and a sample rate.
41    pub fn new(samples: Vec<T>, sample_rate: f32) -> Self {
42        assert!(sample_rate > 0.0, "sample_rate must be positive");
43        Self { samples, sample_rate }
44    }
45
46    /// Duration of the signal in seconds.
47    pub fn duration_secs(&self) -> f32 {
48        self.samples.len() as f32 / self.sample_rate
49    }
50
51    /// Number of samples.
52    pub fn len(&self) -> usize {
53        self.samples.len()
54    }
55
56    pub fn is_empty(&self) -> bool {
57        self.samples.is_empty()
58    }
59
60    /// Extract a sub-window starting at `start` with length `len`.
61    /// Clamps at the end of the buffer.
62    pub fn window(&self, start: usize, len: usize) -> Signal<T> {
63        let end = (start + len).min(self.samples.len());
64        let start = start.min(end);
65        Signal {
66            samples: self.samples[start..end].to_vec(),
67            sample_rate: self.sample_rate,
68        }
69    }
70}
71
72impl Signal<f32> {
73    /// Downsample by integer factor (simple decimation, no anti-alias filter).
74    pub fn downsample(&self, factor: usize) -> Signal<f32> {
75        assert!(factor >= 1);
76        let samples: Vec<f32> = self.samples.iter().step_by(factor).copied().collect();
77        Signal {
78            samples,
79            sample_rate: self.sample_rate / factor as f32,
80        }
81    }
82
83    /// Upsample by integer factor (zero insertion).
84    pub fn upsample(&self, factor: usize) -> Signal<f32> {
85        assert!(factor >= 1);
86        let mut samples = vec![0.0f32; self.samples.len() * factor];
87        for (i, &s) in self.samples.iter().enumerate() {
88            samples[i * factor] = s;
89        }
90        Signal {
91            samples,
92            sample_rate: self.sample_rate * factor as f32,
93        }
94    }
95
96    /// Resample to a new sample rate using linear interpolation.
97    pub fn resample(&self, new_rate: f32) -> Signal<f32> {
98        assert!(new_rate > 0.0);
99        if (new_rate - self.sample_rate).abs() < 1e-3 {
100            return self.clone();
101        }
102        let ratio = self.sample_rate / new_rate;
103        let new_len = (self.samples.len() as f32 / ratio).round() as usize;
104        let mut out = Vec::with_capacity(new_len);
105        for i in 0..new_len {
106            let pos = i as f32 * ratio;
107            let idx = pos as usize;
108            let frac = pos - idx as f32;
109            let a = self.samples.get(idx).copied().unwrap_or(0.0);
110            let b = self.samples.get(idx + 1).copied().unwrap_or(a);
111            out.push(a + frac * (b - a));
112        }
113        Signal { samples: out, sample_rate: new_rate }
114    }
115
116    /// Root-mean-square amplitude.
117    pub fn rms(&self) -> f32 {
118        if self.samples.is_empty() { return 0.0; }
119        let sum_sq: f32 = self.samples.iter().map(|&x| x * x).sum();
120        (sum_sq / self.samples.len() as f32).sqrt()
121    }
122
123    /// Peak absolute value.
124    pub fn peak(&self) -> f32 {
125        self.samples.iter().map(|&x| x.abs()).fold(0.0f32, f32::max)
126    }
127
128    /// Zero-crossing rate (crossings per second).
129    pub fn zero_crossing_rate(&self) -> f32 {
130        if self.samples.len() < 2 { return 0.0; }
131        let crossings = self.samples.windows(2)
132            .filter(|w| (w[0] >= 0.0) != (w[1] >= 0.0))
133            .count();
134        crossings as f32 * self.sample_rate / self.samples.len() as f32
135    }
136
137    /// Total energy (sum of squared samples).
138    pub fn energy(&self) -> f32 {
139        self.samples.iter().map(|&x| x * x).sum()
140    }
141
142    /// Normalize to peak amplitude of 1.0 (in place).
143    pub fn normalize(&mut self) {
144        let pk = self.peak();
145        if pk > 1e-10 {
146            for s in self.samples.iter_mut() {
147                *s /= pk;
148            }
149        }
150    }
151
152    /// Mix another signal into this one (adds samples).
153    pub fn mix_in(&mut self, other: &Signal<f32>, gain: f32) {
154        let len = self.samples.len().min(other.samples.len());
155        for i in 0..len {
156            self.samples[i] += other.samples[i] * gain;
157        }
158    }
159}
160
161// ---------------------------------------------------------------------------
162// ComplexSignal
163// ---------------------------------------------------------------------------
164
165/// A complex-valued signal (e.g. FFT output).
166#[derive(Debug, Clone)]
167pub struct ComplexSignal {
168    pub samples: Vec<Complex32>,
169    pub sample_rate: f32,
170}
171
172impl ComplexSignal {
173    pub fn new(samples: Vec<Complex32>, sample_rate: f32) -> Self {
174        Self { samples, sample_rate }
175    }
176
177    pub fn magnitude_signal(&self) -> Signal<f32> {
178        Signal {
179            samples: self.samples.iter().map(|c| c.norm()).collect(),
180            sample_rate: self.sample_rate,
181        }
182    }
183
184    pub fn phase_signal(&self) -> Signal<f32> {
185        Signal {
186            samples: self.samples.iter().map(|c| c.arg()).collect(),
187            sample_rate: self.sample_rate,
188        }
189    }
190}
191
192// ---------------------------------------------------------------------------
193// SignalGenerator
194// ---------------------------------------------------------------------------
195
196/// Factory for generating standard test/synthesis signals.
197pub struct SignalGenerator;
198
199impl SignalGenerator {
200    /// Pure sine wave.
201    pub fn sine(freq_hz: f32, amplitude: f32, duration_secs: f32, sample_rate: f32) -> Signal<f32> {
202        let n = (duration_secs * sample_rate) as usize;
203        let samples: Vec<f32> = (0..n)
204            .map(|i| amplitude * (2.0 * PI * freq_hz * i as f32 / sample_rate).sin())
205            .collect();
206        Signal::new(samples, sample_rate)
207    }
208
209    /// Square wave via Fourier synthesis (up to `harmonics` odd harmonics).
210    pub fn square(freq_hz: f32, amplitude: f32, duration_secs: f32, sample_rate: f32) -> Signal<f32> {
211        let n = (duration_secs * sample_rate) as usize;
212        let harmonics = 50usize;
213        let samples: Vec<f32> = (0..n)
214            .map(|i| {
215                let t = i as f32 / sample_rate;
216                let mut v = 0.0f32;
217                for k in 0..harmonics {
218                    let h = 2 * k + 1;
219                    v += (2.0 * PI * freq_hz * h as f32 * t).sin() / h as f32;
220                }
221                amplitude * (4.0 / PI) * v
222            })
223            .collect();
224        Signal::new(samples, sample_rate)
225    }
226
227    /// Triangle wave.
228    pub fn triangle(freq_hz: f32, amplitude: f32, duration_secs: f32, sample_rate: f32) -> Signal<f32> {
229        let n = (duration_secs * sample_rate) as usize;
230        let period = sample_rate / freq_hz;
231        let samples: Vec<f32> = (0..n)
232            .map(|i| {
233                let phase = (i as f32 % period) / period; // 0..1
234                let v = if phase < 0.5 {
235                    4.0 * phase - 1.0
236                } else {
237                    3.0 - 4.0 * phase
238                };
239                amplitude * v
240            })
241            .collect();
242        Signal::new(samples, sample_rate)
243    }
244
245    /// Sawtooth wave.
246    pub fn sawtooth(freq_hz: f32, amplitude: f32, duration_secs: f32, sample_rate: f32) -> Signal<f32> {
247        let n = (duration_secs * sample_rate) as usize;
248        let period = sample_rate / freq_hz;
249        let samples: Vec<f32> = (0..n)
250            .map(|i| {
251                let phase = (i as f32 % period) / period; // 0..1
252                amplitude * (2.0 * phase - 1.0)
253            })
254            .collect();
255        Signal::new(samples, sample_rate)
256    }
257
258    /// White noise (uniform distribution in [-amplitude, amplitude]).
259    pub fn noise(amplitude: f32, duration_secs: f32, sample_rate: f32) -> Signal<f32> {
260        let n = (duration_secs * sample_rate) as usize;
261        // Simple LCG pseudo-random noise
262        let mut state: u32 = 0x12345678;
263        let samples: Vec<f32> = (0..n)
264            .map(|_| {
265                state = state.wrapping_mul(1664525).wrapping_add(1013904223);
266                let norm = (state as f32 / u32::MAX as f32) * 2.0 - 1.0;
267                amplitude * norm
268            })
269            .collect();
270        Signal::new(samples, sample_rate)
271    }
272
273    /// Linear chirp sweeping from `f0_hz` to `f1_hz` over `duration_secs`.
274    pub fn chirp(f0_hz: f32, f1_hz: f32, amplitude: f32, duration_secs: f32, sample_rate: f32) -> Signal<f32> {
275        let n = (duration_secs * sample_rate) as usize;
276        let k = (f1_hz - f0_hz) / (2.0 * duration_secs);
277        let samples: Vec<f32> = (0..n)
278            .map(|i| {
279                let t = i as f32 / sample_rate;
280                let phase = 2.0 * PI * (f0_hz * t + k * t * t);
281                amplitude * phase.sin()
282            })
283            .collect();
284        Signal::new(samples, sample_rate)
285    }
286
287    /// Rectangular pulse: high for `pulse_width_secs` then zero.
288    pub fn pulse(amplitude: f32, pulse_width_secs: f32, duration_secs: f32, sample_rate: f32) -> Signal<f32> {
289        let n = (duration_secs * sample_rate) as usize;
290        let pulse_samples = (pulse_width_secs * sample_rate) as usize;
291        let samples: Vec<f32> = (0..n)
292            .map(|i| if i < pulse_samples { amplitude } else { 0.0 })
293            .collect();
294        Signal::new(samples, sample_rate)
295    }
296
297    /// Unit impulse (Dirac delta approximation): 1.0 at sample 0, 0.0 elsewhere.
298    pub fn impulse(amplitude: f32, duration_secs: f32, sample_rate: f32) -> Signal<f32> {
299        let n = (duration_secs * sample_rate) as usize;
300        let mut samples = vec![0.0f32; n.max(1)];
301        if !samples.is_empty() {
302            samples[0] = amplitude;
303        }
304        Signal::new(samples, sample_rate)
305    }
306
307    /// Gaussian-modulated sine (Gabor atom) for analysis.
308    pub fn gabor(freq_hz: f32, sigma: f32, center_secs: f32, amplitude: f32, duration_secs: f32, sample_rate: f32) -> Signal<f32> {
309        let n = (duration_secs * sample_rate) as usize;
310        let samples: Vec<f32> = (0..n)
311            .map(|i| {
312                let t = i as f32 / sample_rate;
313                let gauss = (-((t - center_secs).powi(2)) / (2.0 * sigma * sigma)).exp();
314                amplitude * gauss * (2.0 * PI * freq_hz * t).sin()
315            })
316            .collect();
317        Signal::new(samples, sample_rate)
318    }
319}
320
321// ---------------------------------------------------------------------------
322// WindowFunction
323// ---------------------------------------------------------------------------
324
325/// Standard window functions for spectral analysis.
326#[derive(Debug, Clone, Copy, PartialEq)]
327pub enum WindowFunction {
328    /// Rectangular / boxcar — no windowing.
329    Rectangular,
330    /// Hann window (raised cosine).
331    Hann,
332    /// Hamming window.
333    Hamming,
334    /// Blackman window.
335    Blackman,
336    /// Kaiser window with parameter β.
337    Kaiser(f32),
338    /// Flat-top window for accurate amplitude measurement.
339    FlatTop,
340    /// Nuttall window (minimum 4-term Blackman-Harris).
341    Nuttall,
342    /// Gaussian window with parameter σ (fraction of half-window).
343    Gaussian(f32),
344    /// Bartlett (triangle) window.
345    Bartlett,
346    /// Welch window.
347    Welch,
348}
349
350impl WindowFunction {
351    /// Compute the window coefficient for sample `n` in a window of length `len`.
352    pub fn coefficient(&self, n: usize, len: usize) -> f32 {
353        if len <= 1 { return 1.0; }
354        let n = n as f32;
355        let n_minus_1 = (len - 1) as f32;
356        match self {
357            WindowFunction::Rectangular => 1.0,
358            WindowFunction::Hann => {
359                0.5 * (1.0 - (2.0 * PI * n / n_minus_1).cos())
360            }
361            WindowFunction::Hamming => {
362                0.54 - 0.46 * (2.0 * PI * n / n_minus_1).cos()
363            }
364            WindowFunction::Blackman => {
365                0.42 - 0.5 * (2.0 * PI * n / n_minus_1).cos()
366                    + 0.08 * (4.0 * PI * n / n_minus_1).cos()
367            }
368            WindowFunction::Kaiser(beta) => {
369                let half = n_minus_1 / 2.0;
370                let arg = beta * (1.0 - ((n - half) / half).powi(2)).sqrt();
371                Self::bessel_i0(arg) / Self::bessel_i0(*beta)
372            }
373            WindowFunction::FlatTop => {
374                let a0 = 0.21557895;
375                let a1 = 0.41663158;
376                let a2 = 0.277263158;
377                let a3 = 0.083578947;
378                let a4 = 0.006947368;
379                a0 - a1 * (2.0 * PI * n / n_minus_1).cos()
380                    + a2 * (4.0 * PI * n / n_minus_1).cos()
381                    - a3 * (6.0 * PI * n / n_minus_1).cos()
382                    + a4 * (8.0 * PI * n / n_minus_1).cos()
383            }
384            WindowFunction::Nuttall => {
385                let a0 = 0.355768;
386                let a1 = 0.487396;
387                let a2 = 0.144232;
388                let a3 = 0.012604;
389                a0 - a1 * (2.0 * PI * n / n_minus_1).cos()
390                    + a2 * (4.0 * PI * n / n_minus_1).cos()
391                    - a3 * (6.0 * PI * n / n_minus_1).cos()
392            }
393            WindowFunction::Gaussian(sigma) => {
394                let half = n_minus_1 / 2.0;
395                let exponent = -0.5 * ((n - half) / (sigma * half)).powi(2);
396                exponent.exp()
397            }
398            WindowFunction::Bartlett => {
399                let half = n_minus_1 / 2.0;
400                1.0 - ((n - half) / half).abs()
401            }
402            WindowFunction::Welch => {
403                let half = n_minus_1 / 2.0;
404                1.0 - ((n - half) / half).powi(2)
405            }
406        }
407    }
408
409    /// Apply the window function in-place to a slice of samples.
410    pub fn apply(&self, samples: &mut [f32]) {
411        let len = samples.len();
412        for (i, s) in samples.iter_mut().enumerate() {
413            *s *= self.coefficient(i, len);
414        }
415    }
416
417    /// Generate the full window vector.
418    pub fn generate(&self, len: usize) -> Vec<f32> {
419        (0..len).map(|i| self.coefficient(i, len)).collect()
420    }
421
422    /// Coherent gain (mean of window coefficients) — used for amplitude correction.
423    pub fn coherent_gain(&self, len: usize) -> f32 {
424        let sum: f32 = (0..len).map(|i| self.coefficient(i, len)).sum();
425        sum / len as f32
426    }
427
428    /// Power gain — RMS of window coefficients.
429    pub fn power_gain(&self, len: usize) -> f32 {
430        let sum_sq: f32 = (0..len).map(|i| {
431            let c = self.coefficient(i, len);
432            c * c
433        }).sum();
434        (sum_sq / len as f32).sqrt()
435    }
436
437    /// Modified Bessel function of the first kind, order 0 (used for Kaiser window).
438    fn bessel_i0(x: f32) -> f32 {
439        // Polynomial approximation
440        let ax = x.abs();
441        if ax < 3.75 {
442            let y = (x / 3.75).powi(2);
443            1.0 + y * (3.5156229 + y * (3.0899424 + y * (1.2067492
444                + y * (0.2659732 + y * (0.0360768 + y * 0.0045813)))))
445        } else {
446            let y = 3.75 / ax;
447            (ax.exp() / ax.sqrt())
448                * (0.39894228 + y * (0.01328592 + y * (0.00225319
449                    + y * (-0.00157565 + y * (0.00916281
450                        + y * (-0.02057706 + y * (0.02635537
451                            + y * (-0.01647633 + y * 0.00392377))))))))
452        }
453    }
454}
455
456// ---------------------------------------------------------------------------
457// Envelope follower
458// ---------------------------------------------------------------------------
459
460/// Amplitude envelope follower with attack, hold, and release stages.
461#[derive(Debug, Clone)]
462pub struct Envelope {
463    /// Attack time constant in seconds.
464    pub attack_secs: f32,
465    /// Release time constant in seconds.
466    pub release_secs: f32,
467    /// Hold time in seconds before release begins.
468    pub hold_secs: f32,
469    sample_rate: f32,
470    // Internal state
471    level: f32,
472    hold_counter: f32,
473    attack_coeff: f32,
474    release_coeff: f32,
475}
476
477impl Envelope {
478    pub fn new(attack_secs: f32, release_secs: f32, hold_secs: f32, sample_rate: f32) -> Self {
479        let attack_coeff = Self::time_to_coeff(attack_secs, sample_rate);
480        let release_coeff = Self::time_to_coeff(release_secs, sample_rate);
481        Self {
482            attack_secs,
483            release_secs,
484            hold_secs,
485            sample_rate,
486            level: 0.0,
487            hold_counter: 0.0,
488            attack_coeff,
489            release_coeff,
490        }
491    }
492
493    fn time_to_coeff(time_secs: f32, sample_rate: f32) -> f32 {
494        if time_secs <= 0.0 { return 0.0; }
495        (-1.0 / (time_secs * sample_rate)).exp()
496    }
497
498    /// Process one sample, return the envelope level.
499    pub fn process(&mut self, input: f32) -> f32 {
500        let abs_in = input.abs();
501        if abs_in >= self.level {
502            // Attack
503            self.level = self.attack_coeff * self.level + (1.0 - self.attack_coeff) * abs_in;
504            self.hold_counter = self.hold_secs * self.sample_rate;
505        } else if self.hold_counter > 0.0 {
506            // Hold — level stays put
507            self.hold_counter -= 1.0;
508        } else {
509            // Release
510            self.level = self.release_coeff * self.level + (1.0 - self.release_coeff) * abs_in;
511        }
512        self.level
513    }
514
515    /// Process a buffer, returning the envelope for each sample.
516    pub fn process_buffer(&mut self, buf: &[f32]) -> Vec<f32> {
517        buf.iter().map(|&x| self.process(x)).collect()
518    }
519
520    /// Reset the envelope state.
521    pub fn reset(&mut self) {
522        self.level = 0.0;
523        self.hold_counter = 0.0;
524    }
525
526    /// Current envelope level.
527    pub fn level(&self) -> f32 {
528        self.level
529    }
530}
531
532// ---------------------------------------------------------------------------
533// PeakMeter
534// ---------------------------------------------------------------------------
535
536/// dBFS peak meter with configurable hold time and fallback rate.
537#[derive(Debug, Clone)]
538pub struct PeakMeter {
539    /// Hold time in seconds before the peak indicator starts falling.
540    pub hold_secs: f32,
541    /// Fall rate in dB per second.
542    pub fallback_db_per_sec: f32,
543    sample_rate: f32,
544    peak_linear: f32,
545    hold_counter: f32,
546    display_level_db: f32,
547    clip: bool,
548}
549
550impl PeakMeter {
551    pub fn new(hold_secs: f32, fallback_db_per_sec: f32, sample_rate: f32) -> Self {
552        Self {
553            hold_secs,
554            fallback_db_per_sec,
555            sample_rate,
556            peak_linear: 0.0,
557            hold_counter: 0.0,
558            display_level_db: -f32::INFINITY,
559            clip: false,
560        }
561    }
562
563    /// Feed a block of samples. Updates the peak display.
564    pub fn process(&mut self, buf: &[f32]) {
565        let block_peak = buf.iter().map(|&x| x.abs()).fold(0.0f32, f32::max);
566        if block_peak >= 1.0 {
567            self.clip = true;
568        }
569        if block_peak >= self.peak_linear {
570            self.peak_linear = block_peak;
571            self.hold_counter = self.hold_secs * self.sample_rate;
572        } else if self.hold_counter > 0.0 {
573            self.hold_counter -= buf.len() as f32;
574        } else {
575            // Fall back at fallback_db_per_sec
576            let fall_db = self.fallback_db_per_sec * buf.len() as f32 / self.sample_rate;
577            let current_db = linear_to_db(self.peak_linear).max(-144.0);
578            let new_db = current_db - fall_db;
579            self.peak_linear = db_to_linear(new_db).max(0.0);
580        }
581        self.display_level_db = linear_to_db(self.peak_linear);
582    }
583
584    /// Current peak level in dBFS.
585    pub fn peak_db(&self) -> f32 {
586        self.display_level_db
587    }
588
589    /// Returns true if the signal has clipped (exceeded 0 dBFS).
590    pub fn is_clipping(&self) -> bool {
591        self.clip
592    }
593
594    /// Reset clip indicator.
595    pub fn reset_clip(&mut self) {
596        self.clip = false;
597    }
598
599    /// Reset all state.
600    pub fn reset(&mut self) {
601        self.peak_linear = 0.0;
602        self.hold_counter = 0.0;
603        self.display_level_db = -f32::INFINITY;
604        self.clip = false;
605    }
606}
607
608// ---------------------------------------------------------------------------
609// Utility functions
610// ---------------------------------------------------------------------------
611
612/// Convert linear amplitude to dBFS.
613#[inline]
614pub fn linear_to_db(linear: f32) -> f32 {
615    if linear <= 0.0 { return -f32::INFINITY; }
616    20.0 * linear.log10()
617}
618
619/// Convert dBFS to linear amplitude.
620#[inline]
621pub fn db_to_linear(db: f32) -> f32 {
622    10.0f32.powf(db / 20.0)
623}
624
625/// Next power of two >= n.
626#[inline]
627pub fn next_power_of_two(n: usize) -> usize {
628    if n <= 1 { return 1; }
629    let mut p = 1usize;
630    while p < n { p <<= 1; }
631    p
632}
633
634/// Check if n is a power of two.
635#[inline]
636pub fn is_power_of_two(n: usize) -> bool {
637    n > 0 && (n & (n - 1)) == 0
638}
639
640/// Sinc function (normalized): sinc(x) = sin(π·x) / (π·x).
641#[inline]
642pub fn sinc(x: f32) -> f32 {
643    if x.abs() < 1e-10 { return 1.0; }
644    let px = PI * x;
645    px.sin() / px
646}
647
648/// Frequency in Hz to MIDI note number (A4 = 69, 440 Hz).
649#[inline]
650pub fn freq_to_midi(freq: f32) -> f32 {
651    69.0 + 12.0 * (freq / 440.0).log2()
652}
653
654/// MIDI note number to frequency in Hz.
655#[inline]
656pub fn midi_to_freq(midi: f32) -> f32 {
657    440.0 * 2.0f32.powf((midi - 69.0) / 12.0)
658}
659
660/// Convert frequency to Mel scale.
661#[inline]
662pub fn hz_to_mel(hz: f32) -> f32 {
663    2595.0 * (1.0 + hz / 700.0).log10()
664}
665
666/// Convert Mel scale to frequency.
667#[inline]
668pub fn mel_to_hz(mel: f32) -> f32 {
669    700.0 * (10.0f32.powf(mel / 2595.0) - 1.0)
670}
671
672// ---------------------------------------------------------------------------
673// Tests
674// ---------------------------------------------------------------------------
675
676#[cfg(test)]
677mod tests {
678    use super::*;
679
680    #[test]
681    fn test_signal_new_and_properties() {
682        let sig = Signal::new(vec![0.0f32; 44100], 44100.0);
683        assert_eq!(sig.len(), 44100);
684        assert!((sig.duration_secs() - 1.0).abs() < 1e-5);
685    }
686
687    #[test]
688    fn test_signal_window() {
689        let sig = Signal::new((0..100).map(|i| i as f32).collect(), 100.0);
690        let w = sig.window(10, 20);
691        assert_eq!(w.len(), 20);
692        assert!((w.samples[0] - 10.0).abs() < 1e-5);
693    }
694
695    #[test]
696    fn test_signal_rms() {
697        // RMS of a 1.0 amplitude sine over full period ≈ 1/√2
698        let sig = SignalGenerator::sine(440.0, 1.0, 1.0, 44100.0);
699        let rms = sig.rms();
700        assert!((rms - (1.0f32 / 2.0f32.sqrt())).abs() < 0.01, "rms={}", rms);
701    }
702
703    #[test]
704    fn test_signal_peak() {
705        let sig = SignalGenerator::sine(440.0, 0.5, 0.1, 44100.0);
706        let pk = sig.peak();
707        assert!(pk <= 0.5 + 1e-4);
708        assert!(pk > 0.4);
709    }
710
711    #[test]
712    fn test_signal_energy() {
713        let sig = Signal::new(vec![1.0f32; 100], 100.0);
714        assert!((sig.energy() - 100.0).abs() < 1e-5);
715    }
716
717    #[test]
718    fn test_signal_downsample() {
719        let sig = Signal::new(vec![1.0f32; 100], 100.0);
720        let ds = sig.downsample(2);
721        assert_eq!(ds.len(), 50);
722        assert!((ds.sample_rate - 50.0).abs() < 1e-5);
723    }
724
725    #[test]
726    fn test_signal_upsample() {
727        let sig = Signal::new(vec![1.0f32; 10], 10.0);
728        let us = sig.upsample(3);
729        assert_eq!(us.len(), 30);
730        assert!((us.sample_rate - 30.0).abs() < 1e-5);
731    }
732
733    #[test]
734    fn test_signal_resample() {
735        let sig = SignalGenerator::sine(440.0, 1.0, 1.0, 44100.0);
736        let resampled = sig.resample(22050.0);
737        assert_eq!(resampled.len(), 22050);
738        assert!((resampled.sample_rate - 22050.0).abs() < 1e-3);
739    }
740
741    #[test]
742    fn test_window_hann_endpoints() {
743        let w = WindowFunction::Hann;
744        // Hann window starts and ends at (approximately) 0
745        assert!((w.coefficient(0, 1024)).abs() < 1e-5);
746    }
747
748    #[test]
749    fn test_window_rectangular() {
750        let w = WindowFunction::Rectangular;
751        for i in 0..64 {
752            assert!((w.coefficient(i, 64) - 1.0).abs() < 1e-6);
753        }
754    }
755
756    #[test]
757    fn test_window_apply() {
758        let mut buf = vec![1.0f32; 64];
759        WindowFunction::Hann.apply(&mut buf);
760        // Middle sample should be near 1.0
761        let mid = buf[32];
762        assert!(mid > 0.9, "mid={}", mid);
763    }
764
765    #[test]
766    fn test_envelope() {
767        let mut env = Envelope::new(0.001, 0.1, 0.0, 44100.0);
768        env.process(1.0);
769        assert!(env.level() > 0.0);
770        // After silence, should decay
771        for _ in 0..10000 {
772            env.process(0.0);
773        }
774        assert!(env.level() < 0.5);
775    }
776
777    #[test]
778    fn test_peak_meter() {
779        let mut meter = PeakMeter::new(1.0, 60.0, 44100.0);
780        let buf: Vec<f32> = vec![0.5; 512];
781        meter.process(&buf);
782        let db = meter.peak_db();
783        assert!(db > -7.0 && db < -5.0, "db={}", db);
784    }
785
786    #[test]
787    fn test_db_conversions() {
788        assert!((linear_to_db(1.0) - 0.0).abs() < 1e-5);
789        assert!((db_to_linear(0.0) - 1.0).abs() < 1e-5);
790        assert!((linear_to_db(0.5) - (-6.0206)).abs() < 0.01);
791    }
792
793    #[test]
794    fn test_next_power_of_two() {
795        assert_eq!(next_power_of_two(1), 1);
796        assert_eq!(next_power_of_two(5), 8);
797        assert_eq!(next_power_of_two(8), 8);
798        assert_eq!(next_power_of_two(1000), 1024);
799    }
800
801    #[test]
802    fn test_midi_freq_roundtrip() {
803        let midi = 69.0;
804        let freq = midi_to_freq(midi);
805        assert!((freq - 440.0).abs() < 0.01);
806        let back = freq_to_midi(freq);
807        assert!((back - midi).abs() < 0.01);
808    }
809
810    #[test]
811    fn test_signal_generator_chirp() {
812        let sig = SignalGenerator::chirp(20.0, 2000.0, 1.0, 1.0, 44100.0);
813        assert_eq!(sig.len(), 44100);
814        let pk = sig.peak();
815        assert!(pk > 0.9 && pk <= 1.0 + 1e-4);
816    }
817
818    #[test]
819    fn test_signal_generator_impulse() {
820        let sig = SignalGenerator::impulse(1.0, 0.1, 44100.0);
821        assert_eq!(sig.samples[0], 1.0);
822        assert_eq!(sig.samples[1], 0.0);
823    }
824
825    #[test]
826    fn test_zero_crossing_rate() {
827        let sig = SignalGenerator::sine(440.0, 1.0, 1.0, 44100.0);
828        let zcr = sig.zero_crossing_rate();
829        // Expected: ~880 crossings/sec (2 per cycle)
830        assert!(zcr > 800.0 && zcr < 1000.0, "zcr={}", zcr);
831    }
832}