1use audioadapter_buffers::direct::InterleavedSlice;
42use rubato::{
43 Async, FixedAsync, Resampler as RubatoResampler, SincInterpolationParameters,
44 SincInterpolationType, WindowFunction,
45};
46
47use crate::error::FlowcatError;
48use crate::types::AudioChunk;
49
50pub fn ulaw_to_pcm16(ulaw: &[u8]) -> Vec<i16> {
56 ulaw.iter()
57 .map(|&b| audio_codec_algorithms::decode_ulaw(b))
58 .collect()
59}
60
61pub fn pcm16_to_ulaw(pcm: &[i16]) -> Vec<u8> {
63 pcm.iter()
64 .map(|&s| audio_codec_algorithms::encode_ulaw(s))
65 .collect()
66}
67
68pub fn alaw_to_pcm16(alaw: &[u8]) -> Vec<i16> {
74 alaw.iter()
75 .map(|&b| audio_codec_algorithms::decode_alaw(b))
76 .collect()
77}
78
79pub fn pcm16_to_alaw(pcm: &[i16]) -> Vec<u8> {
81 pcm.iter()
82 .map(|&s| audio_codec_algorithms::encode_alaw(s))
83 .collect()
84}
85
86const BLOCK: usize = 256;
96
97pub struct Resampler {
102 pub from_rate: u32,
104 pub to_rate: u32,
106
107 inner: Option<Async<f32>>,
110 in_buf: Vec<i16>,
112 scratch_in: Vec<f32>,
115 scratch_out: Vec<f32>,
118}
119
120impl Resampler {
121 pub fn new(from_rate: u32, to_rate: u32) -> Result<Self, FlowcatError> {
127 if from_rate == 0 || to_rate == 0 {
128 return Err(FlowcatError::Codec(format!(
129 "invalid sample rate: from={from_rate} to={to_rate} (must be > 0)"
130 )));
131 }
132
133 if from_rate == to_rate {
134 return Ok(Self {
135 from_rate,
136 to_rate,
137 inner: None,
138 in_buf: Vec::new(),
139 scratch_in: Vec::new(),
140 scratch_out: Vec::new(),
141 });
142 }
143
144 let ratio = to_rate as f64 / from_rate as f64;
145 let params = SincInterpolationParameters {
146 sinc_len: 256,
147 f_cutoff: 0.95,
148 oversampling_factor: 256,
149 interpolation: SincInterpolationType::Cubic,
150 window: WindowFunction::BlackmanHarris2,
151 };
152
153 let inner = Async::<f32>::new_sinc(ratio, 1.0, ¶ms, BLOCK, 1, FixedAsync::Input)
158 .map_err(|e| FlowcatError::Codec(format!("rubato init {from_rate}->{to_rate}: {e}")))?;
159
160 let scratch_out = vec![0.0f32; inner.output_frames_max()];
162 let scratch_in = Vec::with_capacity(BLOCK);
163
164 Ok(Self {
165 from_rate,
166 to_rate,
167 inner: Some(inner),
168 in_buf: Vec::new(),
169 scratch_in,
170 scratch_out,
171 })
172 }
173
174 pub fn process(&mut self, input: &AudioChunk) -> Result<AudioChunk, FlowcatError> {
182 if input.sample_rate != self.from_rate {
183 return Err(FlowcatError::Codec(format!(
184 "resampler expected input at {} Hz, got {} Hz",
185 self.from_rate, input.sample_rate
186 )));
187 }
188
189 let resampler = match self.inner.as_mut() {
191 None => return Ok(AudioChunk::new(input.pcm.clone(), self.to_rate)),
192 Some(r) => r,
193 };
194
195 self.in_buf.extend_from_slice(&input.pcm);
196
197 let mut out: Vec<i16> = Vec::new();
198 let mut consumed = 0usize;
199 while self.in_buf.len() - consumed >= BLOCK {
200 let block = &self.in_buf[consumed..consumed + BLOCK];
201
202 self.scratch_in.clear();
204 self.scratch_in
205 .extend(block.iter().map(|&s| s as f32 / 32768.0));
206
207 let in_view = InterleavedSlice::new(&self.scratch_in, 1, BLOCK)
210 .map_err(|e| FlowcatError::Codec(format!("rubato input view: {e}")))?;
211 let cap = self.scratch_out.len();
212 let mut out_view = InterleavedSlice::new_mut(&mut self.scratch_out, 1, cap)
213 .map_err(|e| FlowcatError::Codec(format!("rubato output view: {e}")))?;
214 let (_in_frames, out_frames) = resampler
215 .process_into_buffer(&in_view, &mut out_view, None)
216 .map_err(|e| FlowcatError::Codec(format!("rubato process: {e}")))?;
217
218 out.extend(
219 self.scratch_out[..out_frames]
220 .iter()
221 .map(|&s| f32_to_i16(s)),
222 );
223 consumed += BLOCK;
224 }
225
226 if consumed > 0 {
228 self.in_buf.drain(..consumed);
229 }
230
231 Ok(AudioChunk::new(out, self.to_rate))
232 }
233
234 pub fn flush(&mut self) -> Result<AudioChunk, FlowcatError> {
242 let resampler = match self.inner.as_mut() {
243 None => return Ok(AudioChunk::new(Vec::new(), self.to_rate)),
244 Some(r) => r,
245 };
246 if self.in_buf.is_empty() {
247 return Ok(AudioChunk::new(Vec::new(), self.to_rate));
248 }
249
250 self.scratch_in.clear();
251 self.scratch_in
252 .extend(self.in_buf.iter().map(|&s| s as f32 / 32768.0));
253 self.scratch_in.resize(BLOCK, 0.0); self.in_buf.clear();
255
256 let in_view = InterleavedSlice::new(&self.scratch_in, 1, BLOCK)
257 .map_err(|e| FlowcatError::Codec(format!("rubato input view: {e}")))?;
258 let cap = self.scratch_out.len();
259 let mut out_view = InterleavedSlice::new_mut(&mut self.scratch_out, 1, cap)
260 .map_err(|e| FlowcatError::Codec(format!("rubato output view: {e}")))?;
261 let (_in_frames, out_frames) = resampler
262 .process_into_buffer(&in_view, &mut out_view, None)
263 .map_err(|e| FlowcatError::Codec(format!("rubato flush: {e}")))?;
264
265 let out: Vec<i16> = self.scratch_out[..out_frames]
266 .iter()
267 .map(|&s| f32_to_i16(s))
268 .collect();
269 Ok(AudioChunk::new(out, self.to_rate))
270 }
271}
272
273#[inline]
276fn f32_to_i16(s: f32) -> i16 {
277 (s * 32768.0).round().clamp(-32768.0, 32767.0) as i16
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 fn representative_samples() -> Vec<i16> {
286 vec![
287 0, 1, -1, 100, -100, 1000, -1000, 8000, -8000, 16000, -16000, 32000, -32000, 32767,
288 -32768, 255, -256, 4095, -4096,
289 ]
290 }
291
292 #[test]
297 fn ulaw_decodes_to_g711_reference_values() {
298 assert_eq!(
299 ulaw_to_pcm16(&[0x00]),
300 vec![-32124],
301 "0x00 = negative full-scale"
302 );
303 assert_eq!(
304 ulaw_to_pcm16(&[0x80]),
305 vec![32124],
306 "0x80 = positive full-scale"
307 );
308 assert_eq!(ulaw_to_pcm16(&[0xFF]), vec![0], "0xFF = -0 (silence)");
309 assert_eq!(
310 ulaw_to_pcm16(&[0x7F]),
311 vec![0],
312 "0x7F = +0 (the other zero)"
313 );
314 }
315
316 #[test]
317 fn ulaw_silence_round_trips_to_canonical_zero_code() {
318 assert_eq!(
319 pcm16_to_ulaw(&[0]),
320 vec![0xFF],
321 "encode(0) = canonical -0 code"
322 );
323 assert_eq!(ulaw_to_pcm16(&[0xFF]), vec![0], "0xFF decodes to silence");
324 }
325
326 #[test]
327 fn ulaw_full_scale_encode_does_not_panic_or_wrap() {
328 let enc = pcm16_to_ulaw(&[i16::MAX, i16::MIN]);
329 let dec = ulaw_to_pcm16(&enc);
330 assert!(
331 dec[0] > 30000,
332 "i16::MAX -> near +full-scale, got {}",
333 dec[0]
334 );
335 assert!(
336 dec[1] < -30000,
337 "i16::MIN -> near -full-scale, got {}",
338 dec[1]
339 );
340 }
341
342 #[test]
343 fn alaw_decodes_to_g711_reference_values() {
344 assert_eq!(alaw_to_pcm16(&[0x00]), vec![-5504]);
345 assert_eq!(alaw_to_pcm16(&[0xFF]), vec![848]);
346 assert_eq!(alaw_to_pcm16(&[0x55]), vec![-8]);
350 assert_eq!(alaw_to_pcm16(&[0xD5]), vec![8]);
351 }
352
353 #[test]
354 fn empty_lengths_are_preserved_both_directions() {
355 assert!(ulaw_to_pcm16(&[]).is_empty());
356 assert!(pcm16_to_ulaw(&[]).is_empty());
357 assert!(alaw_to_pcm16(&[]).is_empty());
358 assert!(pcm16_to_alaw(&[]).is_empty());
359 }
360
361 #[test]
363 fn resample_empty_chunk_does_not_panic() {
364 let mut rs = Resampler::new(8000, 16000).unwrap();
365 let out = rs.process(&AudioChunk::new(vec![], 8000)).unwrap();
366 assert_eq!(out.sample_rate, 16000);
367 assert!(out.is_empty(), "empty in -> empty out");
368 assert!(rs.flush().unwrap().is_empty());
369 }
370
371 #[test]
372 fn resample_sub_block_chunk_buffers_without_panic() {
373 let mut rs = Resampler::new(8000, 16000).unwrap();
374 let out = rs.process(&AudioChunk::new(sine(8000, 37), 8000)).unwrap();
375 assert!(out.is_empty(), "37 (< BLOCK) samples are fully buffered");
376 let tail = rs.flush().unwrap();
377 assert_eq!(tail.sample_rate, 16000);
378 assert!(tail.len() < BLOCK * 2);
379 }
380
381 #[test]
382 fn resample_16k_to_8k_roughly_halves() {
383 let mut rs = Resampler::new(16000, 8000).unwrap();
384 let total_in = 3200usize; let out = rs
386 .process(&AudioChunk::new(sine(16000, total_in), 16000))
387 .unwrap();
388 let total_out = out.len() + rs.flush().unwrap().len();
389 assert_eq!(out.sample_rate, 8000);
390 let expected = total_in / 2;
391 assert!(
392 ((expected - BLOCK)..=(expected + BLOCK)).contains(&total_out),
393 "16k->8k expected ~{expected}, got {total_out}"
394 );
395 }
396
397 #[test]
398 fn f32_to_i16_clamps_overshoot_without_wrapping() {
399 assert_eq!(f32_to_i16(2.0), i16::MAX);
400 assert_eq!(f32_to_i16(-2.0), i16::MIN);
401 assert_eq!(f32_to_i16(0.0), 0);
402 assert_eq!(f32_to_i16(1.0), i16::MAX); assert_eq!(f32_to_i16(-1.0), i16::MIN);
404 }
405
406 #[test]
407 fn ulaw_decode_then_pcm_is_stable_through_reencode() {
408 for code in 0u8..=255 {
416 let pcm1 = ulaw_to_pcm16(&[code]);
417 let reencoded = pcm16_to_ulaw(&pcm1);
418 let pcm2 = ulaw_to_pcm16(&reencoded);
419 assert_eq!(
420 pcm2, pcm1,
421 "μ-law code {code} not PCM-stable: {pcm1:?} -> {reencoded:?} -> {pcm2:?}"
422 );
423 }
424 }
425
426 #[test]
427 fn pcm_ulaw_roundtrip_within_g711_quantization() {
428 let pcm = representative_samples();
434 let ulaw = pcm16_to_ulaw(&pcm);
435 let back = ulaw_to_pcm16(&ulaw);
436 assert_eq!(back.len(), pcm.len());
437 for (orig, rt) in pcm.iter().zip(back.iter()) {
438 let o = *orig as i32;
439 let err = (o - *rt as i32).abs();
440 let rel_ok = err as f64 <= (o.unsigned_abs() as f64) * 0.03;
443 assert!(
444 err <= 8 || rel_ok,
445 "μ-law quantization error out of bounds: orig={orig} roundtrip={rt} err={err}"
446 );
447 }
448 }
449
450 #[test]
451 fn alaw_decode_then_pcm_is_stable_through_reencode() {
452 for code in 0u8..=255 {
453 let pcm1 = alaw_to_pcm16(&[code]);
454 let reencoded = pcm16_to_alaw(&pcm1);
455 let pcm2 = alaw_to_pcm16(&reencoded);
456 assert_eq!(pcm2, pcm1, "A-law code {code} not PCM-stable");
457 }
458 }
459
460 fn sine(rate: u32, n: usize) -> Vec<i16> {
462 (0..n)
463 .map(|i| {
464 let t = i as f32 / rate as f32;
465 (16000.0 * (2.0 * std::f32::consts::PI * 440.0 * t).sin()) as i16
466 })
467 .collect()
468 }
469
470 #[test]
471 fn rate_passthrough_returns_input_unchanged() {
472 let mut rs = Resampler::new(8000, 8000).unwrap();
473 let chunk = AudioChunk::new(sine(8000, 160), 8000);
474 let out = rs.process(&chunk).unwrap();
475 assert_eq!(out.sample_rate, 8000);
476 assert_eq!(out.pcm, chunk.pcm);
477 }
478
479 #[test]
480 fn resample_8k_to_16k_roughly_doubles_over_a_stream() {
481 let mut rs = Resampler::new(8000, 16000).unwrap();
485 let chunk_sizes = [160usize, 160, 80, 320, 200, 160, 240]; let total_in: usize = chunk_sizes.iter().sum();
487
488 let mut total_out = 0usize;
489 let mut phase = 0usize;
490 for &n in &chunk_sizes {
491 let samples: Vec<i16> = sine(8000, phase + n)[phase..].to_vec();
492 phase += n;
493 let out = rs.process(&AudioChunk::new(samples, 8000)).unwrap();
494 assert_eq!(out.sample_rate, 16000);
495 total_out += out.len();
496 }
497 total_out += rs.flush().unwrap().len();
498
499 let expected = total_in * 2;
500 let lo = expected.saturating_sub(BLOCK * 2);
501 let hi = expected + BLOCK * 2;
502 assert!(
503 (lo..=hi).contains(&total_out),
504 "8k->16k expected ~{expected} samples (in={total_in}), got {total_out}"
505 );
506 }
507
508 #[test]
509 fn resample_24k_to_8k_roughly_thirds_over_a_stream() {
510 let mut rs = Resampler::new(24000, 8000).unwrap();
513 let chunk_sizes = [480usize, 480, 240, 960, 300, 480]; let total_in: usize = chunk_sizes.iter().sum();
515
516 let mut total_out = 0usize;
517 let mut phase = 0usize;
518 for &n in &chunk_sizes {
519 let samples: Vec<i16> = sine(24000, phase + n)[phase..].to_vec();
520 phase += n;
521 let out = rs.process(&AudioChunk::new(samples, 24000)).unwrap();
522 assert_eq!(out.sample_rate, 8000);
523 total_out += out.len();
524 }
525 total_out += rs.flush().unwrap().len();
526
527 let expected = total_in / 3;
528 let lo = expected.saturating_sub(BLOCK);
529 let hi = expected + BLOCK;
530 assert!(
531 (lo..=hi).contains(&total_out),
532 "24k->8k expected ~{expected} samples (in={total_in}), got {total_out}"
533 );
534 }
535
536 #[test]
537 fn resample_8k_to_24k_roughly_triples() {
538 let mut rs = Resampler::new(8000, 24000).unwrap();
539 let total_in = 1600usize; let out = rs
541 .process(&AudioChunk::new(sine(8000, total_in), 8000))
542 .unwrap();
543 let tail = rs.flush().unwrap().len();
544 let total_out = out.len() + tail;
545 let expected = total_in * 3;
546 assert!(
547 ((expected - BLOCK * 3)..=(expected + BLOCK * 3)).contains(&total_out),
548 "8k->24k expected ~{expected}, got {total_out}"
549 );
550 }
551
552 #[test]
553 fn rejects_zero_rate() {
554 assert!(Resampler::new(0, 16000).is_err());
555 assert!(Resampler::new(8000, 0).is_err());
556 }
557
558 #[test]
559 fn rejects_mismatched_input_rate() {
560 let mut rs = Resampler::new(8000, 16000).unwrap();
561 let wrong = AudioChunk::new(vec![0i16; 160], 16000);
562 assert!(rs.process(&wrong).is_err());
563 }
564}