use audioadapter_buffers::direct::InterleavedSlice;
use rubato::{
Async, FixedAsync, Resampler as RubatoResampler, SincInterpolationParameters,
SincInterpolationType, WindowFunction,
};
use crate::error::FlowcatError;
use crate::types::AudioChunk;
pub fn ulaw_to_pcm16(ulaw: &[u8]) -> Vec<i16> {
ulaw.iter()
.map(|&b| audio_codec_algorithms::decode_ulaw(b))
.collect()
}
pub fn pcm16_to_ulaw(pcm: &[i16]) -> Vec<u8> {
pcm.iter()
.map(|&s| audio_codec_algorithms::encode_ulaw(s))
.collect()
}
pub fn alaw_to_pcm16(alaw: &[u8]) -> Vec<i16> {
alaw.iter()
.map(|&b| audio_codec_algorithms::decode_alaw(b))
.collect()
}
pub fn pcm16_to_alaw(pcm: &[i16]) -> Vec<u8> {
pcm.iter()
.map(|&s| audio_codec_algorithms::encode_alaw(s))
.collect()
}
const BLOCK: usize = 256;
pub struct Resampler {
pub from_rate: u32,
pub to_rate: u32,
inner: Option<Async<f32>>,
in_buf: Vec<i16>,
scratch_in: Vec<f32>,
scratch_out: Vec<f32>,
}
impl Resampler {
pub fn new(from_rate: u32, to_rate: u32) -> Result<Self, FlowcatError> {
if from_rate == 0 || to_rate == 0 {
return Err(FlowcatError::Codec(format!(
"invalid sample rate: from={from_rate} to={to_rate} (must be > 0)"
)));
}
if from_rate == to_rate {
return Ok(Self {
from_rate,
to_rate,
inner: None,
in_buf: Vec::new(),
scratch_in: Vec::new(),
scratch_out: Vec::new(),
});
}
let ratio = to_rate as f64 / from_rate as f64;
let params = SincInterpolationParameters {
sinc_len: 256,
f_cutoff: 0.95,
oversampling_factor: 256,
interpolation: SincInterpolationType::Cubic,
window: WindowFunction::BlackmanHarris2,
};
let inner = Async::<f32>::new_sinc(ratio, 1.0, ¶ms, BLOCK, 1, FixedAsync::Input)
.map_err(|e| FlowcatError::Codec(format!("rubato init {from_rate}->{to_rate}: {e}")))?;
let scratch_out = vec![0.0f32; inner.output_frames_max()];
let scratch_in = Vec::with_capacity(BLOCK);
Ok(Self {
from_rate,
to_rate,
inner: Some(inner),
in_buf: Vec::new(),
scratch_in,
scratch_out,
})
}
pub fn process(&mut self, input: &AudioChunk) -> Result<AudioChunk, FlowcatError> {
if input.sample_rate != self.from_rate {
return Err(FlowcatError::Codec(format!(
"resampler expected input at {} Hz, got {} Hz",
self.from_rate, input.sample_rate
)));
}
let resampler = match self.inner.as_mut() {
None => return Ok(AudioChunk::new(input.pcm.clone(), self.to_rate)),
Some(r) => r,
};
self.in_buf.extend_from_slice(&input.pcm);
let mut out: Vec<i16> = Vec::new();
let mut consumed = 0usize;
while self.in_buf.len() - consumed >= BLOCK {
let block = &self.in_buf[consumed..consumed + BLOCK];
self.scratch_in.clear();
self.scratch_in
.extend(block.iter().map(|&s| s as f32 / 32768.0));
let in_view = InterleavedSlice::new(&self.scratch_in, 1, BLOCK)
.map_err(|e| FlowcatError::Codec(format!("rubato input view: {e}")))?;
let cap = self.scratch_out.len();
let mut out_view = InterleavedSlice::new_mut(&mut self.scratch_out, 1, cap)
.map_err(|e| FlowcatError::Codec(format!("rubato output view: {e}")))?;
let (_in_frames, out_frames) = resampler
.process_into_buffer(&in_view, &mut out_view, None)
.map_err(|e| FlowcatError::Codec(format!("rubato process: {e}")))?;
out.extend(
self.scratch_out[..out_frames]
.iter()
.map(|&s| f32_to_i16(s)),
);
consumed += BLOCK;
}
if consumed > 0 {
self.in_buf.drain(..consumed);
}
Ok(AudioChunk::new(out, self.to_rate))
}
pub fn flush(&mut self) -> Result<AudioChunk, FlowcatError> {
let resampler = match self.inner.as_mut() {
None => return Ok(AudioChunk::new(Vec::new(), self.to_rate)),
Some(r) => r,
};
if self.in_buf.is_empty() {
return Ok(AudioChunk::new(Vec::new(), self.to_rate));
}
self.scratch_in.clear();
self.scratch_in
.extend(self.in_buf.iter().map(|&s| s as f32 / 32768.0));
self.scratch_in.resize(BLOCK, 0.0); self.in_buf.clear();
let in_view = InterleavedSlice::new(&self.scratch_in, 1, BLOCK)
.map_err(|e| FlowcatError::Codec(format!("rubato input view: {e}")))?;
let cap = self.scratch_out.len();
let mut out_view = InterleavedSlice::new_mut(&mut self.scratch_out, 1, cap)
.map_err(|e| FlowcatError::Codec(format!("rubato output view: {e}")))?;
let (_in_frames, out_frames) = resampler
.process_into_buffer(&in_view, &mut out_view, None)
.map_err(|e| FlowcatError::Codec(format!("rubato flush: {e}")))?;
let out: Vec<i16> = self.scratch_out[..out_frames]
.iter()
.map(|&s| f32_to_i16(s))
.collect();
Ok(AudioChunk::new(out, self.to_rate))
}
}
#[inline]
fn f32_to_i16(s: f32) -> i16 {
(s * 32768.0).round().clamp(-32768.0, 32767.0) as i16
}
#[cfg(test)]
mod tests {
use super::*;
fn representative_samples() -> Vec<i16> {
vec![
0, 1, -1, 100, -100, 1000, -1000, 8000, -8000, 16000, -16000, 32000, -32000, 32767,
-32768, 255, -256, 4095, -4096,
]
}
#[test]
fn ulaw_decodes_to_g711_reference_values() {
assert_eq!(
ulaw_to_pcm16(&[0x00]),
vec![-32124],
"0x00 = negative full-scale"
);
assert_eq!(
ulaw_to_pcm16(&[0x80]),
vec![32124],
"0x80 = positive full-scale"
);
assert_eq!(ulaw_to_pcm16(&[0xFF]), vec![0], "0xFF = -0 (silence)");
assert_eq!(
ulaw_to_pcm16(&[0x7F]),
vec![0],
"0x7F = +0 (the other zero)"
);
}
#[test]
fn ulaw_silence_round_trips_to_canonical_zero_code() {
assert_eq!(
pcm16_to_ulaw(&[0]),
vec![0xFF],
"encode(0) = canonical -0 code"
);
assert_eq!(ulaw_to_pcm16(&[0xFF]), vec![0], "0xFF decodes to silence");
}
#[test]
fn ulaw_full_scale_encode_does_not_panic_or_wrap() {
let enc = pcm16_to_ulaw(&[i16::MAX, i16::MIN]);
let dec = ulaw_to_pcm16(&enc);
assert!(
dec[0] > 30000,
"i16::MAX -> near +full-scale, got {}",
dec[0]
);
assert!(
dec[1] < -30000,
"i16::MIN -> near -full-scale, got {}",
dec[1]
);
}
#[test]
fn alaw_decodes_to_g711_reference_values() {
assert_eq!(alaw_to_pcm16(&[0x00]), vec![-5504]);
assert_eq!(alaw_to_pcm16(&[0xFF]), vec![848]);
assert_eq!(alaw_to_pcm16(&[0x55]), vec![-8]);
assert_eq!(alaw_to_pcm16(&[0xD5]), vec![8]);
}
#[test]
fn empty_lengths_are_preserved_both_directions() {
assert!(ulaw_to_pcm16(&[]).is_empty());
assert!(pcm16_to_ulaw(&[]).is_empty());
assert!(alaw_to_pcm16(&[]).is_empty());
assert!(pcm16_to_alaw(&[]).is_empty());
}
#[test]
fn resample_empty_chunk_does_not_panic() {
let mut rs = Resampler::new(8000, 16000).unwrap();
let out = rs.process(&AudioChunk::new(vec![], 8000)).unwrap();
assert_eq!(out.sample_rate, 16000);
assert!(out.is_empty(), "empty in -> empty out");
assert!(rs.flush().unwrap().is_empty());
}
#[test]
fn resample_sub_block_chunk_buffers_without_panic() {
let mut rs = Resampler::new(8000, 16000).unwrap();
let out = rs.process(&AudioChunk::new(sine(8000, 37), 8000)).unwrap();
assert!(out.is_empty(), "37 (< BLOCK) samples are fully buffered");
let tail = rs.flush().unwrap();
assert_eq!(tail.sample_rate, 16000);
assert!(tail.len() < BLOCK * 2);
}
#[test]
fn resample_16k_to_8k_roughly_halves() {
let mut rs = Resampler::new(16000, 8000).unwrap();
let total_in = 3200usize; let out = rs
.process(&AudioChunk::new(sine(16000, total_in), 16000))
.unwrap();
let total_out = out.len() + rs.flush().unwrap().len();
assert_eq!(out.sample_rate, 8000);
let expected = total_in / 2;
assert!(
((expected - BLOCK)..=(expected + BLOCK)).contains(&total_out),
"16k->8k expected ~{expected}, got {total_out}"
);
}
#[test]
fn f32_to_i16_clamps_overshoot_without_wrapping() {
assert_eq!(f32_to_i16(2.0), i16::MAX);
assert_eq!(f32_to_i16(-2.0), i16::MIN);
assert_eq!(f32_to_i16(0.0), 0);
assert_eq!(f32_to_i16(1.0), i16::MAX); assert_eq!(f32_to_i16(-1.0), i16::MIN);
}
#[test]
fn ulaw_decode_then_pcm_is_stable_through_reencode() {
for code in 0u8..=255 {
let pcm1 = ulaw_to_pcm16(&[code]);
let reencoded = pcm16_to_ulaw(&pcm1);
let pcm2 = ulaw_to_pcm16(&reencoded);
assert_eq!(
pcm2, pcm1,
"μ-law code {code} not PCM-stable: {pcm1:?} -> {reencoded:?} -> {pcm2:?}"
);
}
}
#[test]
fn pcm_ulaw_roundtrip_within_g711_quantization() {
let pcm = representative_samples();
let ulaw = pcm16_to_ulaw(&pcm);
let back = ulaw_to_pcm16(&ulaw);
assert_eq!(back.len(), pcm.len());
for (orig, rt) in pcm.iter().zip(back.iter()) {
let o = *orig as i32;
let err = (o - *rt as i32).abs();
let rel_ok = err as f64 <= (o.unsigned_abs() as f64) * 0.03;
assert!(
err <= 8 || rel_ok,
"μ-law quantization error out of bounds: orig={orig} roundtrip={rt} err={err}"
);
}
}
#[test]
fn alaw_decode_then_pcm_is_stable_through_reencode() {
for code in 0u8..=255 {
let pcm1 = alaw_to_pcm16(&[code]);
let reencoded = pcm16_to_alaw(&pcm1);
let pcm2 = alaw_to_pcm16(&reencoded);
assert_eq!(pcm2, pcm1, "A-law code {code} not PCM-stable");
}
}
fn sine(rate: u32, n: usize) -> Vec<i16> {
(0..n)
.map(|i| {
let t = i as f32 / rate as f32;
(16000.0 * (2.0 * std::f32::consts::PI * 440.0 * t).sin()) as i16
})
.collect()
}
#[test]
fn rate_passthrough_returns_input_unchanged() {
let mut rs = Resampler::new(8000, 8000).unwrap();
let chunk = AudioChunk::new(sine(8000, 160), 8000);
let out = rs.process(&chunk).unwrap();
assert_eq!(out.sample_rate, 8000);
assert_eq!(out.pcm, chunk.pcm);
}
#[test]
fn resample_8k_to_16k_roughly_doubles_over_a_stream() {
let mut rs = Resampler::new(8000, 16000).unwrap();
let chunk_sizes = [160usize, 160, 80, 320, 200, 160, 240]; let total_in: usize = chunk_sizes.iter().sum();
let mut total_out = 0usize;
let mut phase = 0usize;
for &n in &chunk_sizes {
let samples: Vec<i16> = sine(8000, phase + n)[phase..].to_vec();
phase += n;
let out = rs.process(&AudioChunk::new(samples, 8000)).unwrap();
assert_eq!(out.sample_rate, 16000);
total_out += out.len();
}
total_out += rs.flush().unwrap().len();
let expected = total_in * 2;
let lo = expected.saturating_sub(BLOCK * 2);
let hi = expected + BLOCK * 2;
assert!(
(lo..=hi).contains(&total_out),
"8k->16k expected ~{expected} samples (in={total_in}), got {total_out}"
);
}
#[test]
fn resample_24k_to_8k_roughly_thirds_over_a_stream() {
let mut rs = Resampler::new(24000, 8000).unwrap();
let chunk_sizes = [480usize, 480, 240, 960, 300, 480]; let total_in: usize = chunk_sizes.iter().sum();
let mut total_out = 0usize;
let mut phase = 0usize;
for &n in &chunk_sizes {
let samples: Vec<i16> = sine(24000, phase + n)[phase..].to_vec();
phase += n;
let out = rs.process(&AudioChunk::new(samples, 24000)).unwrap();
assert_eq!(out.sample_rate, 8000);
total_out += out.len();
}
total_out += rs.flush().unwrap().len();
let expected = total_in / 3;
let lo = expected.saturating_sub(BLOCK);
let hi = expected + BLOCK;
assert!(
(lo..=hi).contains(&total_out),
"24k->8k expected ~{expected} samples (in={total_in}), got {total_out}"
);
}
#[test]
fn resample_8k_to_24k_roughly_triples() {
let mut rs = Resampler::new(8000, 24000).unwrap();
let total_in = 1600usize; let out = rs
.process(&AudioChunk::new(sine(8000, total_in), 8000))
.unwrap();
let tail = rs.flush().unwrap().len();
let total_out = out.len() + tail;
let expected = total_in * 3;
assert!(
((expected - BLOCK * 3)..=(expected + BLOCK * 3)).contains(&total_out),
"8k->24k expected ~{expected}, got {total_out}"
);
}
#[test]
fn rejects_zero_rate() {
assert!(Resampler::new(0, 16000).is_err());
assert!(Resampler::new(8000, 0).is_err());
}
#[test]
fn rejects_mismatched_input_rate() {
let mut rs = Resampler::new(8000, 16000).unwrap();
let wrong = AudioChunk::new(vec![0i16; 160], 16000);
assert!(rs.process(&wrong).is_err());
}
}