Skip to main content

oximedia_graph/filters/audio/
resample.rs

1//! Audio resampling filter.
2//!
3//! This module provides high-quality sample rate conversion using sinc interpolation
4//! with windowed kernels.
5
6#![forbid(unsafe_code)]
7#![allow(clippy::cast_sign_loss)]
8#![allow(clippy::cast_possible_wrap)]
9#![allow(clippy::cast_lossless)]
10#![allow(clippy::needless_pass_by_value)]
11#![allow(clippy::needless_range_loop)]
12#![allow(clippy::get_first)]
13#![allow(clippy::doc_markdown)]
14
15use bytes::{Bytes, BytesMut};
16use std::f64::consts::PI;
17
18use crate::error::{GraphError, GraphResult};
19use crate::frame::FilterFrame;
20use crate::node::{Node, NodeId, NodeState, NodeType};
21use crate::port::{AudioPortFormat, InputPort, OutputPort, PortFormat, PortId, PortType};
22
23use oximedia_audio::{AudioBuffer, AudioFrame, ChannelLayout};
24use oximedia_core::SampleFormat;
25
26/// Quality preset for resampling.
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
28pub enum ResampleQuality {
29    /// Fast resampling with lower quality (8 taps).
30    Fast,
31    /// Medium quality (16 taps).
32    #[default]
33    Medium,
34    /// High quality (32 taps).
35    High,
36    /// Very high quality (64 taps).
37    VeryHigh,
38}
39
40impl ResampleQuality {
41    /// Get the number of filter taps for this quality level.
42    #[must_use]
43    pub fn taps(self) -> usize {
44        match self {
45            Self::Fast => 8,
46            Self::Medium => 16,
47            Self::High => 32,
48            Self::VeryHigh => 64,
49        }
50    }
51
52    /// Get Kaiser window beta parameter for this quality level.
53    #[must_use]
54    pub fn kaiser_beta(self) -> f64 {
55        match self {
56            Self::Fast => 5.0,
57            Self::Medium => 6.0,
58            Self::High => 8.0,
59            Self::VeryHigh => 10.0,
60        }
61    }
62}
63
64/// Configuration for the resample filter.
65#[derive(Clone, Debug)]
66pub struct ResampleConfig {
67    /// Input sample rate in Hz.
68    pub input_rate: u32,
69    /// Output sample rate in Hz.
70    pub output_rate: u32,
71    /// Quality preset.
72    pub quality: ResampleQuality,
73    /// Anti-aliasing filter enabled.
74    pub anti_alias: bool,
75}
76
77impl Default for ResampleConfig {
78    fn default() -> Self {
79        Self {
80            input_rate: 48000,
81            output_rate: 44100,
82            quality: ResampleQuality::Medium,
83            anti_alias: true,
84        }
85    }
86}
87
88impl ResampleConfig {
89    /// Create a new resample configuration.
90    #[must_use]
91    pub fn new(input_rate: u32, output_rate: u32) -> Self {
92        Self {
93            input_rate,
94            output_rate,
95            ..Default::default()
96        }
97    }
98
99    /// Set the quality preset.
100    #[must_use]
101    pub fn with_quality(mut self, quality: ResampleQuality) -> Self {
102        self.quality = quality;
103        self
104    }
105
106    /// Enable or disable anti-aliasing.
107    #[must_use]
108    pub fn with_anti_alias(mut self, enabled: bool) -> Self {
109        self.anti_alias = enabled;
110        self
111    }
112}
113
114/// Sinc interpolation kernel.
115#[derive(Clone, Debug)]
116struct SincKernel {
117    /// Precomputed filter coefficients.
118    coefficients: Vec<f64>,
119    /// Number of taps.
120    taps: usize,
121    /// Kaiser beta parameter.
122    beta: f64,
123    /// Cutoff frequency (normalized).
124    cutoff: f64,
125}
126
127impl SincKernel {
128    /// Create a new sinc kernel.
129    fn new(taps: usize, beta: f64, cutoff: f64) -> Self {
130        let coefficients = Self::compute_coefficients(taps, beta, cutoff);
131        Self {
132            coefficients,
133            taps,
134            beta,
135            cutoff,
136        }
137    }
138
139    /// Compute filter coefficients.
140    fn compute_coefficients(taps: usize, beta: f64, cutoff: f64) -> Vec<f64> {
141        let half_taps = taps / 2;
142        let mut coeffs = Vec::with_capacity(taps);
143
144        for i in 0..taps {
145            let x = (i as f64 - half_taps as f64) / half_taps as f64;
146            let sinc = Self::sinc(x * cutoff * half_taps as f64);
147            let window = Self::kaiser_window(x, beta);
148            coeffs.push(sinc * window * cutoff);
149        }
150
151        // Normalize coefficients
152        let sum: f64 = coeffs.iter().sum();
153        if sum.abs() > f64::EPSILON {
154            for coeff in &mut coeffs {
155                *coeff /= sum;
156            }
157        }
158
159        coeffs
160    }
161
162    /// Sinc function: sin(pi * x) / (pi * x).
163    fn sinc(x: f64) -> f64 {
164        if x.abs() < f64::EPSILON {
165            1.0
166        } else {
167            let px = PI * x;
168            px.sin() / px
169        }
170    }
171
172    /// Kaiser window function.
173    fn kaiser_window(x: f64, beta: f64) -> f64 {
174        if x.abs() >= 1.0 {
175            return 0.0;
176        }
177        let arg = 1.0 - x * x;
178        if arg < 0.0 {
179            return 0.0;
180        }
181        Self::bessel_i0(beta * arg.sqrt()) / Self::bessel_i0(beta)
182    }
183
184    /// Modified Bessel function of the first kind, order 0.
185    fn bessel_i0(x: f64) -> f64 {
186        let mut sum = 1.0;
187        let mut term = 1.0;
188        let x_half = x / 2.0;
189
190        for k in 1..=25 {
191            term *= x_half * x_half / (k * k) as f64;
192            sum += term;
193            if term < f64::EPSILON * sum {
194                break;
195            }
196        }
197
198        sum
199    }
200
201    /// Interpolate samples at a fractional position.
202    fn interpolate(&self, samples: &[f64], position: f64) -> f64 {
203        let base_index = position.floor() as isize;
204        let frac = position - position.floor();
205        let half_taps = (self.taps / 2) as isize;
206
207        let mut result = 0.0;
208        for (tap, coeff) in self.coefficients.iter().enumerate() {
209            let tap_offset = tap as isize - half_taps;
210            let sample_index = base_index + tap_offset;
211
212            if sample_index >= 0 && (sample_index as usize) < samples.len() {
213                // Apply phase-adjusted coefficient
214                let phase_adjust = Self::sinc((tap_offset as f64 - frac) * self.cutoff);
215                let window = Self::kaiser_window(
216                    (tap as f64 - self.taps as f64 / 2.0) / (self.taps as f64 / 2.0),
217                    self.beta,
218                );
219                result += samples[sample_index as usize] * coeff * phase_adjust * window;
220            }
221        }
222
223        result
224    }
225}
226
227/// Internal state for resampling.
228#[derive(Clone, Debug)]
229struct ResampleState {
230    /// Input sample buffer for overlap.
231    input_buffer: Vec<Vec<f64>>,
232    /// Current input position (fractional).
233    position: f64,
234    /// Ratio of input to output rate.
235    ratio: f64,
236    /// Sinc interpolation kernel.
237    kernel: SincKernel,
238    /// Number of channels.
239    channels: usize,
240}
241
242impl ResampleState {
243    /// Create new resample state.
244    fn new(config: &ResampleConfig, channels: usize) -> Self {
245        let ratio = config.input_rate as f64 / config.output_rate as f64;
246        let cutoff = if config.anti_alias && ratio > 1.0 {
247            1.0 / ratio * 0.95 // Apply 5% guard band
248        } else {
249            0.95
250        };
251
252        let kernel = SincKernel::new(config.quality.taps(), config.quality.kaiser_beta(), cutoff);
253
254        Self {
255            input_buffer: vec![Vec::new(); channels],
256            position: 0.0,
257            ratio,
258            kernel,
259            channels,
260        }
261    }
262
263    /// Process samples through the resampler.
264    fn process(&mut self, input: &[Vec<f64>]) -> Vec<Vec<f64>> {
265        if input.is_empty() || input[0].is_empty() {
266            return vec![Vec::new(); self.channels];
267        }
268
269        // Append input to buffer
270        for (ch, samples) in input.iter().enumerate() {
271            if ch < self.channels {
272                self.input_buffer[ch].extend_from_slice(samples);
273            }
274        }
275
276        // Calculate output sample count
277        let available_input = self.input_buffer[0].len() as f64 - self.position;
278        let output_samples = (available_input / self.ratio).floor() as usize;
279
280        if output_samples == 0 {
281            return vec![Vec::new(); self.channels];
282        }
283
284        // Resample each channel
285        let mut output = vec![Vec::with_capacity(output_samples); self.channels];
286
287        for out_idx in 0..output_samples {
288            let in_pos = self.position + out_idx as f64 * self.ratio;
289
290            for ch in 0..self.channels {
291                let sample = self.kernel.interpolate(&self.input_buffer[ch], in_pos);
292                output[ch].push(sample);
293            }
294        }
295
296        // Update position and trim consumed samples
297        self.position += output_samples as f64 * self.ratio;
298        let consumed = self.position.floor() as usize;
299
300        // Keep overlap for next block
301        let keep = self.kernel.taps;
302        if consumed > keep {
303            let trim = consumed - keep;
304            for ch in 0..self.channels {
305                if trim < self.input_buffer[ch].len() {
306                    self.input_buffer[ch].drain(0..trim);
307                }
308            }
309            self.position -= trim as f64;
310        }
311
312        output
313    }
314
315    /// Flush remaining samples.
316    fn flush(&mut self) -> Vec<Vec<f64>> {
317        // Pad with zeros for final samples
318        let padding = self.kernel.taps;
319        for ch in 0..self.channels {
320            self.input_buffer[ch].extend(vec![0.0; padding]);
321        }
322
323        // Process remaining samples
324        let input: Vec<Vec<f64>> = vec![Vec::new(); self.channels];
325        let output = self.process(&input);
326
327        // Reset state
328        for ch in 0..self.channels {
329            self.input_buffer[ch].clear();
330        }
331        self.position = 0.0;
332
333        output
334    }
335}
336
337/// Audio resampling filter using sinc interpolation.
338///
339/// This filter converts audio between different sample rates using
340/// high-quality sinc interpolation with a Kaiser window.
341///
342/// # Example
343///
344/// ```ignore
345/// use oximedia_graph::filters::audio::resample::{ResampleFilter, ResampleConfig, ResampleQuality};
346///
347/// let config = ResampleConfig::new(48000, 44100)
348///     .with_quality(ResampleQuality::High);
349/// let filter = ResampleFilter::new(NodeId(0), "resample", config);
350/// ```
351pub struct ResampleFilter {
352    id: NodeId,
353    name: String,
354    state: NodeState,
355    config: ResampleConfig,
356    resample_state: Option<ResampleState>,
357    inputs: Vec<InputPort>,
358    outputs: Vec<OutputPort>,
359}
360
361impl ResampleFilter {
362    /// Create a new resample filter.
363    #[must_use]
364    pub fn new(id: NodeId, name: impl Into<String>, config: ResampleConfig) -> Self {
365        let input_format =
366            PortFormat::Audio(AudioPortFormat::any().with_sample_rate(config.input_rate));
367        let output_format =
368            PortFormat::Audio(AudioPortFormat::any().with_sample_rate(config.output_rate));
369
370        Self {
371            id,
372            name: name.into(),
373            state: NodeState::Idle,
374            config,
375            resample_state: None,
376            inputs: vec![
377                InputPort::new(PortId(0), "input", PortType::Audio).with_format(input_format)
378            ],
379            outputs: vec![
380                OutputPort::new(PortId(0), "output", PortType::Audio).with_format(output_format)
381            ],
382        }
383    }
384
385    /// Get the current configuration.
386    #[must_use]
387    pub fn config(&self) -> &ResampleConfig {
388        &self.config
389    }
390
391    /// Update the configuration.
392    pub fn set_config(&mut self, config: ResampleConfig) {
393        self.config = config;
394        self.resample_state = None; // Reset state
395    }
396
397    /// Convert audio frame samples to f64 vectors (one per channel).
398    fn frame_to_samples(frame: &AudioFrame) -> Vec<Vec<f64>> {
399        let channels = frame.channels.count();
400        let sample_count = frame.sample_count();
401
402        if sample_count == 0 {
403            return vec![Vec::new(); channels];
404        }
405
406        let mut output = vec![Vec::with_capacity(sample_count); channels];
407
408        match &frame.samples {
409            AudioBuffer::Interleaved(data) => {
410                Self::convert_interleaved_to_f64(data, frame.format, channels, &mut output);
411            }
412            AudioBuffer::Planar(planes) => {
413                Self::convert_planar_to_f64(planes, frame.format, &mut output);
414            }
415        }
416
417        output
418    }
419
420    /// Convert interleaved samples to f64 vectors.
421    fn convert_interleaved_to_f64(
422        data: &Bytes,
423        format: SampleFormat,
424        channels: usize,
425        output: &mut [Vec<f64>],
426    ) {
427        let bytes_per_sample = format.bytes_per_sample();
428        if bytes_per_sample == 0 || channels == 0 {
429            return;
430        }
431
432        let sample_count = data.len() / (bytes_per_sample * channels);
433
434        for i in 0..sample_count {
435            for ch in 0..channels {
436                let offset = (i * channels + ch) * bytes_per_sample;
437                if offset + bytes_per_sample <= data.len() {
438                    let sample =
439                        Self::bytes_to_f64(&data[offset..offset + bytes_per_sample], format);
440                    output[ch].push(sample);
441                }
442            }
443        }
444    }
445
446    /// Convert planar samples to f64 vectors.
447    fn convert_planar_to_f64(planes: &[Bytes], format: SampleFormat, output: &mut [Vec<f64>]) {
448        let bytes_per_sample = format.bytes_per_sample();
449        if bytes_per_sample == 0 {
450            return;
451        }
452
453        for (ch, plane) in planes.iter().enumerate() {
454            if ch >= output.len() {
455                break;
456            }
457            let sample_count = plane.len() / bytes_per_sample;
458            for i in 0..sample_count {
459                let offset = i * bytes_per_sample;
460                if offset + bytes_per_sample <= plane.len() {
461                    let sample =
462                        Self::bytes_to_f64(&plane[offset..offset + bytes_per_sample], format);
463                    output[ch].push(sample);
464                }
465            }
466        }
467    }
468
469    /// Convert bytes to f64 sample.
470    fn bytes_to_f64(bytes: &[u8], format: SampleFormat) -> f64 {
471        match format {
472            SampleFormat::U8 => {
473                if bytes.is_empty() {
474                    return 0.0;
475                }
476                (f64::from(bytes[0]) - 128.0) / 128.0
477            }
478            SampleFormat::S16 => {
479                if bytes.len() < 2 {
480                    return 0.0;
481                }
482                let sample = i16::from_le_bytes([bytes[0], bytes[1]]);
483                f64::from(sample) / f64::from(i16::MAX)
484            }
485            SampleFormat::S32 => {
486                if bytes.len() < 4 {
487                    return 0.0;
488                }
489                let sample = i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
490                f64::from(sample) / f64::from(i32::MAX)
491            }
492            SampleFormat::F32 => {
493                if bytes.len() < 4 {
494                    return 0.0;
495                }
496                f64::from(f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
497            }
498            SampleFormat::F64 => {
499                if bytes.len() < 8 {
500                    return 0.0;
501                }
502                f64::from_le_bytes([
503                    bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
504                ])
505            }
506            _ => 0.0,
507        }
508    }
509
510    /// Convert f64 samples back to audio frame.
511    fn samples_to_frame(
512        samples: Vec<Vec<f64>>,
513        format: SampleFormat,
514        sample_rate: u32,
515        channels: ChannelLayout,
516    ) -> AudioFrame {
517        let channel_count = channels.count();
518        if samples.is_empty() || samples[0].is_empty() || channel_count == 0 {
519            return AudioFrame::new(format, sample_rate, channels);
520        }
521
522        let sample_count = samples[0].len();
523        let bytes_per_sample = format.bytes_per_sample();
524        let mut buffer = BytesMut::with_capacity(sample_count * channel_count * bytes_per_sample);
525
526        // Convert to interleaved format
527        for i in 0..sample_count {
528            for ch in 0..channel_count {
529                let sample = if ch < samples.len() && i < samples[ch].len() {
530                    samples[ch][i]
531                } else {
532                    0.0
533                };
534                Self::f64_to_bytes(sample, format, &mut buffer);
535            }
536        }
537
538        let mut frame = AudioFrame::new(format, sample_rate, channels);
539        frame.samples = AudioBuffer::Interleaved(buffer.freeze());
540        frame
541    }
542
543    /// Convert f64 sample to bytes.
544    fn f64_to_bytes(sample: f64, format: SampleFormat, buffer: &mut BytesMut) {
545        // Clamp sample to valid range
546        let clamped = sample.clamp(-1.0, 1.0);
547
548        match format {
549            SampleFormat::U8 => {
550                let value = ((clamped * 128.0) + 128.0) as u8;
551                buffer.extend_from_slice(&[value]);
552            }
553            SampleFormat::S16 => {
554                let value = (clamped * f64::from(i16::MAX)) as i16;
555                buffer.extend_from_slice(&value.to_le_bytes());
556            }
557            SampleFormat::S32 => {
558                let value = (clamped * f64::from(i32::MAX)) as i32;
559                buffer.extend_from_slice(&value.to_le_bytes());
560            }
561            SampleFormat::F32 => {
562                #[allow(clippy::cast_possible_truncation)]
563                let value = clamped as f32;
564                buffer.extend_from_slice(&value.to_le_bytes());
565            }
566            SampleFormat::F64 => {
567                buffer.extend_from_slice(&clamped.to_le_bytes());
568            }
569            _ => {}
570        }
571    }
572}
573
574impl Node for ResampleFilter {
575    fn id(&self) -> NodeId {
576        self.id
577    }
578
579    fn name(&self) -> &str {
580        &self.name
581    }
582
583    fn node_type(&self) -> NodeType {
584        NodeType::Filter
585    }
586
587    fn state(&self) -> NodeState {
588        self.state
589    }
590
591    fn set_state(&mut self, state: NodeState) -> GraphResult<()> {
592        if !self.state.can_transition_to(state) {
593            return Err(GraphError::InvalidStateTransition {
594                node: self.id,
595                from: self.state.to_string(),
596                to: state.to_string(),
597            });
598        }
599        self.state = state;
600        Ok(())
601    }
602
603    fn inputs(&self) -> &[InputPort] {
604        &self.inputs
605    }
606
607    fn outputs(&self) -> &[OutputPort] {
608        &self.outputs
609    }
610
611    fn initialize(&mut self) -> GraphResult<()> {
612        // State will be initialized on first frame when we know channel count
613        Ok(())
614    }
615
616    fn process(&mut self, input: Option<FilterFrame>) -> GraphResult<Option<FilterFrame>> {
617        let frame = match input {
618            Some(FilterFrame::Audio(frame)) => frame,
619            Some(_) => {
620                return Err(GraphError::PortTypeMismatch {
621                    expected: "Audio".to_string(),
622                    actual: "Video".to_string(),
623                });
624            }
625            None => return Ok(None),
626        };
627
628        // Initialize resample state if needed
629        if self.resample_state.is_none() {
630            let channels = frame.channels.count();
631            self.resample_state = Some(ResampleState::new(&self.config, channels));
632        }
633
634        let state = match self.resample_state.as_mut() {
635            Some(s) => s,
636            None => return Ok(None),
637        };
638
639        // Convert to f64 samples
640        let input_samples = Self::frame_to_samples(&frame);
641
642        // Resample
643        let output_samples = state.process(&input_samples);
644
645        // Convert back to frame
646        let output_frame = Self::samples_to_frame(
647            output_samples,
648            frame.format,
649            self.config.output_rate,
650            frame.channels.clone(),
651        );
652
653        Ok(Some(FilterFrame::Audio(output_frame)))
654    }
655
656    fn flush(&mut self) -> GraphResult<Vec<FilterFrame>> {
657        if let Some(state) = self.resample_state.as_mut() {
658            let output_samples = state.flush();
659            if !output_samples.is_empty() && !output_samples[0].is_empty() {
660                let frame = Self::samples_to_frame(
661                    output_samples,
662                    SampleFormat::F32,
663                    self.config.output_rate,
664                    ChannelLayout::from_count(state.channels),
665                );
666                return Ok(vec![FilterFrame::Audio(frame)]);
667            }
668        }
669        Ok(Vec::new())
670    }
671
672    fn reset(&mut self) -> GraphResult<()> {
673        self.resample_state = None;
674        self.set_state(NodeState::Idle)
675    }
676}
677
678#[cfg(test)]
679mod tests {
680    use super::*;
681
682    #[test]
683    fn test_resample_quality() {
684        assert_eq!(ResampleQuality::Fast.taps(), 8);
685        assert_eq!(ResampleQuality::Medium.taps(), 16);
686        assert_eq!(ResampleQuality::High.taps(), 32);
687        assert_eq!(ResampleQuality::VeryHigh.taps(), 64);
688    }
689
690    #[test]
691    fn test_resample_config() {
692        let config = ResampleConfig::new(48000, 44100)
693            .with_quality(ResampleQuality::High)
694            .with_anti_alias(true);
695
696        assert_eq!(config.input_rate, 48000);
697        assert_eq!(config.output_rate, 44100);
698        assert_eq!(config.quality, ResampleQuality::High);
699        assert!(config.anti_alias);
700    }
701
702    #[test]
703    fn test_sinc_function() {
704        let sinc_0 = SincKernel::sinc(0.0);
705        assert!((sinc_0 - 1.0).abs() < f64::EPSILON);
706
707        let sinc_1 = SincKernel::sinc(1.0);
708        assert!(sinc_1.abs() < 0.01);
709    }
710
711    #[test]
712    fn test_kaiser_window() {
713        let center = SincKernel::kaiser_window(0.0, 6.0);
714        assert!((center - 1.0).abs() < 0.01);
715
716        let edge = SincKernel::kaiser_window(1.0, 6.0);
717        assert!(edge.abs() < f64::EPSILON);
718    }
719
720    #[test]
721    fn test_bessel_i0() {
722        let i0_0 = SincKernel::bessel_i0(0.0);
723        assert!((i0_0 - 1.0).abs() < f64::EPSILON);
724    }
725
726    #[test]
727    fn test_resample_filter_creation() {
728        let config = ResampleConfig::new(48000, 44100);
729        let filter = ResampleFilter::new(NodeId(1), "test_resample", config);
730
731        assert_eq!(filter.id(), NodeId(1));
732        assert_eq!(filter.name(), "test_resample");
733        assert_eq!(filter.node_type(), NodeType::Filter);
734        assert_eq!(filter.state(), NodeState::Idle);
735    }
736
737    #[test]
738    fn test_resample_filter_ports() {
739        let config = ResampleConfig::new(48000, 44100);
740        let filter = ResampleFilter::new(NodeId(0), "test", config);
741
742        assert_eq!(filter.inputs().len(), 1);
743        assert_eq!(filter.outputs().len(), 1);
744        assert_eq!(filter.inputs()[0].port_type, PortType::Audio);
745        assert_eq!(filter.outputs()[0].port_type, PortType::Audio);
746    }
747
748    #[test]
749    fn test_bytes_to_f64_u8() {
750        let sample = ResampleFilter::bytes_to_f64(&[128], SampleFormat::U8);
751        assert!(sample.abs() < 0.01);
752
753        let sample = ResampleFilter::bytes_to_f64(&[255], SampleFormat::U8);
754        assert!((sample - 0.992).abs() < 0.01);
755    }
756
757    #[test]
758    fn test_bytes_to_f64_s16() {
759        let sample = ResampleFilter::bytes_to_f64(&[0, 0], SampleFormat::S16);
760        assert!(sample.abs() < f64::EPSILON);
761
762        let sample = ResampleFilter::bytes_to_f64(&[0xFF, 0x7F], SampleFormat::S16);
763        assert!((sample - 1.0).abs() < 0.001);
764    }
765
766    #[test]
767    fn test_bytes_to_f64_f32() {
768        let value: f32 = 0.5;
769        let bytes = value.to_le_bytes();
770        let sample = ResampleFilter::bytes_to_f64(&bytes, SampleFormat::F32);
771        assert!((sample - 0.5).abs() < f64::EPSILON);
772    }
773
774    #[test]
775    fn test_resample_process_empty() {
776        let config = ResampleConfig::new(48000, 44100);
777        let mut filter = ResampleFilter::new(NodeId(0), "test", config);
778
779        let result = filter.process(None).expect("process should succeed");
780        assert!(result.is_none());
781    }
782
783    #[test]
784    fn test_resample_process_audio() {
785        let config = ResampleConfig::new(48000, 48000); // Same rate for simple test
786        let mut filter = ResampleFilter::new(NodeId(0), "test", config);
787
788        let mut frame = AudioFrame::new(SampleFormat::F32, 48000, ChannelLayout::Mono);
789        // Create some test samples
790        let mut samples = BytesMut::new();
791        for i in 0..1024 {
792            let sample = (i as f32 * 0.001).sin();
793            samples.extend_from_slice(&sample.to_le_bytes());
794        }
795        frame.samples = AudioBuffer::Interleaved(samples.freeze());
796
797        let result = filter
798            .process(Some(FilterFrame::Audio(frame)))
799            .expect("process should succeed");
800        assert!(result.is_some());
801    }
802
803    #[test]
804    fn test_state_transitions() {
805        let config = ResampleConfig::new(48000, 44100);
806        let mut filter = ResampleFilter::new(NodeId(0), "test", config);
807
808        assert!(filter.set_state(NodeState::Processing).is_ok());
809        assert_eq!(filter.state(), NodeState::Processing);
810
811        assert!(filter.set_state(NodeState::Done).is_ok());
812        assert_eq!(filter.state(), NodeState::Done);
813
814        assert!(filter.reset().is_ok());
815        assert_eq!(filter.state(), NodeState::Idle);
816    }
817
818    #[test]
819    fn test_sinc_kernel_creation() {
820        let kernel = SincKernel::new(16, 6.0, 0.95);
821        assert_eq!(kernel.taps, 16);
822        assert_eq!(kernel.coefficients.len(), 16);
823
824        // Coefficients should be normalized
825        let sum: f64 = kernel.coefficients.iter().sum();
826        assert!((sum - 1.0).abs() < 0.01);
827    }
828
829    #[test]
830    fn test_resample_state_creation() {
831        let config = ResampleConfig::new(48000, 44100);
832        let state = ResampleState::new(&config, 2);
833
834        assert_eq!(state.channels, 2);
835        assert!(state.ratio > 1.0); // Downsampling
836    }
837
838    #[test]
839    fn test_f64_conversion_roundtrip() {
840        let original: f64 = 0.5;
841        let mut buffer = BytesMut::new();
842
843        ResampleFilter::f64_to_bytes(original, SampleFormat::F32, &mut buffer);
844        let converted = ResampleFilter::bytes_to_f64(&buffer, SampleFormat::F32);
845
846        assert!((original - converted).abs() < 0.0001);
847    }
848
849    #[test]
850    fn test_sample_clamping() {
851        let mut buffer = BytesMut::new();
852
853        // Test clamping of out-of-range values
854        ResampleFilter::f64_to_bytes(2.0, SampleFormat::F32, &mut buffer);
855        let clamped = ResampleFilter::bytes_to_f64(&buffer, SampleFormat::F32);
856        assert!((clamped - 1.0).abs() < f64::EPSILON);
857
858        buffer.clear();
859        ResampleFilter::f64_to_bytes(-2.0, SampleFormat::F32, &mut buffer);
860        let clamped = ResampleFilter::bytes_to_f64(&buffer, SampleFormat::F32);
861        assert!((clamped + 1.0).abs() < f64::EPSILON);
862    }
863
864    #[test]
865    fn test_resample_different_rates() {
866        let config = ResampleConfig::new(48000, 24000); // 2:1 downsampling
867        let mut filter = ResampleFilter::new(NodeId(0), "test", config);
868
869        let mut frame = AudioFrame::new(SampleFormat::F32, 48000, ChannelLayout::Mono);
870        let mut samples = BytesMut::new();
871        for i in 0..4800 {
872            let sample = (i as f64 * 0.01).sin() as f32;
873            samples.extend_from_slice(&sample.to_le_bytes());
874        }
875        frame.samples = AudioBuffer::Interleaved(samples.freeze());
876
877        let result = filter
878            .process(Some(FilterFrame::Audio(frame)))
879            .expect("process should succeed");
880        assert!(result.is_some());
881
882        if let Some(FilterFrame::Audio(output)) = result {
883            // Output should have approximately half the samples
884            let output_count = output.sample_count();
885            assert!(output_count > 0);
886            assert!(output_count < 4800);
887        }
888    }
889
890    #[test]
891    fn test_flush() {
892        let config = ResampleConfig::new(48000, 44100);
893        let mut filter = ResampleFilter::new(NodeId(0), "test", config);
894
895        // Process some samples first
896        let mut frame = AudioFrame::new(SampleFormat::F32, 48000, ChannelLayout::Mono);
897        let mut samples = BytesMut::new();
898        for _ in 0..1024 {
899            samples.extend_from_slice(&0.5f32.to_le_bytes());
900        }
901        frame.samples = AudioBuffer::Interleaved(samples.freeze());
902
903        let _ = filter.process(Some(FilterFrame::Audio(frame)));
904
905        // Flush should work without error
906        let flushed = filter.flush().expect("flush should succeed");
907        // May or may not have remaining samples depending on state
908        assert!(flushed.len() <= 1);
909    }
910}