Skip to main content

kithara_audio/
resampler.rs

1use std::{
2    iter,
3    num::NonZeroUsize,
4    sync::{
5        Arc,
6        atomic::{AtomicU32, Ordering},
7    },
8};
9
10use audioadapter_buffers::direct::SequentialSliceOfVecs;
11use bon::Builder;
12use fast_interleave::{deinterleave_variable, interleave_variable};
13use kithara_bufpool::{PcmBuf, PcmPool};
14use kithara_decode::{PcmChunk, PcmMeta, PcmSpec};
15use portable_atomic::AtomicF32;
16use rubato::{
17    Async, Fft, FixedAsync, FixedSync, PolynomialDegree, Resampler as RubatoResampler,
18    SincInterpolationParameters, SincInterpolationType, WindowFunction,
19};
20use smallvec::SmallVec;
21use tracing::{debug, info, trace};
22
23use crate::traits::AudioEffect;
24
25/// Quality preset for the audio resampler.
26///
27/// Controls the resampling algorithm and interpolation parameters.
28/// Higher quality uses more CPU but produces better audio fidelity.
29#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
30pub enum ResamplerQuality {
31    /// Fastest resampling using polynomial interpolation.
32    /// Suitable for previews or low-power devices.
33    Fast,
34    /// Balanced sinc resampling (64-tap, linear interpolation).
35    Normal,
36    /// Good sinc resampling (128-tap, linear interpolation).
37    Good,
38    /// High sinc resampling (256-tap, cubic interpolation).
39    /// Recommended for music playback.
40    #[default]
41    High,
42    /// Maximum quality using FFT-based resampling.
43    /// Highest CPU usage, best for offline processing or high-end playback.
44    Maximum,
45}
46
47impl From<ResamplerQuality> for SincInterpolationParameters {
48    fn from(quality: ResamplerQuality) -> Self {
49        /// Oversampling factor for good/high quality sinc filters.
50        const OVERSAMPLING_HIGH: usize = 256;
51        /// Oversampling factor for normal quality sinc filter.
52        const OVERSAMPLING_NORMAL: usize = 128;
53        /// Cutoff frequency ratio for sinc filters.
54        const CUTOFF: f32 = 0.95;
55        /// Sinc filter length for good quality (128-tap).
56        const LEN_GOOD: usize = 128;
57        /// Sinc filter length for high quality (256-tap).
58        const LEN_HIGH: usize = 256;
59        /// Sinc filter length for normal quality (64-tap).
60        const LEN_NORMAL: usize = 64;
61
62        match quality {
63            ResamplerQuality::Good => Self {
64                sinc_len: LEN_GOOD,
65                f_cutoff: CUTOFF,
66                interpolation: SincInterpolationType::Linear,
67                oversampling_factor: OVERSAMPLING_HIGH,
68                window: WindowFunction::BlackmanHarris2,
69            },
70            ResamplerQuality::High => Self {
71                sinc_len: LEN_HIGH,
72                f_cutoff: CUTOFF,
73                interpolation: SincInterpolationType::Cubic,
74                oversampling_factor: OVERSAMPLING_HIGH,
75                window: WindowFunction::BlackmanHarris2,
76            },
77            ResamplerQuality::Normal | ResamplerQuality::Fast | ResamplerQuality::Maximum => Self {
78                sinc_len: LEN_NORMAL,
79                f_cutoff: CUTOFF,
80                interpolation: SincInterpolationType::Linear,
81                oversampling_factor: OVERSAMPLING_NORMAL,
82                window: WindowFunction::BlackmanHarris2,
83            },
84        }
85    }
86}
87
88/// Enum wrapper for rubato resamplers (trait is not object-safe).
89enum ResamplerKind {
90    Poly(Async<f32>),
91    Sinc(Async<f32>),
92    Fft(Box<Fft<f32>>),
93}
94
95impl ResamplerKind {
96    // ast-grep-ignore: idioms.match-self-conversion
97    fn input_frames_next(&self) -> usize {
98        match self {
99            Self::Poly(r) | Self::Sinc(r) => r.input_frames_next(),
100            Self::Fft(r) => r.input_frames_next(),
101        }
102    }
103    // ast-grep-ignore: idioms.match-self-conversion
104    fn output_frames_next(&self) -> usize {
105        match self {
106            Self::Poly(r) | Self::Sinc(r) => r.output_frames_next(),
107            Self::Fft(r) => r.output_frames_next(),
108        }
109    }
110
111    fn process_into_buffer(
112        &mut self,
113        input: &[Vec<f32>],
114        output: &mut [Vec<f32>],
115    ) -> Result<(usize, usize), rubato::ResampleError> {
116        let channels = input.len();
117        let input_frames = input[0].len();
118        let output_frames = output[0].len();
119
120        let input_adapter =
121            SequentialSliceOfVecs::new(input, channels, input_frames).map_err(|_| {
122                rubato::ResampleError::InsufficientInputBufferSize {
123                    expected: input_frames,
124                    actual: 0,
125                }
126            })?;
127        let mut output_adapter = SequentialSliceOfVecs::new_mut(output, channels, output_frames)
128            .map_err(|_| rubato::ResampleError::InsufficientOutputBufferSize {
129                expected: output_frames,
130                actual: 0,
131            })?;
132
133        match self {
134            Self::Poly(r) | Self::Sinc(r) => {
135                r.process_into_buffer(&input_adapter, &mut output_adapter, None)
136            }
137            Self::Fft(r) => r.process_into_buffer(&input_adapter, &mut output_adapter, None),
138        }
139    }
140
141    fn set_resample_ratio(&mut self, ratio: f64, ramp: bool) -> Result<(), rubato::ResampleError> {
142        match self {
143            Self::Poly(r) | Self::Sinc(r) => r.set_resample_ratio(ratio, ramp),
144            Self::Fft(r) => r.set_resample_ratio(ratio, ramp),
145        }
146    }
147}
148
149/// Configuration parameters for the resampler effect.
150///
151/// Contains all values needed to construct a [`ResamplerProcessor`].
152#[derive(Builder)]
153#[builder(state_mod(vis = "pub"))]
154#[non_exhaustive]
155pub struct ResamplerParams {
156    /// Shared atomic for dynamic host sample rate tracking.
157    pub host_sample_rate: Arc<AtomicU32>,
158    /// Shared atomic for dynamic playback rate (1.0 = normal speed).
159    ///
160    /// Affects the resampling ratio: `ratio = host_rate / (source_rate × playback_rate)`.
161    /// At rate=2.0, audio plays at double speed with pitch shift (vinyl effect).
162    #[builder(default = Arc::new(AtomicF32::new(1.0)))]
163    pub playback_rate: Arc<AtomicF32>,
164    /// Shared PCM pool for output buffers.
165    pub pool: Option<PcmPool>,
166    /// Quality preset controlling resampling algorithm.
167    #[builder(default)]
168    pub quality: ResamplerQuality,
169    /// Initial source sample rate.
170    pub source_sample_rate: u32,
171    /// Number of audio channels.
172    pub channels: usize,
173    /// Number of input frames per resampler processing block.
174    #[builder(default = ResamplerProcessor::DEFAULT_CHUNK_SIZE)]
175    pub chunk_size: usize,
176}
177
178impl ResamplerParams {
179    /// Create resampler params with required runtime values and default settings.
180    pub fn new(host_sample_rate: Arc<AtomicU32>, source_sample_rate: u32, channels: usize) -> Self {
181        Self::builder()
182            .host_sample_rate(host_sample_rate)
183            .source_sample_rate(source_sample_rate)
184            .channels(channels)
185            .build()
186    }
187}
188
189/// Audio resampler that converts between source and host sample rates.
190///
191/// Monitors `host_sample_rate` (an `Arc<AtomicU32>`) and `playback_rate`
192/// (an `Arc<AtomicF32>`) for dynamic changes.
193/// When `host_sample_rate == 0` or equals `source_rate` and `playback_rate == 1.0`,
194/// operates in passthrough mode.
195pub struct ResamplerProcessor {
196    host_sample_rate: Arc<AtomicU32>,
197    /// Shared atomic for dynamic playback rate tracking.
198    playback_rate: Arc<AtomicF32>,
199    /// Most recently observed input `PcmMeta`. Carried over to each
200    /// resampled output chunk so the timeline still gets the decoder's
201    /// authoritative `timestamp` / `end_timestamp` after rate
202    /// conversion (rubato changes frame counts but not wall-clock
203    /// duration). `None` until the first input chunk arrives.
204    last_input_meta: Option<PcmMeta>,
205    resampler: Option<ResamplerKind>,
206    /// Pool for interleave output buffers.
207    pool: PcmPool,
208    output_spec: PcmSpec,
209    quality: ResamplerQuality,
210    /// Accumulated input buffer (planar format).
211    input_buffer: SmallVec<[Vec<f32>; 8]>,
212    temp_deinterleave: SmallVec<[Vec<f32>; 8]>,
213    temp_input_slice: SmallVec<[Vec<f32>; 8]>,
214    temp_output_all: SmallVec<[Vec<f32>; 8]>,
215    temp_output_bufs: SmallVec<[Vec<f32>; 8]>,
216    current_playback_rate: f64,
217    current_ratio: f64,
218    source_rate: u32,
219    channels: usize,
220    chunk_size: usize,
221}
222
223impl ResamplerProcessor {
224    /// Default resampler chunk size in frames.
225    const DEFAULT_CHUNK_SIZE: usize = 4096;
226
227    /// Sub-chunk count for FFT resampler.
228    const FFT_SUB_CHUNKS: usize = 2;
229
230    /// Maximum ratio adjustment factor for async resamplers.
231    const MAX_RATIO_ADJUSTMENT: f64 = 8.0;
232
233    /// Minimum playback rate to avoid division by zero or extreme ratios.
234    const MIN_PLAYBACK_RATE: f64 = 0.01;
235
236    /// Passthrough detection tolerance for playback rate.
237    const PASSTHROUGH_TOLERANCE: f64 = 0.0001;
238
239    /// Create a new resampler from configuration parameters.
240    pub fn new(params: ResamplerParams) -> Self {
241        let source_rate = params.source_sample_rate;
242        let channels = params.channels;
243        let host_sr = params.host_sample_rate.load(Ordering::Relaxed);
244        let target_rate = if host_sr == 0 { source_rate } else { host_sr };
245        let output_spec = PcmSpec {
246            channels: u16::try_from(channels).unwrap_or(u16::MAX),
247            sample_rate: target_rate,
248        };
249
250        let initial_playback_rate = f64::from(params.playback_rate.load(Ordering::Relaxed));
251
252        let mut processor = Self {
253            channels,
254            chunk_size: params.chunk_size,
255            current_playback_rate: initial_playback_rate,
256            current_ratio: 1.0,
257            host_sample_rate: params.host_sample_rate,
258            input_buffer: smallvec_new_vecs(channels),
259            output_spec,
260            playback_rate: params.playback_rate,
261            pool: params.pool.unwrap_or_else(|| PcmPool::default().clone()),
262            quality: params.quality,
263            resampler: None,
264            source_rate,
265            temp_deinterleave: smallvec_new_vecs(channels),
266            temp_input_slice: smallvec_new_vecs(channels),
267            temp_output_all: smallvec_new_vecs(channels),
268            temp_output_bufs: smallvec_new_vecs(channels),
269            last_input_meta: None,
270        };
271
272        processor.update_resampler_if_needed();
273
274        info!(
275            source_rate,
276            host_sr = host_sr,
277            target_rate,
278            channels,
279            active = !processor.is_passthrough(),
280            quality = ?params.quality,
281            "Resampler initialized"
282        );
283
284        processor
285    }
286
287    #[cfg_attr(feature = "perf", hotpath::measure)]
288    fn append_to_buffer(&mut self, interleaved: &[f32]) {
289        if interleaved.is_empty() {
290            return;
291        }
292
293        let frames = interleaved.len() / self.channels;
294
295        if self.temp_deinterleave.len() < self.channels {
296            self.temp_deinterleave.resize_with(self.channels, Vec::new);
297        }
298
299        for buf in &mut self.temp_deinterleave[..self.channels] {
300            buf.resize(frames, 0.0);
301        }
302
303        let num_channels = NonZeroUsize::new(self.channels).expect("channels must be > 0");
304        deinterleave_variable(
305            interleaved,
306            num_channels,
307            &mut self.temp_deinterleave[..self.channels],
308            0..frames,
309        );
310
311        for ch in 0..self.channels {
312            self.input_buffer[ch].extend_from_slice(&self.temp_deinterleave[ch][..frames]);
313        }
314    }
315
316    /// Assemble the final `PcmChunk` from a successful flush `process_block`.
317    fn build_flush_output(&mut self, buffered: usize, out_len: usize) -> Option<PcmChunk> {
318        for buf in &mut self.input_buffer {
319            buf.clear();
320        }
321
322        let frames_f64 =
323            (num_traits::cast::AsPrimitive::<f64>::as_(buffered) * self.current_ratio).ceil();
324        let actual_output_frames =
325            num_traits::cast::ToPrimitive::to_usize(&frames_f64).unwrap_or(usize::MAX);
326        let frames_to_use = actual_output_frames.min(out_len);
327
328        if frames_to_use == 0 {
329            return None;
330        }
331
332        for buf in &mut self.temp_output_all {
333            buf.clear();
334        }
335        for (ch, buf) in self.temp_output_bufs.iter().enumerate() {
336            self.temp_output_all[ch].extend_from_slice(&buf[..frames_to_use]);
337        }
338
339        let interleaved = self.interleave(&self.temp_output_all);
340        let mut meta = self.last_input_meta.unwrap_or_default();
341        meta.spec = self.output_spec;
342        let frame_count = interleaved
343            .len()
344            .checked_div(self.channels.max(1))
345            .unwrap_or(0);
346        let out_frames = u32::try_from(frame_count).unwrap_or(u32::MAX);
347        meta.frames = out_frames;
348        Some(PcmChunk::new(meta, interleaved))
349    }
350
351    fn create_resampler(
352        quality: ResamplerQuality,
353        ratio: f64,
354        chunk_size: usize,
355        channels: usize,
356        source_rate: u32,
357        target_rate: u32,
358    ) -> Result<ResamplerKind, rubato::ResamplerConstructionError> {
359        match quality {
360            ResamplerQuality::Fast => {
361                let poly = Async::new_poly(
362                    ratio,
363                    Self::MAX_RATIO_ADJUSTMENT,
364                    PolynomialDegree::Cubic,
365                    chunk_size,
366                    channels,
367                    FixedAsync::Input,
368                )?;
369                Ok(ResamplerKind::Poly(poly))
370            }
371            ResamplerQuality::Normal | ResamplerQuality::Good | ResamplerQuality::High => {
372                let sinc = Async::new_sinc(
373                    ratio,
374                    Self::MAX_RATIO_ADJUSTMENT,
375                    &SincInterpolationParameters::from(quality),
376                    chunk_size,
377                    channels,
378                    FixedAsync::Input,
379                )?;
380                Ok(ResamplerKind::Sinc(sinc))
381            }
382            ResamplerQuality::Maximum => {
383                let fft = Fft::new(
384                    source_rate as usize,
385                    target_rate as usize,
386                    chunk_size,
387                    Self::FFT_SUB_CHUNKS,
388                    channels,
389                    FixedSync::Input,
390                )?;
391                Ok(ResamplerKind::Fft(Box::new(fft)))
392            }
393        }
394    }
395
396    /// Drain accumulated input into `temp_output_all` one block at a time.
397    /// Returns `false` if the underlying resampler errored.
398    fn drive_resample_loop(&mut self, channels: usize, input_frames: usize) -> bool {
399        while self.input_buffer[0].len() >= input_frames {
400            let output_frames = {
401                let Some(resampler) = self.resampler.as_ref() else {
402                    return false;
403                };
404                resampler.output_frames_next()
405            };
406
407            let result = {
408                let Some(mut resampler) = self.resampler.take() else {
409                    return false;
410                };
411                let res = self.process_block(&mut resampler, input_frames, output_frames);
412                self.resampler = Some(resampler);
413                res
414            };
415
416            match result {
417                Ok(out_len) => {
418                    for ch in 0..channels {
419                        let src = &self.temp_output_bufs[ch][..out_len];
420                        self.temp_output_all[ch].extend_from_slice(src);
421                    }
422                    for buf in &mut self.input_buffer {
423                        buf.drain(..input_frames);
424                    }
425
426                    if self.input_buffer[0].len() < input_frames {
427                        break;
428                    }
429                }
430                Err(e) => {
431                    trace!(err = %e, "Resampler error");
432                    return false;
433                }
434            }
435        }
436        true
437    }
438
439    fn ensure_temp_buffers(&mut self, channels: usize) {
440        if self.temp_input_slice.len() < channels {
441            self.temp_input_slice.resize_with(channels, Vec::new);
442        }
443        if self.temp_output_bufs.len() < channels {
444            self.temp_output_bufs.resize_with(channels, Vec::new);
445        }
446    }
447
448    /// Turn the accumulated planar output into an interleaved `PcmChunk`.
449    fn finalize_resample_chunk(&self, _input_frames: usize) -> Option<PcmChunk> {
450        if self.temp_output_all[0].is_empty() {
451            return None;
452        }
453
454        let interleaved = self.interleave(&self.temp_output_all);
455        let mut meta = self.last_input_meta.unwrap_or_default();
456        meta.spec = self.output_spec;
457        let frame_count = interleaved
458            .len()
459            .checked_div(self.channels.max(1))
460            .unwrap_or(0);
461        let out_frames = u32::try_from(frame_count).unwrap_or(u32::MAX);
462        meta.frames = out_frames;
463        Some(PcmChunk::new(meta, interleaved))
464    }
465
466    /// Flush remaining data from buffer (called at end of stream).
467    pub fn flush_buffer(&mut self) -> Option<PcmChunk> {
468        self.resampler.as_ref()?;
469
470        if self.input_buffer[0].is_empty() {
471            return None;
472        }
473
474        let input_frames = self.resampler.as_ref()?.input_frames_next();
475        let channels = self.channels;
476        let buffered = self.input_buffer[0].len();
477
478        self.pad_input_for_flush(input_frames, buffered);
479
480        self.ensure_temp_buffers(channels);
481
482        let output_frames = {
483            let resampler = self.resampler.as_ref()?;
484            resampler.output_frames_next()
485        };
486
487        let result = {
488            let mut resampler = self.resampler.take()?;
489            let res = self.process_block(&mut resampler, input_frames, output_frames);
490            self.resampler = Some(resampler);
491            res
492        };
493
494        match result {
495            Ok(out_len) => self.build_flush_output(buffered, out_len),
496            Err(e) => {
497                trace!(err = %e, "Resampler flush error");
498                None
499            }
500        }
501    }
502
503    #[cfg_attr(feature = "perf", hotpath::measure)]
504    fn interleave(&self, planar: &[Vec<f32>]) -> PcmBuf {
505        if planar.is_empty() || planar[0].is_empty() {
506            return self.pool.get();
507        }
508
509        let frames = planar[0].len();
510        let total = frames * self.channels;
511
512        let mut result = self.pool.get();
513        if let Err(_e) = result.ensure_len(total) {
514            tracing::warn!("PCM pool budget exhausted during resampling");
515            return self.pool.get();
516        }
517
518        let num_channels = NonZeroUsize::new(self.channels).expect("channels must be > 0");
519        interleave_variable(planar, 0..frames, &mut result[..], num_channels);
520
521        result
522    }
523
524    fn is_passthrough(&self) -> bool {
525        self.resampler.is_none()
526    }
527
528    /// Pad input buffer with zeros so a final block can be processed.
529    fn pad_input_for_flush(&mut self, input_frames: usize, buffered: usize) {
530        let padding_needed = input_frames.saturating_sub(buffered);
531        for buf in &mut self.input_buffer {
532            buf.extend(iter::repeat_n(0.0, padding_needed));
533        }
534        debug!(buffered, padding_needed, "Flushing resampler buffer");
535    }
536
537    fn process_block(
538        &mut self,
539        resampler: &mut ResamplerKind,
540        input_frames: usize,
541        output_frames: usize,
542    ) -> Result<usize, rubato::ResampleError> {
543        let channels = self.channels;
544
545        for ch in 0..channels {
546            self.temp_input_slice[ch].clear();
547            self.temp_input_slice[ch].extend_from_slice(&self.input_buffer[ch][..input_frames]);
548        }
549
550        for ch in 0..channels {
551            self.temp_output_bufs[ch].resize(output_frames, 0.0);
552        }
553
554        let (_, out_len) = resampler.process_into_buffer(
555            &self.temp_input_slice[..channels],
556            &mut self.temp_output_bufs[..channels],
557        )?;
558        Ok(out_len)
559    }
560
561    fn ratio_for_target(&self, target_rate: u32) -> f64 {
562        let rate =
563            f64::from(self.playback_rate.load(Ordering::Relaxed)).max(Self::MIN_PLAYBACK_RATE);
564        if self.source_rate > 0 {
565            f64::from(target_rate) / (f64::from(self.source_rate) * rate)
566        } else {
567            1.0 / rate
568        }
569    }
570
571    fn recreate_resampler(&mut self, target_rate: u32, new_ratio: f64) {
572        match Self::create_resampler(
573            self.quality,
574            new_ratio,
575            self.chunk_size,
576            self.channels,
577            self.source_rate,
578            target_rate,
579        ) {
580            Ok(resampler) => {
581                self.resampler = Some(resampler);
582                self.current_ratio = new_ratio;
583                for buf in &mut self.input_buffer {
584                    buf.clear();
585                }
586            }
587            Err(e) => {
588                debug!(err = %e, "Failed to create resampler, staying in current mode");
589            }
590        }
591    }
592
593    #[cfg_attr(feature = "perf", hotpath::measure)]
594    fn resample(&mut self, chunk: &PcmChunk) -> Option<PcmChunk> {
595        self.resampler.as_ref()?;
596
597        self.last_input_meta = Some(chunk.meta);
598        self.append_to_buffer(&chunk.pcm);
599
600        let input_frames = self.resampler.as_ref()?.input_frames_next();
601        let channels = self.channels;
602
603        if self.input_buffer[0].len() < input_frames {
604            return None;
605        }
606
607        self.ensure_temp_buffers(channels);
608        self.reset_output_accumulator(channels);
609
610        let loop_ok = self.drive_resample_loop(channels, input_frames);
611        if !loop_ok {
612            return None;
613        }
614
615        self.finalize_resample_chunk(input_frames)
616    }
617
618    /// Clear and resize the per-channel output accumulator used by `resample`.
619    fn reset_output_accumulator(&mut self, channels: usize) {
620        if self.temp_output_all.len() < channels {
621            self.temp_output_all.resize_with(channels, Vec::new);
622        }
623        for buf in &mut self.temp_output_all {
624            buf.clear();
625        }
626    }
627
628    fn should_passthrough(source_rate: u32, target_rate: u32, playback_rate: f64) -> bool {
629        (source_rate == target_rate || target_rate == 0)
630            && (playback_rate - 1.0).abs() < Self::PASSTHROUGH_TOLERANCE
631    }
632
633    fn switch_to_passthrough(&mut self, target_rate: u32, currently_pt: bool) {
634        if !currently_pt {
635            debug!(
636                source_rate = self.source_rate,
637                target_rate, "Resampler switching to passthrough"
638            );
639            self.resampler = None;
640        }
641        self.current_ratio = 1.0;
642        for buf in &mut self.input_buffer {
643            buf.clear();
644        }
645    }
646
647    fn target_rate(&self) -> u32 {
648        let host_sr = self.host_sample_rate.load(Ordering::Relaxed);
649        if host_sr == 0 {
650            self.source_rate
651        } else {
652            host_sr
653        }
654    }
655
656    fn try_update_ratio(
657        &mut self,
658        target_rate: u32,
659        new_ratio: f64,
660        currently_pt: bool,
661        ratio_changed: bool,
662    ) -> bool {
663        if currently_pt || !ratio_changed {
664            return false;
665        }
666        let Some(ref mut resampler) = self.resampler else {
667            return false;
668        };
669
670        match resampler.set_resample_ratio(new_ratio, false) {
671            Ok(()) => {
672                debug!(
673                    new_ratio,
674                    source_rate = self.source_rate,
675                    target_rate,
676                    "Resampler ratio updated dynamically"
677                );
678                self.current_ratio = new_ratio;
679                true
680            }
681            Err(e) => {
682                debug!(err = %e, "Failed to update ratio dynamically, recreating");
683                self.resampler = None;
684                false
685            }
686        }
687    }
688
689    fn update_resampler_if_needed(&mut self) {
690        let target_rate = self.target_rate();
691        self.output_spec.sample_rate = target_rate;
692        let new_playback_rate =
693            f64::from(self.playback_rate.load(Ordering::Relaxed)).max(Self::MIN_PLAYBACK_RATE);
694        let should_pt = Self::should_passthrough(self.source_rate, target_rate, new_playback_rate);
695        let currently_pt = self.is_passthrough();
696        let new_ratio = self.ratio_for_target(target_rate);
697        let ratio_changed = (new_ratio - self.current_ratio).abs() > Self::PASSTHROUGH_TOLERANCE;
698
699        if should_pt {
700            self.switch_to_passthrough(target_rate, currently_pt);
701            self.current_playback_rate = new_playback_rate;
702            return;
703        }
704
705        if self.try_update_ratio(target_rate, new_ratio, currently_pt, ratio_changed) {
706            self.current_playback_rate = new_playback_rate;
707            return;
708        }
709
710        if currently_pt || self.resampler.is_none() || ratio_changed {
711            self.recreate_resampler(target_rate, new_ratio);
712            self.current_playback_rate = new_playback_rate;
713        }
714    }
715}
716
717/// Create a `SmallVec` of empty Vecs for each channel.
718#[cfg_attr(feature = "perf", hotpath::measure)]
719fn smallvec_new_vecs(channels: usize) -> SmallVec<[Vec<f32>; 8]> {
720    (0..channels).map(|_| Vec::new()).collect()
721}
722
723impl ResamplerProcessor {
724    /// Apply incoming chunk spec changes in-place (channels first, then rate).
725    fn apply_source_spec_changes(&mut self, chunk_channels: usize, chunk_rate: u32) {
726        if chunk_channels != self.channels {
727            self.handle_channel_change(chunk_channels, chunk_rate);
728        } else if chunk_rate != self.source_rate {
729            self.handle_source_rate_change(chunk_rate);
730        }
731    }
732
733    /// React to a changed channel count in incoming chunks (ABR switch).
734    fn handle_channel_change(&mut self, chunk_channels: usize, chunk_rate: u32) {
735        self.channels = chunk_channels;
736        self.source_rate = chunk_rate;
737        self.input_buffer = smallvec_new_vecs(chunk_channels);
738        self.temp_input_slice = smallvec_new_vecs(chunk_channels);
739        self.temp_output_bufs = smallvec_new_vecs(chunk_channels);
740        self.temp_output_all = smallvec_new_vecs(chunk_channels);
741        self.temp_deinterleave = smallvec_new_vecs(chunk_channels);
742        let channels_u16 = u16::try_from(chunk_channels).unwrap_or(u16::MAX);
743        self.output_spec.channels = channels_u16;
744        self.resampler = None;
745    }
746
747    /// React to a changed source sample rate while channel count is stable.
748    fn handle_source_rate_change(&mut self, chunk_rate: u32) {
749        self.source_rate = chunk_rate;
750        for buf in &mut self.input_buffer {
751            buf.clear();
752        }
753    }
754
755    /// Passthrough path: take the chunk, stamp the current output spec, return it.
756    fn passthrough_chunk(&self, mut chunk: PcmChunk) -> PcmChunk {
757        chunk.meta.spec = self.output_spec;
758        chunk
759    }
760}
761
762impl AudioEffect for ResamplerProcessor {
763    fn flush(&mut self) -> Option<PcmChunk> {
764        self.flush_buffer()
765    }
766
767    #[cfg_attr(feature = "perf", hotpath::measure)]
768    fn process(&mut self, chunk: PcmChunk) -> Option<PcmChunk> {
769        let chunk_rate = chunk.spec().sample_rate;
770        let chunk_channels = chunk.spec().channels as usize;
771
772        self.apply_source_spec_changes(chunk_channels, chunk_rate);
773        self.update_resampler_if_needed();
774        if self.is_passthrough() {
775            return Some(self.passthrough_chunk(chunk));
776        }
777        self.resample(&chunk)
778    }
779
780    fn reset(&mut self) {
781        for buf in &mut self.input_buffer {
782            buf.clear();
783        }
784        self.resampler = None;
785    }
786}
787
788#[cfg(test)]
789mod tests {
790    use kithara_bufpool::PcmPool;
791    use kithara_test_utils::kithara;
792
793    use super::*;
794
795    fn test_chunk(spec: PcmSpec, pcm: Vec<f32>) -> PcmChunk {
796        PcmChunk::new(
797            PcmMeta {
798                spec,
799                ..Default::default()
800            },
801            PcmPool::default().attach(pcm),
802        )
803    }
804
805    fn make_host_rate(rate: u32) -> Arc<AtomicU32> {
806        Arc::new(AtomicU32::new(rate))
807    }
808
809    fn params(host_sr: Arc<AtomicU32>, source_rate: u32, channels: usize) -> ResamplerParams {
810        ResamplerParams::new(host_sr, source_rate, channels)
811    }
812
813    fn params_with_rate(
814        host_sr: Arc<AtomicU32>,
815        source_rate: u32,
816        channels: usize,
817        rate: Arc<AtomicF32>,
818    ) -> ResamplerParams {
819        ResamplerParams::builder()
820            .host_sample_rate(host_sr)
821            .source_sample_rate(source_rate)
822            .channels(channels)
823            .playback_rate(rate)
824            .build()
825    }
826
827    fn params_with_quality(
828        host_sr: Arc<AtomicU32>,
829        source_rate: u32,
830        channels: usize,
831        quality: ResamplerQuality,
832    ) -> ResamplerParams {
833        ResamplerParams::builder()
834            .host_sample_rate(host_sr)
835            .source_sample_rate(source_rate)
836            .channels(channels)
837            .quality(quality)
838            .build()
839    }
840
841    #[kithara::test]
842    #[case::same_rate(44100, 44100, true)]
843    #[case::host_zero(0, 44100, true)]
844    #[case::different_rate(44100, 48000, false)]
845    fn test_passthrough_mode(
846        #[case] host_rate: u32,
847        #[case] source_rate: u32,
848        #[case] expected: bool,
849    ) {
850        let processor = ResamplerProcessor::new(params(make_host_rate(host_rate), source_rate, 2));
851        assert_eq!(processor.is_passthrough(), expected);
852    }
853
854    #[kithara::test]
855    fn test_passthrough_processing() {
856        let mut processor = ResamplerProcessor::new(params(make_host_rate(44100), 44100, 2));
857
858        let chunk = test_chunk(
859            PcmSpec {
860                channels: 2,
861                sample_rate: 44100,
862            },
863            vec![0.1, 0.2, 0.3, 0.4],
864        );
865
866        let result = processor.process(chunk.clone());
867        assert!(result.is_some());
868        let out = result.unwrap();
869        assert_eq!(out.pcm, chunk.pcm);
870        assert_eq!(out.spec().sample_rate, 44100);
871    }
872
873    #[kithara::test]
874    fn test_output_spec() {
875        let processor = ResamplerProcessor::new(params(make_host_rate(44100), 48000, 2));
876        assert_eq!(processor.output_spec.sample_rate, 44100);
877        assert_eq!(processor.output_spec.channels, 2);
878    }
879
880    #[kithara::test]
881    fn test_dynamic_host_rate_change() {
882        let host_sr = make_host_rate(44100);
883        let mut processor = ResamplerProcessor::new(params(host_sr.clone(), 44100, 2));
884        assert!(processor.is_passthrough());
885
886        host_sr.store(48000, Ordering::Relaxed);
887
888        let chunk = test_chunk(
889            PcmSpec {
890                channels: 2,
891                sample_rate: 44100,
892            },
893            vec![0.1; 2048],
894        );
895        let _ = processor.process(chunk);
896
897        assert!(!processor.is_passthrough());
898    }
899
900    #[kithara::test]
901    #[case::small(100, false)]
902    #[case::large(16384, true)]
903    fn test_chunk_size_threshold(#[case] interleaved_len: usize, #[case] produces_output: bool) {
904        let mut processor = ResamplerProcessor::new(params(make_host_rate(44100), 48000, 2));
905
906        let chunk = test_chunk(
907            PcmSpec {
908                channels: 2,
909                sample_rate: 48000,
910            },
911            vec![0.1; interleaved_len],
912        );
913
914        let result = processor.process(chunk);
915        if produces_output {
916            assert!(result.is_some());
917            assert!(!result.unwrap().pcm.is_empty());
918        } else {
919            assert!(result.is_none());
920            assert_eq!(processor.input_buffer[0].len(), interleaved_len / 2);
921        }
922    }
923
924    #[kithara::test]
925    fn test_source_rate_change_updates_dynamically() {
926        let mut processor = ResamplerProcessor::new(params(make_host_rate(44100), 48000, 2));
927
928        let chunk1 = test_chunk(
929            PcmSpec {
930                channels: 2,
931                sample_rate: 48000,
932            },
933            vec![0.1; 4096],
934        );
935        let _ = processor.process(chunk1);
936        assert_eq!(processor.source_rate, 48000);
937
938        let chunk2 = test_chunk(
939            PcmSpec {
940                channels: 2,
941                sample_rate: 44100,
942            },
943            vec![0.2; 4096],
944        );
945        let _ = processor.process(chunk2);
946        assert_eq!(processor.source_rate, 44100);
947        assert!(processor.is_passthrough());
948    }
949
950    #[kithara::test]
951    fn test_no_data_loss_across_chunks() {
952        let mut processor = ResamplerProcessor::new(params(make_host_rate(44100), 48000, 2));
953
954        let mut total_input_frames = 0;
955        let mut total_output_frames = 0;
956
957        for _ in 0..10 {
958            let chunk = test_chunk(
959                PcmSpec {
960                    channels: 2,
961                    sample_rate: 48000,
962                },
963                vec![0.1; 2048],
964            );
965            total_input_frames += 1024;
966
967            if let Some(out) = processor.process(chunk) {
968                total_output_frames += out.frames();
969            }
970        }
971
972        let expected = usize::try_from(i64::from(total_input_frames) * 44100 / 48000)
973            .expect("test bounded result fits usize");
974        let tolerance = 1024 * 2;
975        assert!(
976            total_output_frames + tolerance >= expected,
977            "Output {} + tolerance {} should be >= expected {}",
978            total_output_frames,
979            tolerance,
980            expected
981        );
982    }
983
984    #[kithara::test]
985    fn test_audio_effect_trait() {
986        let mut processor = ResamplerProcessor::new(params(make_host_rate(44100), 44100, 2));
987
988        let chunk = test_chunk(
989            PcmSpec {
990                channels: 2,
991                sample_rate: 44100,
992            },
993            vec![0.1, 0.2, 0.3, 0.4],
994        );
995
996        let result = AudioEffect::process(&mut processor, chunk);
997        assert!(result.is_some());
998
999        AudioEffect::reset(&mut processor);
1000        assert!(processor.input_buffer[0].is_empty());
1001    }
1002
1003    #[kithara::test]
1004    #[case::rate_2x_halves(2.0, true, 5000)]
1005    #[case::rate_half_doubles(0.5, false, 12000)]
1006    fn test_playback_rate_scales_output(
1007        #[case] rate_value: f32,
1008        #[case] expect_less_than: bool,
1009        #[case] threshold: usize,
1010    ) {
1011        let rate = Arc::new(AtomicF32::new(rate_value));
1012        let mut processor =
1013            ResamplerProcessor::new(params_with_rate(make_host_rate(44100), 44100, 2, rate));
1014        assert!(!processor.is_passthrough());
1015        let chunk = test_chunk(
1016            PcmSpec {
1017                channels: 2,
1018                sample_rate: 44100,
1019            },
1020            vec![0.1; 16384],
1021        );
1022        let result = processor.process(chunk);
1023        assert!(result.is_some());
1024        let output_frames = result.unwrap().frames();
1025        if expect_less_than {
1026            assert!(
1027                output_frames < threshold,
1028                "Expected < {threshold}, got {output_frames}"
1029            );
1030        } else {
1031            assert!(
1032                output_frames > threshold,
1033                "Expected > {threshold}, got {output_frames}"
1034            );
1035        }
1036    }
1037
1038    #[kithara::test]
1039    fn test_playback_rate_1x_same_rate_passthrough() {
1040        let rate = Arc::new(AtomicF32::new(1.0));
1041        let processor =
1042            ResamplerProcessor::new(params_with_rate(make_host_rate(44100), 44100, 2, rate));
1043        assert!(processor.is_passthrough());
1044    }
1045
1046    #[kithara::test]
1047    fn test_playback_rate_dynamic_change() {
1048        let rate = Arc::new(AtomicF32::new(1.0));
1049        let mut processor = ResamplerProcessor::new(params_with_rate(
1050            make_host_rate(44100),
1051            44100,
1052            2,
1053            Arc::clone(&rate),
1054        ));
1055        assert!(processor.is_passthrough());
1056        rate.store(2.0, Ordering::Relaxed);
1057        let chunk = test_chunk(
1058            PcmSpec {
1059                channels: 2,
1060                sample_rate: 44100,
1061            },
1062            vec![0.1; 16384],
1063        );
1064        let _ = processor.process(chunk);
1065        assert!(!processor.is_passthrough());
1066    }
1067
1068    #[kithara::test]
1069    #[case::fast(ResamplerQuality::Fast)]
1070    #[case::normal(ResamplerQuality::Normal)]
1071    #[case::good(ResamplerQuality::Good)]
1072    #[case::high(ResamplerQuality::High)]
1073    #[case::maximum(ResamplerQuality::Maximum)]
1074    fn test_quality_resamples(#[case] quality: ResamplerQuality) {
1075        let mut processor = ResamplerProcessor::new(params_with_quality(
1076            make_host_rate(44100),
1077            48000,
1078            2,
1079            quality,
1080        ));
1081        assert!(!processor.is_passthrough());
1082
1083        let chunk = test_chunk(
1084            PcmSpec {
1085                channels: 2,
1086                sample_rate: 48000,
1087            },
1088            vec![0.1; 16384],
1089        );
1090        let result = processor.process(chunk);
1091        assert!(result.is_some());
1092    }
1093}