Skip to main content

math_signal_core/
lib.rs

1#![doc = include_str!("../README.md")]
2
3pub mod surface;
4use video_analysis_core::{DetectError, Result};
5
6fn invalid_argument(message: impl Into<String>) -> DetectError {
7    DetectError::InvalidArgument(message.into())
8}
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
11/// Data type for sample rate.
12pub struct SampleRate(pub u32);
13
14impl SampleRate {
15    /// Creates a new value.
16    pub fn new(value: u32) -> Result<Self> {
17        if value == 0 {
18            return Err(DetectError::InvalidAudioFormat {
19                sample_rate: value,
20                channels: 1,
21            });
22        }
23        Ok(Self(value))
24    }
25
26    /// Returns get.
27    pub fn get(self) -> u32 {
28        self.0
29    }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33/// Data type for resample ratio.
34pub struct ResampleRatio {
35    /// The input value.
36    pub input: SampleRate,
37    /// The output value.
38    pub output: SampleRate,
39}
40
41impl ResampleRatio {
42    /// Creates a new value.
43    pub fn new(input: SampleRate, output: SampleRate) -> Result<Self> {
44        let ratio = Self { input, output };
45        ratio.validate()?;
46        Ok(ratio)
47    }
48
49    /// Validates this value.
50    pub fn validate(self) -> Result<()> {
51        SampleRate::new(self.input.get())?;
52        SampleRate::new(self.output.get())?;
53        Ok(())
54    }
55
56    /// Borrows this value as a f64.
57    pub fn as_f64(self) -> f64 {
58        self.output.get() as f64 / self.input.get() as f64
59    }
60
61    /// Returns source position for output.
62    pub fn source_position_for_output(self, output_index: usize) -> f64 {
63        output_index as f64 * self.input.get() as f64 / self.output.get() as f64
64    }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68/// Variants describing interpolation mode.
69pub enum InterpolationMode {
70    /// The nearest variant.
71    Nearest,
72    /// The linear variant.
73    Linear,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq)]
77/// Basic finite mono signal level summary.
78pub struct SignalLevels {
79    /// Number of samples summarized.
80    pub count: usize,
81    /// Maximum absolute sample value.
82    pub peak: f32,
83    /// Root mean square amplitude.
84    pub rms: f32,
85    /// Arithmetic mean sample value.
86    pub mean: f32,
87    /// Alias for mean, named for audio DC offset workflows.
88    pub dc_offset: f32,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92/// Data type for resample spec.
93pub struct ResampleSpec {
94    /// The input value.
95    pub input: SampleRate,
96    /// The output value.
97    pub output: SampleRate,
98    /// The interpolation value.
99    pub interpolation: InterpolationMode,
100}
101
102impl ResampleSpec {
103    /// Creates a new value.
104    pub fn new(
105        input: SampleRate,
106        output: SampleRate,
107        interpolation: InterpolationMode,
108    ) -> Result<Self> {
109        let spec = Self {
110            input,
111            output,
112            interpolation,
113        };
114        spec.validate()?;
115        Ok(spec)
116    }
117
118    /// Validates this value.
119    pub fn validate(self) -> Result<()> {
120        ResampleRatio::new(self.input, self.output)?;
121        Ok(())
122    }
123
124    /// Returns ratio.
125    pub fn ratio(self) -> ResampleRatio {
126        ResampleRatio {
127            input: self.input,
128            output: self.output,
129        }
130    }
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134/// Variants describing window function.
135pub enum WindowFunction {
136    /// The rectangular variant.
137    Rectangular,
138    /// The hann variant.
139    Hann,
140    /// The hamming variant.
141    Hamming,
142    /// The blackman variant.
143    Blackman,
144}
145
146impl WindowFunction {
147    /// Returns coefficient.
148    pub fn coefficient(self, index: usize, len: usize) -> f32 {
149        if len <= 1 {
150            return 1.0;
151        }
152        let phase = 2.0 * std::f32::consts::PI * index as f32 / (len - 1) as f32;
153        match self {
154            Self::Rectangular => 1.0,
155            Self::Hann => 0.5 - 0.5 * phase.cos(),
156            Self::Hamming => 0.54 - 0.46 * phase.cos(),
157            Self::Blackman => 0.42 - 0.5 * phase.cos() + 0.08 * (2.0 * phase).cos(),
158        }
159    }
160
161    /// Returns weights.
162    pub fn weights(self, len: usize) -> Vec<f32> {
163        (0..len).map(|index| self.coefficient(index, len)).collect()
164    }
165
166    /// Returns apply.
167    pub fn apply(self, samples: &[f32]) -> Vec<f32> {
168        samples
169            .iter()
170            .enumerate()
171            .map(|(index, sample)| sample * self.coefficient(index, samples.len()))
172            .collect()
173    }
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177/// Data type for window spec.
178pub struct WindowSpec {
179    /// The function value.
180    pub function: WindowFunction,
181    /// The len value.
182    pub len: usize,
183}
184
185impl WindowSpec {
186    /// Creates a new value.
187    pub fn new(function: WindowFunction, len: usize) -> Result<Self> {
188        let spec = Self { function, len };
189        spec.validate()?;
190        Ok(spec)
191    }
192
193    /// Validates this value.
194    pub fn validate(self) -> Result<()> {
195        if self.len == 0 {
196            return Err(invalid_argument("window length must be greater than zero"));
197        }
198        Ok(())
199    }
200
201    /// Returns weights.
202    pub fn weights(self) -> Vec<f32> {
203        self.function.weights(self.len)
204    }
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq)]
208/// Data type for frame stride.
209pub struct FrameStride {
210    /// The frame size value.
211    pub frame_size: usize,
212    /// The hop size value.
213    pub hop_size: usize,
214}
215
216impl FrameStride {
217    /// Creates a new value.
218    pub fn new(frame_size: usize, hop_size: usize) -> Result<Self> {
219        if frame_size == 0 || hop_size == 0 {
220            return Err(invalid_argument(
221                "frame_size and hop_size must be greater than zero",
222            ));
223        }
224        Ok(Self {
225            frame_size,
226            hop_size,
227        })
228    }
229
230    /// Returns frame count.
231    pub fn frame_count(self, samples_len: usize) -> usize {
232        if samples_len < self.frame_size {
233            return 0;
234        }
235        1 + (samples_len - self.frame_size) / self.hop_size
236    }
237}
238
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240/// Variants describing biquad design.
241pub enum BiquadDesign {
242    /// The low pass variant.
243    LowPass,
244    /// The high pass variant.
245    HighPass,
246    /// The band pass variant.
247    BandPass,
248    /// The notch variant.
249    Notch,
250}
251
252#[derive(Debug, Clone, Copy, PartialEq)]
253/// Variants describing parametric biquad design.
254pub enum ParametricBiquadDesign {
255    /// The low pass variant.
256    LowPass,
257    /// The high pass variant.
258    HighPass,
259    /// The band pass variant.
260    BandPass,
261    /// The notch variant.
262    Notch,
263    /// Peaking equalizer with gain in dB.
264    PeakingEq { gain_db: f32 },
265    /// Low shelf with gain in dB.
266    LowShelf { gain_db: f32 },
267    /// High shelf with gain in dB.
268    HighShelf { gain_db: f32 },
269    /// The all pass variant.
270    AllPass,
271}
272
273#[derive(Debug, Clone, Copy, PartialEq)]
274/// Data type for biquad coefficients.
275pub struct BiquadCoefficients {
276    /// The b0 value.
277    pub b0: f32,
278    /// The b1 value.
279    pub b1: f32,
280    /// The b2 value.
281    pub b2: f32,
282    /// The a1 value.
283    pub a1: f32,
284    /// The a2 value.
285    pub a2: f32,
286}
287
288impl BiquadCoefficients {
289    /// Validates this value.
290    pub fn validate(self) -> Result<()> {
291        if [self.b0, self.b1, self.b2, self.a1, self.a2]
292            .iter()
293            .any(|value| !value.is_finite())
294        {
295            return Err(invalid_argument("biquad coefficients must be finite"));
296        }
297        Ok(())
298    }
299}
300
301impl BiquadDesign {
302    /// Returns design.
303    pub fn design(
304        self,
305        sample_rate: SampleRate,
306        cutoff_hz: f32,
307        q: f32,
308    ) -> Result<BiquadCoefficients> {
309        if !cutoff_hz.is_finite() || cutoff_hz <= 0.0 || !q.is_finite() || q <= 0.0 {
310            return Err(invalid_argument(
311                "biquad cutoff must be finite and positive, and q must be finite and positive",
312            ));
313        }
314        let nyquist = sample_rate.get() as f32 / 2.0;
315        if cutoff_hz >= nyquist {
316            return Err(invalid_argument("biquad cutoff must be below Nyquist"));
317        }
318        let omega = 2.0 * std::f32::consts::PI * cutoff_hz / sample_rate.get() as f32;
319        let cos = omega.cos();
320        let sin = omega.sin();
321        let alpha = sin / (2.0 * q);
322
323        let (b0, b1, b2, a0, a1, a2) = match self {
324            Self::LowPass => (
325                (1.0 - cos) / 2.0,
326                1.0 - cos,
327                (1.0 - cos) / 2.0,
328                1.0 + alpha,
329                -2.0 * cos,
330                1.0 - alpha,
331            ),
332            Self::HighPass => (
333                (1.0 + cos) / 2.0,
334                -(1.0 + cos),
335                (1.0 + cos) / 2.0,
336                1.0 + alpha,
337                -2.0 * cos,
338                1.0 - alpha,
339            ),
340            Self::BandPass => (alpha, 0.0, -alpha, 1.0 + alpha, -2.0 * cos, 1.0 - alpha),
341            Self::Notch => (1.0, -2.0 * cos, 1.0, 1.0 + alpha, -2.0 * cos, 1.0 - alpha),
342        };
343        BiquadCoefficients {
344            b0: b0 / a0,
345            b1: b1 / a0,
346            b2: b2 / a0,
347            a1: a1 / a0,
348            a2: a2 / a0,
349        }
350        .validate()?;
351        Ok(BiquadCoefficients {
352            b0: b0 / a0,
353            b1: b1 / a0,
354            b2: b2 / a0,
355            a1: a1 / a0,
356            a2: a2 / a0,
357        })
358    }
359}
360
361/// Designs a parametric biquad filter.
362pub fn design_parametric_biquad(
363    design: ParametricBiquadDesign,
364    sample_rate: SampleRate,
365    frequency_hz: f32,
366    q: f32,
367) -> Result<BiquadCoefficients> {
368    if !frequency_hz.is_finite() || frequency_hz <= 0.0 || !q.is_finite() || q <= 0.0 {
369        return Err(invalid_argument(
370            "biquad frequency must be finite and positive, and q must be finite and positive",
371        ));
372    }
373    let nyquist = sample_rate.get() as f32 / 2.0;
374    if frequency_hz >= nyquist {
375        return Err(invalid_argument("biquad frequency must be below Nyquist"));
376    }
377    match design {
378        ParametricBiquadDesign::LowPass => {
379            return BiquadDesign::LowPass.design(sample_rate, frequency_hz, q);
380        }
381        ParametricBiquadDesign::HighPass => {
382            return BiquadDesign::HighPass.design(sample_rate, frequency_hz, q);
383        }
384        ParametricBiquadDesign::BandPass => {
385            return BiquadDesign::BandPass.design(sample_rate, frequency_hz, q);
386        }
387        ParametricBiquadDesign::Notch => {
388            return BiquadDesign::Notch.design(sample_rate, frequency_hz, q);
389        }
390        _ => {}
391    }
392
393    let omega = 2.0 * std::f32::consts::PI * frequency_hz / sample_rate.get() as f32;
394    let cos = omega.cos();
395    let sin = omega.sin();
396    let alpha = sin / (2.0 * q);
397    let (b0, b1, b2, a0, a1, a2) = match design {
398        ParametricBiquadDesign::PeakingEq { gain_db } => {
399            let a = 10.0_f32.powf(gain_db / 40.0);
400            (
401                1.0 + alpha * a,
402                -2.0 * cos,
403                1.0 - alpha * a,
404                1.0 + alpha / a,
405                -2.0 * cos,
406                1.0 - alpha / a,
407            )
408        }
409        ParametricBiquadDesign::LowShelf { gain_db } => {
410            let a = 10.0_f32.powf(gain_db / 40.0);
411            let sqrt_a = a.sqrt();
412            let shelf_alpha = sin / 2.0 * ((a + 1.0 / a) * (1.0 / q - 1.0) + 2.0).sqrt();
413            (
414                a * ((a + 1.0) - (a - 1.0) * cos + 2.0 * sqrt_a * shelf_alpha),
415                2.0 * a * ((a - 1.0) - (a + 1.0) * cos),
416                a * ((a + 1.0) - (a - 1.0) * cos - 2.0 * sqrt_a * shelf_alpha),
417                (a + 1.0) + (a - 1.0) * cos + 2.0 * sqrt_a * shelf_alpha,
418                -2.0 * ((a - 1.0) + (a + 1.0) * cos),
419                (a + 1.0) + (a - 1.0) * cos - 2.0 * sqrt_a * shelf_alpha,
420            )
421        }
422        ParametricBiquadDesign::HighShelf { gain_db } => {
423            let a = 10.0_f32.powf(gain_db / 40.0);
424            let sqrt_a = a.sqrt();
425            let shelf_alpha = sin / 2.0 * ((a + 1.0 / a) * (1.0 / q - 1.0) + 2.0).sqrt();
426            (
427                a * ((a + 1.0) + (a - 1.0) * cos + 2.0 * sqrt_a * shelf_alpha),
428                -2.0 * a * ((a - 1.0) + (a + 1.0) * cos),
429                a * ((a + 1.0) + (a - 1.0) * cos - 2.0 * sqrt_a * shelf_alpha),
430                (a + 1.0) - (a - 1.0) * cos + 2.0 * sqrt_a * shelf_alpha,
431                2.0 * ((a - 1.0) - (a + 1.0) * cos),
432                (a + 1.0) - (a - 1.0) * cos - 2.0 * sqrt_a * shelf_alpha,
433            )
434        }
435        ParametricBiquadDesign::AllPass => (
436            1.0 - alpha,
437            -2.0 * cos,
438            1.0 + alpha,
439            1.0 + alpha,
440            -2.0 * cos,
441            1.0 - alpha,
442        ),
443        ParametricBiquadDesign::LowPass
444        | ParametricBiquadDesign::HighPass
445        | ParametricBiquadDesign::BandPass
446        | ParametricBiquadDesign::Notch => unreachable!(),
447    };
448    BiquadCoefficients {
449        b0: b0 / a0,
450        b1: b1 / a0,
451        b2: b2 / a0,
452        a1: a1 / a0,
453        a2: a2 / a0,
454    }
455    .validate()?;
456    Ok(BiquadCoefficients {
457        b0: b0 / a0,
458        b1: b1 / a0,
459        b2: b2 / a0,
460        a1: a1 / a0,
461        a2: a2 / a0,
462    })
463}
464
465#[derive(Debug, Clone, PartialEq)]
466/// Data type for fir kernel1d.
467pub struct FirKernel1d {
468    values: Vec<f32>,
469}
470
471impl FirKernel1d {
472    /// Creates a new value.
473    pub fn new(values: impl Into<Vec<f32>>) -> Result<Self> {
474        let kernel = Self {
475            values: values.into(),
476        };
477        kernel.validate()?;
478        Ok(kernel)
479    }
480
481    /// Returns values.
482    pub fn values(&self) -> &[f32] {
483        &self.values
484    }
485
486    /// Validates this value.
487    pub fn validate(&self) -> Result<()> {
488        if self.values.is_empty() {
489            return Err(invalid_argument("FIR kernel must not be empty"));
490        }
491        if self.values.iter().any(|value| !value.is_finite()) {
492            return Err(invalid_argument("FIR kernel values must be finite"));
493        }
494        Ok(())
495    }
496}
497
498/// Returns interpolate at.
499pub fn interpolate_at(samples: &[f32], position: f64, mode: InterpolationMode) -> Result<f32> {
500    if samples.is_empty() {
501        return Err(invalid_argument("samples must not be empty"));
502    }
503    if !position.is_finite() || position < 0.0 {
504        return Err(invalid_argument(
505            "interpolation position must be finite and non-negative",
506        ));
507    }
508    let max_index = samples.len().saturating_sub(1) as f64;
509    let clamped = position.min(max_index);
510    Ok(match mode {
511        InterpolationMode::Nearest => samples[clamped.round() as usize],
512        InterpolationMode::Linear => {
513            let left = clamped.floor() as usize;
514            let right = clamped.ceil() as usize;
515            if left == right {
516                samples[left]
517            } else {
518                let fraction = (clamped - left as f64) as f32;
519                samples[left] * (1.0 - fraction) + samples[right] * fraction
520            }
521        }
522    })
523}
524
525/// Resamples a mono buffer with the requested interpolation mode.
526pub fn resample_mono(
527    samples: &[f32],
528    input_rate: SampleRate,
529    output_rate: SampleRate,
530    mode: InterpolationMode,
531) -> Result<Vec<f32>> {
532    ResampleRatio::new(input_rate, output_rate)?;
533    if samples.is_empty() {
534        return Ok(Vec::new());
535    }
536    if samples.iter().any(|sample| !sample.is_finite()) {
537        return Err(invalid_argument("samples must be finite"));
538    }
539    if input_rate == output_rate {
540        return Ok(samples.to_vec());
541    }
542    let ratio = output_rate.get() as f64 / input_rate.get() as f64;
543    let output_len = ((samples.len() as f64) * ratio).round().max(1.0) as usize;
544    let spec = ResampleSpec::new(input_rate, output_rate, mode)?;
545    (0..output_len)
546        .map(|index| {
547            interpolate_at(
548                samples,
549                spec.ratio().source_position_for_output(index),
550                mode,
551            )
552        })
553        .collect()
554}
555
556/// Resamples interleaved audio with independent per-channel interpolation.
557pub fn resample_interleaved(
558    samples: &[f32],
559    channels: u16,
560    input_rate: SampleRate,
561    output_rate: SampleRate,
562    mode: InterpolationMode,
563) -> Result<Vec<f32>> {
564    if channels == 0 {
565        return Err(DetectError::InvalidAudioFormat {
566            sample_rate: input_rate.get(),
567            channels,
568        });
569    }
570    if !samples.len().is_multiple_of(channels as usize) {
571        return Err(invalid_argument(
572            "interleaved sample length must be divisible by channel count",
573        ));
574    }
575    if samples.is_empty() {
576        return Ok(Vec::new());
577    }
578    if input_rate == output_rate {
579        return Ok(samples.to_vec());
580    }
581    let channels_usize = channels as usize;
582    let frames = samples.len() / channels_usize;
583    let ratio = output_rate.get() as f64 / input_rate.get() as f64;
584    let output_frames = ((frames as f64) * ratio).round().max(1.0) as usize;
585    let spec = ResampleSpec::new(input_rate, output_rate, mode)?;
586    let mut output = Vec::with_capacity(output_frames * channels_usize);
587    for output_frame in 0..output_frames {
588        let position = spec.ratio().source_position_for_output(output_frame);
589        for channel in 0..channels_usize {
590            output.push(interpolate_interleaved_channel(
591                samples,
592                channels_usize,
593                channel,
594                position,
595                mode,
596            )?);
597        }
598    }
599    Ok(output)
600}
601
602fn interpolate_interleaved_channel(
603    samples: &[f32],
604    channels: usize,
605    channel: usize,
606    position: f64,
607    mode: InterpolationMode,
608) -> Result<f32> {
609    if !position.is_finite() || position < 0.0 {
610        return Err(invalid_argument(
611            "interpolation position must be finite and non-negative",
612        ));
613    }
614    let frames = samples.len() / channels;
615    let max_index = frames.saturating_sub(1) as f64;
616    let clamped = position.min(max_index);
617    Ok(match mode {
618        InterpolationMode::Nearest => samples[clamped.round() as usize * channels + channel],
619        InterpolationMode::Linear => {
620            let left = clamped.floor() as usize;
621            let right = clamped.ceil() as usize;
622            if left == right {
623                samples[left * channels + channel]
624            } else {
625                let fraction = (clamped - left as f64) as f32;
626                let left_sample = samples[left * channels + channel];
627                let right_sample = samples[right * channels + channel];
628                left_sample * (1.0 - fraction) + right_sample * fraction
629            }
630        }
631    })
632}
633
634/// Computes peak, RMS, mean, and DC offset for a finite mono signal.
635pub fn signal_levels(samples: &[f32]) -> Result<SignalLevels> {
636    validate_samples(samples, "samples")?;
637    if samples.is_empty() {
638        return Err(invalid_argument("samples must not be empty"));
639    }
640    let count = samples.len();
641    let peak = samples
642        .iter()
643        .map(|sample| sample.abs())
644        .fold(0.0, f32::max);
645    let mean = samples.iter().sum::<f32>() / count as f32;
646    let rms = (samples.iter().map(|sample| sample * sample).sum::<f32>() / count as f32).sqrt();
647    Ok(SignalLevels {
648        count,
649        peak,
650        rms,
651        mean,
652        dc_offset: mean,
653    })
654}
655
656/// Applies a centered FIR kernel to mono samples, zero-padding beyond boundaries.
657pub fn apply_fir_mono(samples: &[f32], kernel: &FirKernel1d) -> Result<Vec<f32>> {
658    validate_samples(samples, "samples")?;
659    kernel.validate()?;
660    let center = kernel.values().len() / 2;
661    let mut output = Vec::with_capacity(samples.len());
662    for index in 0..samples.len() {
663        let mut acc = 0.0;
664        for (tap_index, coefficient) in kernel.values().iter().copied().enumerate() {
665            let sample_index = index as isize + tap_index as isize - center as isize;
666            if (0..samples.len() as isize).contains(&sample_index) {
667                acc += samples[sample_index as usize] * coefficient;
668            }
669        }
670        if !acc.is_finite() {
671            return Err(invalid_argument("FIR filtering produced non-finite values"));
672        }
673        output.push(acc);
674    }
675    Ok(output)
676}
677
678/// Scales a finite mono signal to a target peak amplitude.
679pub fn normalize_peak(samples: &[f32], target_peak: f32) -> Result<Vec<f32>> {
680    validate_samples(samples, "samples")?;
681    if !target_peak.is_finite() || target_peak < 0.0 {
682        return Err(invalid_argument(
683            "target peak must be finite and non-negative",
684        ));
685    }
686    if samples.is_empty() {
687        return Ok(Vec::new());
688    }
689    let peak = signal_levels(samples)?.peak;
690    if peak <= f32::EPSILON {
691        if target_peak == 0.0 {
692            return Ok(samples.to_vec());
693        }
694        return Err(invalid_argument(
695            "cannot normalize a silent signal to a non-zero peak",
696        ));
697    }
698    let scale = target_peak / peak;
699    samples
700        .iter()
701        .map(|sample| {
702            let value = sample * scale;
703            if value.is_finite() {
704                Ok(value)
705            } else {
706                Err(invalid_argument(
707                    "peak normalization produced non-finite values",
708                ))
709            }
710        })
711        .collect()
712}
713
714/// Converts decibels to a linear amplitude ratio.
715pub fn db_to_linear(db: f32) -> Result<f32> {
716    if !db.is_finite() {
717        return Err(invalid_argument("dB value must be finite"));
718    }
719    Ok(10.0_f32.powf(db / 20.0))
720}
721
722fn validate_samples(samples: &[f32], label: &str) -> Result<()> {
723    if samples.iter().any(|sample| !sample.is_finite()) {
724        return Err(invalid_argument(format!("{label} must be finite")));
725    }
726    Ok(())
727}
728
729/// Converts a linear amplitude ratio to decibels.
730pub fn linear_to_db(linear: f32) -> Result<f32> {
731    if !linear.is_finite() || linear <= 0.0 {
732        return Err(invalid_argument(
733            "linear amplitude must be finite and greater than zero",
734        ));
735    }
736    Ok(20.0 * linear.log10())
737}
738
739/// Returns resample indices.
740pub fn resample_indices(spec: ResampleSpec, output_len: usize) -> Result<Vec<f64>> {
741    spec.validate()?;
742    Ok((0..output_len)
743        .map(|index| spec.ratio().source_position_for_output(index))
744        .collect())
745}
746
747/// Returns FFT bin frequency.
748pub fn fft_bin_frequency(bin: usize, fft_size: usize, sample_rate: SampleRate) -> Result<f32> {
749    if fft_size == 0 {
750        return Err(invalid_argument("fft_size must be greater than zero"));
751    }
752    if bin > fft_size / 2 {
753        return Err(invalid_argument("fft bin must not exceed the Nyquist bin"));
754    }
755    Ok(bin as f32 * sample_rate.get() as f32 / fft_size as f32)
756}
757
758/// Returns frequency to FFT bin.
759pub fn frequency_to_fft_bin(
760    frequency_hz: f32,
761    fft_size: usize,
762    sample_rate: SampleRate,
763) -> Result<usize> {
764    if !frequency_hz.is_finite() || frequency_hz < 0.0 {
765        return Err(invalid_argument(
766            "frequency must be finite and non-negative",
767        ));
768    }
769    if fft_size == 0 {
770        return Err(invalid_argument("fft_size must be greater than zero"));
771    }
772    let nyquist = sample_rate.get() as f32 / 2.0;
773    if frequency_hz > nyquist {
774        return Err(invalid_argument("frequency must not exceed Nyquist"));
775    }
776    Ok((frequency_hz * fft_size as f32 / sample_rate.get() as f32).round() as usize)
777}
778
779#[cfg(test)]
780mod tests {
781    use super::*;
782
783    #[test]
784    fn window_lengths_and_symmetry_match_expectations() {
785        let weights = WindowFunction::Hann.weights(4);
786        assert_eq!(weights.len(), 4);
787        assert!((weights[0] - weights[3]).abs() < 1.0e-6);
788    }
789
790    #[test]
791    fn interpolation_handles_edge_cases() {
792        assert_eq!(
793            interpolate_at(&[0.0, 10.0], 0.5, InterpolationMode::Linear).unwrap(),
794            5.0
795        );
796        assert_eq!(
797            interpolate_at(&[0.0, 10.0], 2.0, InterpolationMode::Nearest).unwrap(),
798            10.0
799        );
800    }
801
802    #[test]
803    fn resample_and_filter_descriptors_validate() {
804        let ratio = ResampleRatio::new(
805            SampleRate::new(48_000).unwrap(),
806            SampleRate::new(16_000).unwrap(),
807        )
808        .unwrap();
809        assert!(ratio.as_f64() < 1.0);
810        assert!(BiquadDesign::LowPass
811            .design(SampleRate::new(48_000).unwrap(), 1_000.0, 0.707)
812            .is_ok());
813        assert!(FirKernel1d::new([0.25, 0.5, 0.25]).is_ok());
814    }
815
816    #[test]
817    fn resampling_and_db_helpers_work() {
818        let input = SampleRate::new(4).unwrap();
819        let output = SampleRate::new(8).unwrap();
820        let resampled =
821            resample_mono(&[0.0, 1.0, 0.0], input, output, InterpolationMode::Linear).unwrap();
822        assert_eq!(resampled.len(), 6);
823        assert_eq!(resampled[0], 0.0);
824        assert_eq!(*resampled.last().unwrap(), 0.0);
825
826        let interleaved = resample_interleaved(
827            &[0.0, 1.0, 1.0, 0.0],
828            2,
829            SampleRate::new(2).unwrap(),
830            SampleRate::new(4).unwrap(),
831            InterpolationMode::Linear,
832        )
833        .unwrap();
834        assert_eq!(interleaved.len(), 8);
835        assert!((db_to_linear(6.020_600_3).unwrap() - 2.0).abs() < 1.0e-5);
836        assert!(linear_to_db(2.0).unwrap() > 6.0);
837    }
838
839    #[test]
840    fn parametric_design_supports_eq_variants() {
841        for design in [
842            ParametricBiquadDesign::PeakingEq { gain_db: 3.0 },
843            ParametricBiquadDesign::LowShelf { gain_db: -3.0 },
844            ParametricBiquadDesign::HighShelf { gain_db: 3.0 },
845            ParametricBiquadDesign::AllPass,
846        ] {
847            assert!(design_parametric_biquad(
848                design,
849                SampleRate::new(48_000).unwrap(),
850                1_000.0,
851                0.707
852            )
853            .is_ok());
854        }
855    }
856
857    #[test]
858    fn fft_frequency_helpers_round_trip() {
859        let sample_rate = SampleRate::new(8_000).unwrap();
860        let bin = frequency_to_fft_bin(1_000.0, 8, sample_rate).unwrap();
861        assert_eq!(bin, 1);
862        assert_eq!(fft_bin_frequency(bin, 8, sample_rate).unwrap(), 1_000.0);
863    }
864}