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)]
11pub struct SampleRate(pub u32);
13
14impl SampleRate {
15 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 pub fn get(self) -> u32 {
28 self.0
29 }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct ResampleRatio {
35 pub input: SampleRate,
37 pub output: SampleRate,
39}
40
41impl ResampleRatio {
42 pub fn new(input: SampleRate, output: SampleRate) -> Result<Self> {
44 let ratio = Self { input, output };
45 ratio.validate()?;
46 Ok(ratio)
47 }
48
49 pub fn validate(self) -> Result<()> {
51 SampleRate::new(self.input.get())?;
52 SampleRate::new(self.output.get())?;
53 Ok(())
54 }
55
56 pub fn as_f64(self) -> f64 {
58 self.output.get() as f64 / self.input.get() as f64
59 }
60
61 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)]
68pub enum InterpolationMode {
70 Nearest,
72 Linear,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq)]
77pub struct SignalLevels {
79 pub count: usize,
81 pub peak: f32,
83 pub rms: f32,
85 pub mean: f32,
87 pub dc_offset: f32,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub struct ResampleSpec {
94 pub input: SampleRate,
96 pub output: SampleRate,
98 pub interpolation: InterpolationMode,
100}
101
102impl ResampleSpec {
103 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 pub fn validate(self) -> Result<()> {
120 ResampleRatio::new(self.input, self.output)?;
121 Ok(())
122 }
123
124 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)]
134pub enum WindowFunction {
136 Rectangular,
138 Hann,
140 Hamming,
142 Blackman,
144}
145
146impl WindowFunction {
147 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 pub fn weights(self, len: usize) -> Vec<f32> {
163 (0..len).map(|index| self.coefficient(index, len)).collect()
164 }
165
166 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)]
177pub struct WindowSpec {
179 pub function: WindowFunction,
181 pub len: usize,
183}
184
185impl WindowSpec {
186 pub fn new(function: WindowFunction, len: usize) -> Result<Self> {
188 let spec = Self { function, len };
189 spec.validate()?;
190 Ok(spec)
191 }
192
193 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 pub fn weights(self) -> Vec<f32> {
203 self.function.weights(self.len)
204 }
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq)]
208pub struct FrameStride {
210 pub frame_size: usize,
212 pub hop_size: usize,
214}
215
216impl FrameStride {
217 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 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)]
240pub enum BiquadDesign {
242 LowPass,
244 HighPass,
246 BandPass,
248 Notch,
250}
251
252#[derive(Debug, Clone, Copy, PartialEq)]
253pub enum ParametricBiquadDesign {
255 LowPass,
257 HighPass,
259 BandPass,
261 Notch,
263 PeakingEq { gain_db: f32 },
265 LowShelf { gain_db: f32 },
267 HighShelf { gain_db: f32 },
269 AllPass,
271}
272
273#[derive(Debug, Clone, Copy, PartialEq)]
274pub struct BiquadCoefficients {
276 pub b0: f32,
278 pub b1: f32,
280 pub b2: f32,
282 pub a1: f32,
284 pub a2: f32,
286}
287
288impl BiquadCoefficients {
289 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 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
361pub 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)]
466pub struct FirKernel1d {
468 values: Vec<f32>,
469}
470
471impl FirKernel1d {
472 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 pub fn values(&self) -> &[f32] {
483 &self.values
484 }
485
486 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
498pub 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
525pub 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
556pub 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
634pub 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
656pub 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
678pub 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
714pub 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
729pub 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
739pub 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
747pub 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
758pub 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}