pub(crate) struct CaptureResampleResult { pub(crate) output_len: usize, pub(crate) dropped: bool, } pub(crate) fn resample_capture_to_48k( samples: &[i16], input_sample_rate_hz: u32, resample_pos: &mut f64, resample_last: &mut i16, scratch: &mut Vec, ) -> CaptureResampleResult { scratch.clear(); if samples.is_empty() { return CaptureResampleResult { output_len: 0, dropped: false, }; } let ratio = input_sample_rate_hz.max(1) as f64 / crate::frame::SAMPLE_RATE_HZ as f64; let mut pos = *resample_pos; let mut dropped = false; while pos < samples.len() as f64 { let i = pos.floor() as isize; let frac = pos - i as f64; let a = if i <= 0 { *resample_last as f64 } else { samples[(i - 1) as usize] as f64 }; let b = if i < samples.len() as isize { samples[i as usize] as f64 } else { a }; let value = (a + frac * (b - a)) .round() .clamp(i16::MIN as f64, i16::MAX as f64) as i16; if scratch.len() < scratch.capacity() { scratch.push(value); } else { dropped = true; } pos += ratio; } *resample_pos = pos - samples.len() as f64; *resample_last = *samples.last().unwrap_or(resample_last); CaptureResampleResult { output_len: scratch.len(), dropped, } } #[cfg(test)] mod android_voice_unit_resampler_tests { use super::resample_capture_to_48k; #[test] fn android_voice_unit_resampler_reuses_scratch_without_capacity_growth() { let samples: Vec = (0..882).map(|i| i as i16).collect(); let mut pos = 0.0; let mut last = 0_i16; let mut scratch = Vec::with_capacity(960); let first = resample_capture_to_48k(&samples, 44_100, &mut pos, &mut last, &mut scratch); let first_len = first.output_len; assert_eq!(first_len, 960); assert!(!first.dropped); assert_eq!(scratch.len(), first_len); let warmed_capacity = scratch.capacity(); let warmed_ptr = scratch.as_ptr(); for _ in 0..8 { let result = resample_capture_to_48k(&samples, 44_100, &mut pos, &mut last, &mut scratch); let len = result.output_len; assert_eq!(len, scratch.len()); assert!(len >= 959 && len <= 960); assert!(!result.dropped); assert_eq!(scratch.capacity(), warmed_capacity); assert_eq!(scratch.as_ptr(), warmed_ptr); } } #[test] fn android_voice_unit_resampler_truncates_oversized_burst_without_capacity_growth() { let samples: Vec = (0..4_800).map(|i| i as i16).collect(); let mut pos = 0.0; let mut last = 0_i16; let mut scratch = Vec::with_capacity(960); let warmed_capacity = scratch.capacity(); let warmed_ptr = scratch.as_ptr(); let result = resample_capture_to_48k(&samples, 48_000, &mut pos, &mut last, &mut scratch); assert_eq!(result.output_len, warmed_capacity); assert!(result.dropped); assert_eq!(scratch.len(), warmed_capacity); assert_eq!(scratch.capacity(), warmed_capacity); assert_eq!(scratch.as_ptr(), warmed_ptr); assert_eq!(pos, 0.0); assert_eq!(last, *samples.last().unwrap()); } }