Skip to main content

audio_engine_core/processor/
resampler.rs

1//! High-quality resampling using SoX VHQ Polyphase implementation
2
3use crate::config::{PhaseResponse, ResampleQuality};
4use rayon::prelude::*;
5use soxr::{
6    format::Mono,
7    params::{QualityFlags, QualityRecipe, QualitySpec, Rolloff, RuntimeSpec},
8    Soxr,
9};
10
11/// Error type for resampler operations
12#[derive(Debug, Clone)]
13pub enum ResamplerError {
14    /// Soxr initialization failed (e.g., invalid sample rate combination)
15    InitializationFailed(String),
16    /// Processing failed
17    ProcessFailed(String),
18}
19
20impl std::fmt::Display for ResamplerError {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        match self {
23            ResamplerError::InitializationFailed(msg) => {
24                write!(f, "Soxr initialization failed: {}", msg)
25            }
26            ResamplerError::ProcessFailed(msg) => write!(f, "Resampling process failed: {}", msg),
27        }
28    }
29}
30
31impl std::error::Error for ResamplerError {}
32
33/// High-quality resampler using SoX (VHQ Polyphase implementation)
34pub struct Resampler {
35    channels: usize,
36    from_rate: u32,
37    to_rate: u32,
38}
39
40/// Convert ResampleQuality enum to SoX QualityRecipe
41/// FIX for Defect 30: Actually use different quality levels
42/// Note: QualityRecipe has Low variant, plus high() and very_high() constructor functions
43fn quality_to_recipe(quality: ResampleQuality) -> QualityRecipe {
44    match quality {
45        ResampleQuality::Low => QualityRecipe::Low, // Fast, lower quality (enum variant)
46        ResampleQuality::Standard => QualityRecipe::high(), // High quality (constructor)
47        ResampleQuality::High => QualityRecipe::high(), // High quality (constructor)
48        ResampleQuality::UltraHigh => QualityRecipe::very_high(), // VHQ, slowest (constructor)
49    }
50}
51
52/// Create a QualitySpec with the given recipe and phase response
53fn make_quality_spec(recipe: QualityRecipe, phase: PhaseResponse) -> QualitySpec {
54    QualitySpec::configure(recipe, Rolloff::default(), QualityFlags::HighPrecisionClock)
55        .with_phase_response(phase.to_soxr_value())
56}
57
58fn deinterleave_frame_major(
59    input: &[f64],
60    channels: usize,
61    frames: usize,
62    channel_inputs: &mut [Vec<f64>],
63) {
64    for frame in input[..frames * channels].chunks_exact(channels) {
65        for (ch, &sample) in frame.iter().enumerate() {
66            channel_inputs[ch].push(sample);
67        }
68    }
69}
70
71fn channel_outputs_have_frames(
72    channel_outputs: &[Vec<f64>],
73    channels: usize,
74    frames: usize,
75) -> bool {
76    channel_outputs
77        .iter()
78        .take(channels)
79        .all(|channel| channel.len() >= frames)
80}
81
82fn interleave_channel_outputs_to_vec(
83    channel_outputs: &[Vec<f64>],
84    channels: usize,
85    output: &mut Vec<f64>,
86) -> usize {
87    if channel_outputs.is_empty() || channel_outputs[0].is_empty() {
88        output.clear();
89        return 0;
90    }
91
92    let out_frames = channel_outputs[0].len();
93    output.clear();
94    output.reserve(out_frames * channels);
95
96    if channel_outputs_have_frames(channel_outputs, channels, out_frames) {
97        for frame in 0..out_frames {
98            for channel in channel_outputs.iter().take(channels) {
99                output.push(channel[frame]);
100            }
101        }
102    } else {
103        for frame in 0..out_frames {
104            for channel in channel_outputs.iter().take(channels) {
105                output.push(channel.get(frame).copied().unwrap_or(0.0));
106            }
107        }
108    }
109
110    out_frames
111}
112
113fn interleave_channel_outputs_to_vec_with_max_frames(
114    channel_outputs: &[Vec<f64>],
115    channels: usize,
116    output: &mut Vec<f64>,
117) -> usize {
118    let out_frames = channel_outputs
119        .iter()
120        .take(channels)
121        .map(Vec::len)
122        .max()
123        .unwrap_or(0);
124    output.clear();
125    if out_frames == 0 {
126        return 0;
127    }
128
129    output.reserve(out_frames * channels);
130    for frame in 0..out_frames {
131        for channel in channel_outputs.iter().take(channels) {
132            output.push(channel.get(frame).copied().unwrap_or(0.0));
133        }
134    }
135
136    out_frames
137}
138
139fn interleave_channel_outputs_to_slice(
140    channel_outputs: &[Vec<f64>],
141    channels: usize,
142    output: &mut [f64],
143) -> usize {
144    if channel_outputs.is_empty() || channel_outputs[0].is_empty() {
145        return 0;
146    }
147
148    let out_frames = channel_outputs[0].len();
149
150    if output.len() >= out_frames * channels
151        && channel_outputs_have_frames(channel_outputs, channels, out_frames)
152    {
153        for (frame, out_frame) in output
154            .chunks_exact_mut(channels)
155            .take(out_frames)
156            .enumerate()
157        {
158            for (dst, channel) in out_frame
159                .iter_mut()
160                .zip(channel_outputs.iter().take(channels))
161            {
162                *dst = channel[frame];
163            }
164        }
165    } else {
166        for frame in 0..out_frames {
167            for (ch, channel) in channel_outputs.iter().take(channels).enumerate() {
168                let idx = frame * channels + ch;
169                if idx < output.len() {
170                    output[idx] = channel.get(frame).copied().unwrap_or(0.0);
171                }
172            }
173        }
174    }
175
176    out_frames
177}
178
179impl Resampler {
180    pub fn new(channels: usize, from_rate: u32, to_rate: u32) -> Self {
181        Self {
182            channels,
183            from_rate,
184            to_rate,
185        }
186    }
187
188    /// Resample audio data using SoX VHQ polyphase filter.
189    ///
190    /// Input and output are interleaved f64 samples for Hi-Fi transparency.
191    ///
192    /// Optimised for multi-channel parallelism:
193    /// - De-interleaves channels
194    /// - Processes each channel on a separate thread (Rayon)
195    /// - Re-interleaves result
196    ///
197    /// This avoids phase discontinuities from time-chunking while maintaining high performance.
198    ///
199    /// Returns Err if Soxr initialization fails (e.g., invalid sample rate combination).
200    pub fn resample_parallel(
201        &self,
202        input: &[f64],
203        phase: PhaseResponse,
204        quality: ResampleQuality,
205    ) -> Result<Vec<f64>, ResamplerError> {
206        if self.from_rate == self.to_rate {
207            return Ok(input.to_vec());
208        }
209
210        // Validate sample rates
211        if self.from_rate == 0 || self.to_rate == 0 {
212            return Err(ResamplerError::InitializationFailed(format!(
213                "Invalid sample rate: from_rate={}, to_rate={}",
214                self.from_rate, self.to_rate
215            )));
216        }
217
218        // 1. De-interleave
219        let frames = input.len() / self.channels;
220        let mut plan_channels: Vec<Vec<f64>> = vec![Vec::with_capacity(frames); self.channels];
221        deinterleave_frame_major(input, self.channels, frames, &mut plan_channels);
222
223        // 2. Process channels in parallel
224        let resampled_channels: Result<Vec<Vec<f64>>, ResamplerError> = plan_channels
225            .into_par_iter()
226            .enumerate()
227            .map(|(ch_idx, channel_data)| {
228                // Configure SoX for this channel with phase response and quality
229                // FIX for Defect 30: Use quality parameter instead of hardcoded very_high
230                let quality_spec = make_quality_spec(quality_to_recipe(quality), phase);
231
232                let runtime_spec = RuntimeSpec::new(1); // 1 channel per thread
233
234                let mut soxr = Soxr::<Mono<f64>>::new_with_params(
235                    self.from_rate as f64,
236                    self.to_rate as f64,
237                    quality_spec,
238                    runtime_spec,
239                )
240                .map_err(|e| {
241                    ResamplerError::InitializationFailed(format!("Channel {}: {:?}", ch_idx, e))
242                })?;
243
244                // Output estimation
245                let expected_frames = (channel_data.len() as f64 * self.to_rate as f64
246                    / self.from_rate as f64)
247                    .ceil() as usize
248                    + 100;
249                let mut channel_output = Vec::with_capacity(expected_frames);
250
251                // Chunked processing to avoid massive single-pass overhead
252                // 8192 frames is a good balance for cache usage
253                let inner_chunk_size = 8192;
254                let mut output_scratch = vec![0.0; (inner_chunk_size as f64 * 1.5) as usize]; // Spare room for resampling ratio
255
256                let total_chunks = channel_data.len() / inner_chunk_size + 1;
257
258                // Log only for first channel to avoid spam
259                if ch_idx == 0 {
260                    log::info!(
261                        "Starting resampling on thread. Total chunks: {}, Phase: {:?}",
262                        total_chunks,
263                        phase
264                    );
265                }
266
267                for (i, chunk) in channel_data.chunks(inner_chunk_size).enumerate() {
268                    let processed = soxr.process(chunk, &mut output_scratch).map_err(|e| {
269                        ResamplerError::ProcessFailed(format!(
270                            "Channel {} chunk {}: {:?}",
271                            ch_idx, i, e
272                        ))
273                    })?;
274
275                    if processed.output_frames > 0 {
276                        channel_output
277                            .extend_from_slice(&output_scratch[..processed.output_frames]);
278                    }
279
280                    // Periodic log check (every ~10%)
281                    if ch_idx == 0 && i > 0 && i % (total_chunks.max(10) / 10).max(1) == 0 {
282                        log::debug!("Resampling progress: {}%", i * 100 / total_chunks);
283                    }
284                }
285
286                // Flush the resampler (pass empty slice)
287                let mut flush_scratch = vec![0.0; 4096];
288                if let Ok(processed) = soxr.process(&[], &mut flush_scratch) {
289                    if processed.output_frames > 0 {
290                        channel_output.extend_from_slice(&flush_scratch[..processed.output_frames]);
291                    }
292                }
293
294                Ok(channel_output)
295            })
296            .collect();
297
298        let resampled_channels = resampled_channels?;
299
300        // 3. Re-interleave
301        if resampled_channels.is_empty() {
302            return Ok(Vec::new());
303        }
304
305        let mut final_output = Vec::with_capacity(resampled_channels[0].len() * self.channels);
306        interleave_channel_outputs_to_vec(&resampled_channels, self.channels, &mut final_output);
307
308        Ok(final_output)
309    }
310}
311
312/// Stateful streaming resampler that maintains SoX instances across chunks.
313/// This is used by AudioPipeline for memory-efficient streaming resampling.
314///
315/// FIX for Defect 33: Pre-allocate all buffers to avoid heap allocation in process_chunk
316pub struct StreamingResampler {
317    soxr_instances: Vec<Soxr<Mono<f64>>>,
318    channels: usize,
319    from_rate: u32,
320    to_rate: u32,
321    /// Pre-allocated output scratch buffer (per channel, reused)
322    output_scratch: Vec<f64>,
323    /// Pre-allocated channel input buffers (Defect 33 fix)
324    channel_inputs: Vec<Vec<f64>>,
325    /// Pre-allocated channel output buffers (Defect 33 fix)
326    channel_outputs: Vec<Vec<f64>>,
327    /// Pre-allocated interleaved output buffer (Defect 33 fix)
328    interleaved_output: Vec<f64>,
329}
330
331pub struct ResampleOutput<'a> {
332    pub samples: &'a [f64],
333    pub frames: usize,
334}
335
336impl StreamingResampler {
337    pub fn from_rate(&self) -> u32 {
338        self.from_rate
339    }
340
341    pub fn to_rate(&self) -> u32 {
342        self.to_rate
343    }
344
345    /// Naive ratio upper bound, in interleaved samples, on the output a single
346    /// `process_chunk_*` call can return.
347    ///
348    /// Per-call Soxr output is now capped (via slicing the pre-allocated scratch
349    /// to this bound) inside `process_chunk_*`, so this is a *valid* single-call
350    /// ceiling: any excess input is buffered by Soxr and recovered on later
351    /// calls. Callers reserving an output/leftover buffer for one render-loop
352    /// input chunk should size it with this method.
353    pub fn max_output_len_for_input(&self, input_samples: usize) -> usize {
354        if self.channels == 0 {
355            return 0;
356        }
357        let input_frames = input_samples / self.channels;
358        let ratio = self.to_rate as f64 / self.from_rate as f64;
359        (input_frames as f64 * ratio).ceil() as usize * self.channels + self.channels * 64
360    }
361
362    /// Absolute hard upper bound, in interleaved samples, on the output a single
363    /// `process_chunk_*` call can return for this resampler.
364    ///
365    /// This equals the pre-allocated `interleaved_output` capacity. With per-call
366    /// output now capped to the naive ratio bound (see `max_output_len_for_input`),
367    /// `max_output_len_for_input` is the right size for per-call reserves; this
368    /// method remains the absolute ceiling tied to the internal buffer capacity.
369    pub fn max_output_samples_per_chunk(&self) -> usize {
370        self.interleaved_output.capacity()
371    }
372
373    pub fn input_frames_for_output_frames(&self, output_frames: usize) -> usize {
374        if output_frames == 0 || self.to_rate == 0 {
375            return 0;
376        }
377
378        let ratio = self.from_rate as f64 / self.to_rate as f64;
379        (output_frames as f64 * ratio).ceil() as usize + 64
380    }
381
382    /// Create a new streaming resampler with default (linear) phase and High quality
383    pub fn new(channels: usize, from_rate: u32, to_rate: u32) -> Result<Self, ResamplerError> {
384        Self::with_phase(channels, from_rate, to_rate, PhaseResponse::default())
385    }
386
387    /// Create a new streaming resampler with specified phase response (High quality)
388    ///
389    /// Returns Err if Soxr initialization fails (e.g., invalid sample rates like 0 Hz)
390    pub fn with_phase(
391        channels: usize,
392        from_rate: u32,
393        to_rate: u32,
394        phase: PhaseResponse,
395    ) -> Result<Self, ResamplerError> {
396        Self::with_quality(channels, from_rate, to_rate, phase, ResampleQuality::High)
397    }
398
399    /// Create a new streaming resampler with specified phase response and quality level
400    ///
401    /// FIX for Defect 30: Allow quality configuration
402    /// FIX for Defect 33: Pre-allocate all buffers to avoid heap allocation in process_chunk
403    ///
404    /// Returns Err if Soxr initialization fails (e.g., invalid sample rates like 0 Hz)
405    pub fn with_quality(
406        channels: usize,
407        from_rate: u32,
408        to_rate: u32,
409        phase: PhaseResponse,
410        quality: ResampleQuality,
411    ) -> Result<Self, ResamplerError> {
412        // Validate sample rates before creating Soxr instances
413        if from_rate == 0 || to_rate == 0 {
414            return Err(ResamplerError::InitializationFailed(format!(
415                "Invalid sample rate: from_rate={}, to_rate={}",
416                from_rate, to_rate
417            )));
418        }
419
420        let mut soxr_instances = Vec::with_capacity(channels);
421        for ch_idx in 0..channels {
422            // Create params for each channel with phase response and quality
423            // FIX for Defect 30: Use quality parameter
424            let quality_spec = make_quality_spec(quality_to_recipe(quality), phase);
425            let runtime_spec = RuntimeSpec::new(1);
426
427            match Soxr::<Mono<f64>>::new_with_params(
428                from_rate as f64,
429                to_rate as f64,
430                quality_spec,
431                runtime_spec,
432            ) {
433                Ok(soxr) => soxr_instances.push(soxr),
434                Err(e) => {
435                    return Err(ResamplerError::InitializationFailed(format!(
436                        "Soxr failed for channel {}: {:?} (from={}Hz, to={}Hz)",
437                        ch_idx, e, from_rate, to_rate
438                    )));
439                }
440            }
441        }
442
443        // Pre-allocate all buffers (Defect 33 fix)
444        let max_input_frames = 16384; // Typical chunk size
445                                      // True conversion ratio. Per-call Soxr output is now capped (via slicing)
446                                      // to the naive ratio bound, so this sizes the scratch/output buffers to the
447                                      // real worst case for this direction (downsample ratios < 1 no longer
448                                      // over-reserve the old conservative 2.0).
449        let max_ratio = to_rate as f64 / from_rate as f64;
450        let max_output_per_channel = (max_input_frames as f64 * max_ratio).ceil() as usize + 64;
451
452        // Pre-allocate channel buffers
453        let channel_inputs: Vec<Vec<f64>> = (0..channels)
454            .map(|_| Vec::with_capacity(max_input_frames))
455            .collect();
456        let channel_outputs: Vec<Vec<f64>> = (0..channels)
457            .map(|_| Vec::with_capacity(max_output_per_channel))
458            .collect();
459        let interleaved_output = Vec::with_capacity(max_output_per_channel * channels);
460
461        Ok(Self {
462            soxr_instances,
463            channels,
464            from_rate,
465            to_rate,
466            output_scratch: vec![0.0; max_output_per_channel],
467            channel_inputs,
468            channel_outputs,
469            interleaved_output,
470        })
471    }
472
473    fn process_chunk_to_internal_output(&mut self, input: &[f64]) -> usize {
474        // Clear and reuse pre-allocated channel input buffers (Defect 33 fix)
475        for ch_buf in &mut self.channel_inputs {
476            ch_buf.clear();
477        }
478
479        let input_frames = input.len() / self.channels;
480
481        // De-interleave input into pre-allocated buffers. Trailing incomplete
482        // frames are intentionally ignored, matching `input.len() / channels`.
483        deinterleave_frame_major(input, self.channels, input_frames, &mut self.channel_inputs);
484
485        // Clear and reuse pre-allocated channel output buffers (Defect 33 fix)
486        for ch_buf in &mut self.channel_outputs {
487            ch_buf.clear();
488        }
489
490        // Process each channel
491        for (ch, channel_data) in self.channel_inputs.iter().enumerate() {
492            // Naive ratio bound for this call. We never resize on the audio thread;
493            // instead we cap Soxr's output by slicing the pre-allocated scratch.
494            // Capping to the naive bound keeps `max_output_len_for_input` a valid
495            // single-call ceiling. The `.min(len)` keeps it panic-safe for offline
496            // callers feeding chunks larger than `max_input_frames` — Soxr buffers
497            // the excess and recovers it on later process/flush calls.
498            let expected_output = (channel_data.len() as f64 * self.to_rate as f64
499                / self.from_rate as f64)
500                .ceil() as usize
501                + 64;
502            let cap = expected_output.min(self.output_scratch.len());
503
504            let processed = match self.soxr_instances[ch]
505                .process(channel_data, &mut self.output_scratch[..cap])
506            {
507                Ok(p) => p,
508                Err(e) => {
509                    log::error!(
510                        "Resampler process_chunk failed (ch={}, in_frames={}): {:?}",
511                        ch,
512                        channel_data.len(),
513                        e
514                    );
515                    self.interleaved_output.clear();
516                    return 0;
517                }
518            };
519
520            self.channel_outputs[ch]
521                .extend_from_slice(&self.output_scratch[..processed.output_frames]);
522        }
523
524        interleave_channel_outputs_to_vec(
525            &self.channel_outputs,
526            self.channels,
527            &mut self.interleaved_output,
528        )
529    }
530
531    /// Process a chunk of interleaved audio and borrow the resampler-owned output.
532    ///
533    /// Resampling processes only complete input frames; trailing samples where
534    /// `input.len() % channels != 0` are ignored to preserve existing behavior.
535    /// The equal-rate bypass returns the original input slice unchanged.
536    /// The borrowed slice remains valid until the next mutable resampler call.
537    pub fn process_chunk_borrowed<'a>(&'a mut self, input: &'a [f64]) -> ResampleOutput<'a> {
538        if self.from_rate == self.to_rate {
539            return ResampleOutput {
540                samples: input,
541                frames: input.len() / self.channels,
542            };
543        }
544
545        let input_frames = input.len() / self.channels;
546        if input_frames == 0 {
547            self.interleaved_output.clear();
548            return ResampleOutput {
549                samples: &self.interleaved_output,
550                frames: 0,
551            };
552        }
553
554        let frames = self.process_chunk_to_internal_output(input);
555        ResampleOutput {
556            samples: &self.interleaved_output,
557            frames,
558        }
559    }
560
561    /// Process a chunk and append the result directly to a caller-owned buffer.
562    pub fn process_chunk_append(&mut self, input: &[f64], output: &mut Vec<f64>) -> usize {
563        let result = self.process_chunk_borrowed(input);
564        output.extend_from_slice(result.samples);
565        result.frames
566    }
567
568    /// Process a chunk into a pre-allocated output buffer (zero-allocation version)
569    ///
570    /// Returns the number of frames written to output.
571    /// Output buffer must be large enough: output.len() >= input.len() * to_rate / from_rate + 64
572    pub fn process_chunk_into(&mut self, input: &[f64], output: &mut [f64]) -> usize {
573        if self.from_rate == self.to_rate {
574            let copy_len = input.len().min(output.len());
575            output[..copy_len].copy_from_slice(&input[..copy_len]);
576            return copy_len / self.channels;
577        }
578
579        let input_frames = input.len() / self.channels;
580        if input_frames == 0 {
581            return 0;
582        }
583
584        // Clear and reuse pre-allocated buffers
585        for ch_buf in &mut self.channel_inputs {
586            ch_buf.clear();
587        }
588
589        // De-interleave complete frames only, preserving truncation semantics
590        // for `input.len() % channels != 0`.
591        deinterleave_frame_major(input, self.channels, input_frames, &mut self.channel_inputs);
592
593        // Clear output buffers
594        for ch_buf in &mut self.channel_outputs {
595            ch_buf.clear();
596        }
597
598        // Process each channel
599        for (ch, channel_data) in self.channel_inputs.iter().enumerate() {
600            // See process_chunk_to_internal_output: cap Soxr output via slicing
601            // instead of resizing on the audio thread.
602            let expected_output = (channel_data.len() as f64 * self.to_rate as f64
603                / self.from_rate as f64)
604                .ceil() as usize
605                + 64;
606            let cap = expected_output.min(self.output_scratch.len());
607
608            let processed = match self.soxr_instances[ch]
609                .process(channel_data, &mut self.output_scratch[..cap])
610            {
611                Ok(p) => p,
612                Err(e) => {
613                    log::error!(
614                        "Resampler process_chunk_into failed (ch={}, in_frames={}): {:?}",
615                        ch,
616                        channel_data.len(),
617                        e
618                    );
619                    return 0;
620                }
621            };
622
623            self.channel_outputs[ch]
624                .extend_from_slice(&self.output_scratch[..processed.output_frames]);
625        }
626
627        interleave_channel_outputs_to_slice(&self.channel_outputs, self.channels, output)
628    }
629
630    pub fn reset(&mut self) {
631        for ch_buf in &mut self.channel_inputs {
632            ch_buf.clear();
633        }
634        for ch_buf in &mut self.channel_outputs {
635            ch_buf.clear();
636        }
637        self.interleaved_output.clear();
638    }
639
640    /// Flush remaining samples and borrow the resampler-owned interleaved output.
641    pub fn flush_borrowed(&mut self) -> ResampleOutput<'_> {
642        for channel_output in &mut self.channel_outputs {
643            channel_output.clear();
644        }
645
646        for ch in 0..self.channels {
647            // Keep flushing until no more output
648            loop {
649                match self.soxr_instances[ch].process(&[], &mut self.output_scratch) {
650                    Ok(processed) if processed.output_frames > 0 => {
651                        self.channel_outputs[ch]
652                            .extend_from_slice(&self.output_scratch[..processed.output_frames]);
653                    }
654                    _ => break,
655                }
656            }
657        }
658
659        let frames = interleave_channel_outputs_to_vec_with_max_frames(
660            &self.channel_outputs,
661            self.channels,
662            &mut self.interleaved_output,
663        );
664        ResampleOutput {
665            samples: &self.interleaved_output,
666            frames,
667        }
668    }
669
670    /// Flush any remaining samples directly into a caller-owned output buffer.
671    pub fn flush_into(&mut self, output: &mut Vec<f64>) -> usize {
672        let result = self.flush_borrowed();
673        output.extend_from_slice(result.samples);
674        result.frames
675    }
676
677    /// Flush any remaining samples in the resampler's internal buffers
678    pub fn flush(&mut self) -> Vec<f64> {
679        self.flush_borrowed().samples.to_vec()
680    }
681}
682
683#[cfg(test)]
684mod tests {
685    use super::*;
686
687    #[test]
688    fn deinterleave_frame_major_preserves_order_and_truncates_partial_frame() {
689        let input = vec![1.0, 2.0, 3.0, 4.0, 5.0, 99.0];
690        let mut channels = vec![Vec::new(), Vec::new()];
691
692        deinterleave_frame_major(&input, 2, input.len() / 2, &mut channels);
693
694        assert_eq!(channels[0], vec![1.0, 3.0, 5.0]);
695        assert_eq!(channels[1], vec![2.0, 4.0, 99.0]);
696
697        let input = vec![1.0, 2.0, 3.0, 4.0, 5.0];
698        let mut channels = vec![Vec::new(), Vec::new()];
699
700        deinterleave_frame_major(&input, 2, input.len() / 2, &mut channels);
701
702        assert_eq!(channels[0], vec![1.0, 3.0]);
703        assert_eq!(channels[1], vec![2.0, 4.0]);
704    }
705
706    #[test]
707    fn interleave_channel_outputs_fast_path_preserves_multichannel_order() {
708        let channel_outputs = vec![
709            vec![1.0, 4.0, 7.0],
710            vec![2.0, 5.0, 8.0],
711            vec![3.0, 6.0, 9.0],
712        ];
713        let mut output = Vec::new();
714
715        let frames = interleave_channel_outputs_to_vec(&channel_outputs, 3, &mut output);
716
717        assert_eq!(frames, 3);
718        assert_eq!(output, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]);
719    }
720
721    #[test]
722    fn interleave_channel_outputs_pads_short_channels() {
723        let channel_outputs = vec![vec![1.0, 3.0, 5.0], vec![2.0]];
724        let mut output = Vec::new();
725
726        let frames = interleave_channel_outputs_to_vec(&channel_outputs, 2, &mut output);
727
728        assert_eq!(frames, 3);
729        assert_eq!(output, vec![1.0, 2.0, 3.0, 0.0, 5.0, 0.0]);
730    }
731
732    #[test]
733    fn interleave_channel_outputs_with_max_frames_pads_short_channels() {
734        let channel_outputs = vec![vec![1.0], vec![2.0, 4.0, 6.0]];
735        let mut output = Vec::new();
736
737        let frames =
738            interleave_channel_outputs_to_vec_with_max_frames(&channel_outputs, 2, &mut output);
739
740        assert_eq!(frames, 3);
741        assert_eq!(output, vec![1.0, 2.0, 0.0, 4.0, 0.0, 6.0]);
742    }
743
744    #[test]
745    fn interleave_channel_outputs_to_slice_preserves_tail_when_output_is_longer() {
746        let channel_outputs = vec![vec![1.0, 3.0], vec![2.0, 4.0]];
747        let mut output = vec![42.0; 6];
748
749        let frames = interleave_channel_outputs_to_slice(&channel_outputs, 2, &mut output);
750
751        assert_eq!(frames, 2);
752        assert_eq!(output, vec![1.0, 2.0, 3.0, 4.0, 42.0, 42.0]);
753    }
754
755    #[test]
756    fn process_chunk_borrowed_equal_rate_reports_complete_frames_and_full_input() {
757        let mut resampler = StreamingResampler::new(2, 48_000, 48_000).unwrap();
758        let input = vec![1.0, 2.0, 3.0, 4.0, 5.0];
759
760        let result = resampler.process_chunk_borrowed(&input);
761
762        assert_eq!(result.frames, 2);
763        assert_eq!(result.samples, input.as_slice());
764    }
765
766    #[test]
767    fn input_frames_for_output_frames_tracks_rate_ratio_with_margin() {
768        let upsampler = StreamingResampler::new(2, 44_100, 384_000).unwrap();
769        let expected_up = ((512.0_f64 * 44_100.0 / 384_000.0).ceil() as usize) + 64;
770        assert_eq!(upsampler.input_frames_for_output_frames(512), expected_up);
771
772        let downsampler = StreamingResampler::new(2, 96_000, 48_000).unwrap();
773        assert_eq!(downsampler.input_frames_for_output_frames(2112), 4288);
774        assert_eq!(downsampler.input_frames_for_output_frames(0), 0);
775    }
776
777    #[test]
778    fn process_chunk_append_equal_rate_preserves_prefix_and_full_input() {
779        let mut resampler = StreamingResampler::new(2, 48_000, 48_000).unwrap();
780        let input = vec![1.0, 2.0, 3.0, 4.0, 5.0];
781        let mut output = vec![-1.0, -2.0];
782
783        let frames = resampler.process_chunk_append(&input, &mut output);
784
785        assert_eq!(frames, 2);
786        assert_eq!(output, vec![-1.0, -2.0, 1.0, 2.0, 3.0, 4.0, 5.0]);
787    }
788
789    #[test]
790    fn process_chunk_append_matches_borrowed_for_resampling() {
791        let input = (0..2048)
792            .map(|sample| sample as f64 / 2048.0)
793            .collect::<Vec<_>>();
794        let mut borrowed_resampler = StreamingResampler::new(2, 44_100, 48_000).unwrap();
795        let mut append_resampler = StreamingResampler::new(2, 44_100, 48_000).unwrap();
796        let expected = borrowed_resampler
797            .process_chunk_borrowed(&input)
798            .samples
799            .to_vec();
800        let mut actual = vec![99.0];
801
802        let frames = append_resampler.process_chunk_append(&input, &mut actual);
803
804        assert_eq!(frames * 2, expected.len());
805        assert_eq!(&actual[..1], &[99.0]);
806        assert_eq!(&actual[1..], expected.as_slice());
807    }
808
809    #[test]
810    fn process_chunk_into_reuses_internal_capacity_after_warmup() {
811        let input = (0..4096)
812            .map(|sample| sample as f64 / 4096.0)
813            .collect::<Vec<_>>();
814        let mut resampler = StreamingResampler::new(2, 44_100, 48_000).unwrap();
815        let mut output = vec![0.0; resampler.max_output_len_for_input(input.len())];
816
817        let _ = resampler.process_chunk_into(&input, &mut output);
818        let warmed_input_caps = resampler
819            .channel_inputs
820            .iter()
821            .map(Vec::capacity)
822            .collect::<Vec<_>>();
823        let warmed_output_caps = resampler
824            .channel_outputs
825            .iter()
826            .map(Vec::capacity)
827            .collect::<Vec<_>>();
828        let warmed_interleaved_cap = resampler.interleaved_output.capacity();
829        let warmed_scratch_len = resampler.output_scratch.len();
830
831        let _ = resampler.process_chunk_into(&input, &mut output);
832
833        assert_eq!(
834            resampler
835                .channel_inputs
836                .iter()
837                .map(Vec::capacity)
838                .collect::<Vec<_>>(),
839            warmed_input_caps
840        );
841        assert_eq!(
842            resampler
843                .channel_outputs
844                .iter()
845                .map(Vec::capacity)
846                .collect::<Vec<_>>(),
847            warmed_output_caps
848        );
849        assert_eq!(
850            resampler.interleaved_output.capacity(),
851            warmed_interleaved_cap
852        );
853        assert_eq!(resampler.output_scratch.len(), warmed_scratch_len);
854    }
855
856    #[test]
857    fn flush_into_matches_flush_and_preserves_prefix() {
858        let input = (0..2048)
859            .map(|sample| sample as f64 / 2048.0)
860            .collect::<Vec<_>>();
861        let mut wrapper_resampler = StreamingResampler::new(2, 44_100, 48_000).unwrap();
862        let mut append_resampler = StreamingResampler::new(2, 44_100, 48_000).unwrap();
863        let mut scratch = Vec::new();
864        let _ = wrapper_resampler.process_chunk_append(&input, &mut scratch);
865        scratch.clear();
866        let _ = append_resampler.process_chunk_append(&input, &mut scratch);
867        let expected = wrapper_resampler.flush();
868        let mut actual = vec![99.0];
869
870        let frames = append_resampler.flush_into(&mut actual);
871
872        assert_eq!(frames * 2, expected.len());
873        assert_eq!(&actual[..1], &[99.0]);
874        assert_eq!(&actual[1..], expected.as_slice());
875    }
876
877    #[test]
878    fn flush_into_reuses_warmed_output_capacity() {
879        let input = (0..4096)
880            .map(|sample| sample as f64 / 4096.0)
881            .collect::<Vec<_>>();
882        let mut resampler = StreamingResampler::new(2, 44_100, 48_000).unwrap();
883        let mut scratch = Vec::new();
884        let _ = resampler.process_chunk_append(&input, &mut scratch);
885        let mut output = Vec::with_capacity(resampler.max_output_len_for_input(input.len()));
886
887        let _ = resampler.flush_into(&mut output);
888        let warmed_capacity = output.capacity();
889        output.clear();
890        scratch.clear();
891        let _ = resampler.process_chunk_append(&input, &mut scratch);
892        let _ = resampler.flush_into(&mut output);
893
894        assert_eq!(output.capacity(), warmed_capacity);
895    }
896
897    #[test]
898    fn flush_into_reuses_internal_capacity_after_warmup() {
899        let input = (0..4096)
900            .map(|sample| sample as f64 / 4096.0)
901            .collect::<Vec<_>>();
902        let mut resampler = StreamingResampler::new(2, 44_100, 48_000).unwrap();
903        let mut output = Vec::with_capacity(resampler.max_output_len_for_input(input.len()));
904        let mut scratch = Vec::new();
905        let _ = resampler.process_chunk_append(&input, &mut scratch);
906        let _ = resampler.flush_into(&mut output);
907        let warmed_channel_caps = resampler
908            .channel_outputs
909            .iter()
910            .map(Vec::capacity)
911            .collect::<Vec<_>>();
912        let warmed_interleaved_cap = resampler.interleaved_output.capacity();
913        let warmed_scratch_len = resampler.output_scratch.len();
914
915        output.clear();
916        scratch.clear();
917        let _ = resampler.process_chunk_append(&input, &mut scratch);
918        let _ = resampler.flush_into(&mut output);
919
920        assert_eq!(
921            resampler
922                .channel_outputs
923                .iter()
924                .map(Vec::capacity)
925                .collect::<Vec<_>>(),
926            warmed_channel_caps
927        );
928        assert_eq!(
929            resampler.interleaved_output.capacity(),
930            warmed_interleaved_cap
931        );
932        assert_eq!(resampler.output_scratch.len(), warmed_scratch_len);
933    }
934}