use super::OPUS_SAMPLE_RATE;
use super::PcmFrame;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Resampler {
sample_rate: u32,
channels: u16,
}
impl Resampler {
pub fn new(sample_rate: u32, channels: u16) -> Self {
Self {
sample_rate: sample_rate.max(1),
channels: channels.max(1),
}
}
pub fn to_opus_mono() -> Self {
Self::new(OPUS_SAMPLE_RATE, 1)
}
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
pub fn channels(&self) -> u16 {
self.channels
}
pub fn resample(&self, frame: &PcmFrame) -> PcmFrame {
let in_channels = frame.channels.max(1);
let in_rate = frame.sample_rate.max(1);
if frame.samples.is_empty() {
return PcmFrame::new(Vec::new(), self.sample_rate, self.channels);
}
let planes: Vec<Vec<i16>> = (0..in_channels as usize)
.map(|ch| {
let plane: Vec<i16> = frame
.samples
.iter()
.skip(ch)
.step_by(in_channels as usize)
.copied()
.collect();
resample_plane(&plane, in_rate, self.sample_rate)
})
.collect();
PcmFrame::new(
interleave(&planes, self.channels),
self.sample_rate,
self.channels,
)
}
}
fn resample_plane(samples: &[i16], from_rate: u32, to_rate: u32) -> Vec<i16> {
if from_rate == to_rate || samples.is_empty() {
return samples.to_vec();
}
let in_len = samples.len();
let out_len = (in_len as f64 * f64::from(to_rate) / f64::from(from_rate)).round() as usize;
match out_len {
0 => return Vec::new(),
1 => return vec![samples[0]],
_ => {}
}
if in_len == 1 {
return vec![samples[0]; out_len];
}
let step = (in_len - 1) as f64 / (out_len - 1) as f64;
(0..out_len)
.map(|i| {
let pos = i as f64 * step;
let idx = pos.floor() as usize;
if idx + 1 >= in_len {
return samples[in_len - 1];
}
let frac = pos - idx as f64;
let a = f64::from(samples[idx]);
let b = f64::from(samples[idx + 1]);
clamp_to_i16(a + (b - a) * frac)
})
.collect()
}
fn interleave(planes: &[Vec<i16>], out_channels: u16) -> Vec<i16> {
let out_channels = out_channels.max(1) as usize;
let frames = planes.first().map_or(0, Vec::len);
let mut out = Vec::with_capacity(frames * out_channels);
for f in 0..frames {
if out_channels == 1 && planes.len() > 1 {
let sum: f64 = planes.iter().map(|p| f64::from(p[f])).sum();
out.push(clamp_to_i16(sum / planes.len() as f64));
continue;
}
for ch in 0..out_channels {
let plane = planes.get(ch).unwrap_or(&planes[planes.len() - 1]);
out.push(plane[f]);
}
}
out
}
fn clamp_to_i16(v: f64) -> i16 {
v.round().clamp(f64::from(i16::MIN), f64::from(i16::MAX)) as i16
}
#[derive(Debug)]
pub struct StreamResampler {
out_rate: u32,
in_rate: u32,
in_channels: u16,
inbuf: Vec<f32>,
pos: f64,
}
impl StreamResampler {
pub fn to_opus_mono() -> Self {
Self::new(OPUS_SAMPLE_RATE)
}
pub fn new(out_rate: u32) -> Self {
Self {
out_rate: out_rate.max(1),
in_rate: 0,
in_channels: 1,
inbuf: Vec::new(),
pos: 0.0,
}
}
pub fn push(&mut self, frame: &PcmFrame) -> Vec<i16> {
let in_rate = frame.sample_rate.max(1);
let in_channels = frame.channels.max(1);
if in_rate != self.in_rate || in_channels != self.in_channels {
self.in_rate = in_rate;
self.in_channels = in_channels;
self.inbuf.clear();
self.pos = 0.0;
}
let ch = in_channels as usize;
self.inbuf.reserve(frame.frames());
for chunk in frame.samples.chunks_exact(ch) {
let sum: f32 = chunk.iter().map(|&s| f32::from(s)).sum();
self.inbuf.push(sum / ch as f32);
}
if in_rate == self.out_rate {
let out: Vec<i16> = self
.inbuf
.drain(..)
.map(|v| v.round().clamp(f32::from(i16::MIN), f32::from(i16::MAX)) as i16)
.collect();
self.pos = 0.0;
return out;
}
let step = f64::from(in_rate) / f64::from(self.out_rate);
let mut out = Vec::new();
while self.pos + 1.0 < self.inbuf.len() as f64 {
let idx = self.pos.floor() as usize;
let frac = (self.pos - idx as f64) as f32;
let a = self.inbuf[idx];
let b = self.inbuf[idx + 1];
let v = a + (b - a) * frac;
out.push(v.round().clamp(f32::from(i16::MIN), f32::from(i16::MAX)) as i16);
self.pos += step;
}
let consumed = self.pos.floor() as usize;
if consumed > 0 {
let keep_from = consumed.min(self.inbuf.len());
self.inbuf.drain(..keep_from);
self.pos -= consumed as f64;
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn block_length_follows_the_rate_ratio_exactly() {
for (from, to, frames_in, frames_out) in [
(16_000, 48_000, 320, 960),
(48_000, 16_000, 960, 320),
(8_000, 48_000, 160, 960),
(44_100, 48_000, 882, 960),
(24_000, 48_000, 480, 960),
] {
let out = Resampler::new(to, 1).resample(&PcmFrame::mono(vec![0; frames_in], from));
assert_eq!(out.frames(), frames_out, "{from} -> {to}");
}
}
#[test]
fn identical_rate_and_channels_is_a_passthrough() {
let frame = PcmFrame::new(vec![100, 200, 300, 400], 48_000, 2);
assert_eq!(Resampler::new(48_000, 2).resample(&frame), frame);
}
#[test]
fn endpoints_are_preserved_across_rate_change() {
let input: Vec<i16> = vec![1000, 2000, 3000, 4000, 5000];
let out = Resampler::new(48_000, 1).resample(&PcmFrame::mono(input.clone(), 16_000));
assert_eq!(out.samples[0], input[0]);
assert_eq!(*out.samples.last().unwrap(), *input.last().unwrap());
}
#[test]
fn stereo_downmix_averages_channels() {
let stereo = PcmFrame::new(vec![100, 200, 300, 400], 48_000, 2);
let out = Resampler::new(48_000, 1).resample(&stereo);
assert_eq!(out.samples, vec![150, 350]);
assert_eq!(out.channels, 1);
}
#[test]
fn mono_upmix_duplicates_into_both_channels() {
let mono = PcmFrame::mono(vec![100, 200], 48_000);
let out = Resampler::new(48_000, 2).resample(&mono);
assert_eq!(out.samples, vec![100, 100, 200, 200]);
assert_eq!(out.frames(), 2);
}
#[test]
fn channels_are_resampled_independently_not_smeared() {
let stereo = PcmFrame::new(vec![10_000, 0, 10_000, 0, 10_000, 0, 10_000, 0], 24_000, 2);
let out = Resampler::new(48_000, 2).resample(&stereo);
let right_max = out
.samples
.iter()
.skip(1)
.step_by(2)
.copied()
.max()
.unwrap();
assert_eq!(
right_max, 0,
"silent channel picked up energy from the left"
);
}
#[test]
fn empty_input_yields_an_empty_frame_at_the_target_layout() {
let out = Resampler::new(48_000, 2).resample(&PcmFrame::mono(Vec::new(), 16_000));
assert!(out.is_empty());
assert_eq!(out.sample_rate, 48_000);
assert_eq!(out.channels, 2);
}
#[test]
fn single_input_sample_is_held_across_the_output() {
let out = Resampler::new(48_000, 1).resample(&PcmFrame::mono(vec![1234], 16_000));
assert_eq!(out.samples, vec![1234; 3]);
}
#[test]
fn full_scale_input_never_wraps_polarity() {
let loud = PcmFrame::mono(vec![i16::MIN, i16::MAX, i16::MIN, i16::MAX], 16_000);
let out = Resampler::new(48_000, 1).resample(&loud);
let lo = *loud.samples.iter().min().unwrap();
let hi = *loud.samples.iter().max().unwrap();
assert!(
out.samples.iter().all(|s| (lo..=hi).contains(s)),
"interpolation escaped the input range"
);
assert_eq!(out.samples[0], i16::MIN);
assert_eq!(*out.samples.last().unwrap(), i16::MAX);
}
#[test]
fn stream_identity_rate_passes_through_downmixed() {
let mut r = StreamResampler::to_opus_mono();
let frame = PcmFrame::new(vec![100, 200, 300, 400], 48_000, 2);
let out = r.push(&frame);
assert_eq!(out, vec![150, 350]);
}
#[test]
fn stream_downsample_halves_sample_count() {
let mut r = StreamResampler::to_opus_mono();
let input: Vec<i16> = (0..960).map(|i| (i % 100) as i16).collect();
let frame = PcmFrame::mono(input, 96_000);
let out = r.push(&frame);
assert!(
(475..=480).contains(&out.len()),
"unexpected out len {}",
out.len()
);
}
#[test]
fn stream_upsample_is_continuous_across_blocks() {
let mut r = StreamResampler::to_opus_mono();
let block: Vec<i16> = (0..240).map(|i| i as i16).collect();
let a = r.push(&PcmFrame::mono(block.clone(), 24_000));
let b = r.push(&PcmFrame::mono(block, 24_000));
assert!(!a.is_empty() && !b.is_empty());
let total = a.len() + b.len();
assert!((950..=960).contains(&total), "unexpected total {total}");
}
}