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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
30pub enum ResamplerQuality {
31 Fast,
34 Normal,
36 Good,
38 #[default]
41 High,
42 Maximum,
45}
46
47impl From<ResamplerQuality> for SincInterpolationParameters {
48 fn from(quality: ResamplerQuality) -> Self {
49 const OVERSAMPLING_HIGH: usize = 256;
51 const OVERSAMPLING_NORMAL: usize = 128;
53 const CUTOFF: f32 = 0.95;
55 const LEN_GOOD: usize = 128;
57 const LEN_HIGH: usize = 256;
59 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
88enum ResamplerKind {
90 Poly(Async<f32>),
91 Sinc(Async<f32>),
92 Fft(Box<Fft<f32>>),
93}
94
95impl ResamplerKind {
96 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 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#[derive(Builder)]
153#[builder(state_mod(vis = "pub"))]
154#[non_exhaustive]
155pub struct ResamplerParams {
156 pub host_sample_rate: Arc<AtomicU32>,
158 #[builder(default = Arc::new(AtomicF32::new(1.0)))]
163 pub playback_rate: Arc<AtomicF32>,
164 pub pool: Option<PcmPool>,
166 #[builder(default)]
168 pub quality: ResamplerQuality,
169 pub source_sample_rate: u32,
171 pub channels: usize,
173 #[builder(default = ResamplerProcessor::DEFAULT_CHUNK_SIZE)]
175 pub chunk_size: usize,
176}
177
178impl ResamplerParams {
179 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
189pub struct ResamplerProcessor {
196 host_sample_rate: Arc<AtomicU32>,
197 playback_rate: Arc<AtomicF32>,
199 last_input_meta: Option<PcmMeta>,
205 resampler: Option<ResamplerKind>,
206 pool: PcmPool,
208 output_spec: PcmSpec,
209 quality: ResamplerQuality,
210 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 const DEFAULT_CHUNK_SIZE: usize = 4096;
226
227 const FFT_SUB_CHUNKS: usize = 2;
229
230 const MAX_RATIO_ADJUSTMENT: f64 = 8.0;
232
233 const MIN_PLAYBACK_RATE: f64 = 0.01;
235
236 const PASSTHROUGH_TOLERANCE: f64 = 0.0001;
238
239 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 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 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 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 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 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 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#[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 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 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 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 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}