use super::*;
use bytes::Bytes;
use rubato::Resampler;
#[test]
#[cfg_attr(miri, ignore = "rubato sinc resampler is too slow under Miri")]
fn test_resample_downsample_length() {
let input: Vec<f32> = (0..4800).map(|i| (i as f32).sin()).collect();
let output = resample(&input, SampleRate(48000), SampleRate(16000)).unwrap();
assert!(!output.is_empty());
assert!(
output.len() > 1400 && output.len() < 1700,
"Unexpected output length: {}",
output.len()
);
}
#[test]
#[cfg_attr(miri, ignore = "rubato sinc resampler is too slow under Miri")]
fn test_resample_upsample_length() {
let input: Vec<f32> = (0..800).map(|i| (i as f32).sin()).collect();
let output = resample(&input, SampleRate(8000), SampleRate(16000)).unwrap();
assert!(!output.is_empty());
assert!(
output.len() > 1200 && output.len() < 1700,
"Unexpected output length: {}",
output.len()
);
}
#[test]
#[cfg_attr(miri, ignore = "rubato sinc resampler is too slow under Miri")]
fn test_resample_preserves_dc() {
let input = vec![0.5_f32; 4800];
let output = resample(&input, SampleRate(48000), SampleRate(16000)).unwrap();
let start = output.len() / 10;
let end = output.len() - start;
for &sample in &output[start..end] {
assert!(
(sample - 0.5).abs() < 0.05,
"DC signal not preserved: {sample}"
);
}
}
#[test]
fn test_resample_empty() {
let output = resample(&[], SampleRate(48000), SampleRate(16000)).unwrap();
assert!(output.is_empty());
}
#[test]
fn test_resample_zero_rate_returns_empty() {
let input = vec![1.0, 2.0, 3.0];
assert!(
resample(&input, SampleRate(0), SampleRate(16000))
.unwrap()
.is_empty()
);
assert!(
resample(&input, SampleRate(16000), SampleRate(0))
.unwrap()
.is_empty()
);
}
#[test]
fn test_resample_same_rate() {
let input = vec![1.0, 2.0, 3.0, 4.0];
let output = resample(&input, SampleRate(16000), SampleRate(16000)).unwrap();
assert_eq!(output.len(), input.len());
for (a, b) in input.iter().zip(output.iter()) {
assert!((a - b).abs() < 1e-5);
}
}
#[test]
fn test_buffer_short_input_returns_none() {
let new_samples = vec![0.0; 100];
let mut buffer = Vec::new();
let result = prepare_audio_buffer(&new_samples, &mut buffer);
assert!(result.is_none());
assert_eq!(buffer.len(), 100);
}
#[test]
fn test_buffer_exact_frame() {
let new_samples = vec![1.0; N_FFT];
let mut buffer = Vec::new();
let result = prepare_audio_buffer(&new_samples, &mut buffer);
assert!(result.is_some());
let usable = result.unwrap();
assert_eq!(usable, N_FFT);
consume_audio_buffer(&mut buffer, usable);
assert!(buffer.is_empty());
}
#[test]
fn test_buffer_leftover_correct() {
let new_samples = vec![1.0; N_FFT + 50];
let mut buffer = Vec::new();
let result = prepare_audio_buffer(&new_samples, &mut buffer);
assert!(result.is_some());
let usable = result.unwrap();
assert_eq!(usable, N_FFT); consume_audio_buffer(&mut buffer, usable);
assert_eq!(buffer.len(), 50);
}
#[test]
fn test_buffer_accumulates_across_calls() {
let mut buffer = Vec::new();
let result = prepare_audio_buffer(&vec![1.0; 200], &mut buffer);
assert!(result.is_none());
assert_eq!(buffer.len(), 200);
let result = prepare_audio_buffer(&vec![2.0; 200], &mut buffer);
assert!(result.is_some());
let usable = result.unwrap();
assert_eq!(usable, 320);
consume_audio_buffer(&mut buffer, usable);
assert_eq!(buffer.len(), 80);
}
#[test]
fn test_buffer_truncation_at_5s() {
let mut buffer = vec![0.0; 90000];
let new_samples = vec![1.0; 1000];
let result = prepare_audio_buffer(&new_samples, &mut buffer);
assert!(result.is_some());
let usable = result.unwrap();
consume_audio_buffer(&mut buffer, usable);
assert!(usable + buffer.len() <= MAX_BUFFER_SAMPLES);
}
#[test]
fn test_buffer_multi_frame() {
let new_samples = vec![1.0; N_FFT + HOP_LENGTH];
let mut buffer = Vec::new();
let result = prepare_audio_buffer(&new_samples, &mut buffer);
assert!(result.is_some());
let usable = result.unwrap();
assert_eq!(usable, N_FFT + HOP_LENGTH);
consume_audio_buffer(&mut buffer, usable);
assert!(buffer.is_empty());
}
#[test]
#[cfg_attr(miri, ignore = "rubato sinc resampler is too slow under Miri")]
fn test_resample_nan_input() {
let input = vec![f32::NAN; 1000];
let output = resample(&input, SampleRate(48000), SampleRate(16000)).unwrap();
assert!(!output.is_empty());
for &s in &output {
assert!(s.is_finite(), "NaN should be sanitized to zero, got {s}");
}
}
#[test]
#[cfg_attr(miri, ignore = "rubato sinc resampler is too slow under Miri")]
fn test_resample_infinity_input() {
let input = vec![f32::INFINITY; 500];
let output = resample(&input, SampleRate(48000), SampleRate(16000)).unwrap();
assert!(!output.is_empty());
for &s in &output {
assert!(
s.is_finite(),
"Infinity should be sanitized to zero, got {s}"
);
}
}
#[test]
#[cfg_attr(miri, ignore = "rubato sinc resampler is too slow under Miri")]
fn test_resample_mixed_nan_normal() {
let mut input = vec![0.5_f32; 480];
input[100] = f32::NAN;
input[200] = f32::NEG_INFINITY;
let output = resample(&input, SampleRate(48000), SampleRate(16000)).unwrap();
assert!(!output.is_empty());
for &s in &output {
assert!(s.is_finite(), "Non-finite values should be sanitized");
}
}
#[test]
fn test_prepare_buffer_empty_input() {
let mut buffer = vec![1.0; 100];
let result = prepare_audio_buffer(&[], &mut buffer);
assert!(result.is_none());
assert_eq!(buffer.len(), 100);
}
#[test]
fn test_prepare_buffer_exactly_max() {
let new_samples = vec![1.0; MAX_BUFFER_SAMPLES];
let mut buffer = Vec::new();
let result = prepare_audio_buffer(&new_samples, &mut buffer);
assert!(result.is_some());
let usable = result.unwrap();
consume_audio_buffer(&mut buffer, usable);
assert!(usable + buffer.len() <= MAX_BUFFER_SAMPLES);
}
#[test]
fn test_prepare_buffer_one_over_max() {
let new_samples = vec![1.0; MAX_BUFFER_SAMPLES + 1];
let mut buffer = Vec::new();
let result = prepare_audio_buffer(&new_samples, &mut buffer);
assert!(result.is_some());
let usable = result.unwrap();
consume_audio_buffer(&mut buffer, usable);
assert!(usable + buffer.len() <= MAX_BUFFER_SAMPLES);
}
pub(super) fn make_wav_bytes(samples: &[i16], sample_rate: u32) -> Vec<u8> {
let data_size = (samples.len() * 2) as u32;
let file_size = 36 + data_size;
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&file_size.to_le_bytes());
buf.extend_from_slice(b"WAVE");
buf.extend_from_slice(b"fmt ");
buf.extend_from_slice(&16u32.to_le_bytes()); buf.extend_from_slice(&1u16.to_le_bytes()); buf.extend_from_slice(&1u16.to_le_bytes()); buf.extend_from_slice(&sample_rate.to_le_bytes());
buf.extend_from_slice(&(sample_rate * 2).to_le_bytes()); buf.extend_from_slice(&2u16.to_le_bytes()); buf.extend_from_slice(&16u16.to_le_bytes()); buf.extend_from_slice(b"data");
buf.extend_from_slice(&data_size.to_le_bytes());
for &s in samples {
buf.extend_from_slice(&s.to_le_bytes());
}
buf
}
#[test]
fn test_decode_audio_bytes_empty() {
let result = decode_audio_bytes(&[]);
assert!(result.is_err(), "Expected error for empty input, got Ok");
}
#[test]
fn test_decode_audio_bytes_invalid_data() {
let garbage: Vec<u8> = (0u8..128).collect();
let result = decode_audio_bytes(&garbage);
assert!(
result.is_err(),
"Expected error for invalid audio data, got Ok"
);
}
#[test]
fn test_decode_audio_bytes_ape_overflow_crash_is_graceful() {
let crash = include_bytes!("../../../tests/fixtures/ape_overflow_crash.bin");
assert_eq!(crash.len(), 36, "fixture must stay the 36-byte crash input");
let result = decode_audio_bytes(crash);
assert!(
result.is_err(),
"crafted APEv2 header must yield a decode error, not panic or Ok"
);
}
#[test]
fn test_decode_audio_bytes_wav() {
let silence: Vec<i16> = vec![0; 16000]; let wav = make_wav_bytes(&silence, 16000);
let samples = decode_audio_bytes(&wav).unwrap();
assert!(!samples.is_empty());
assert!((samples.len() as i64 - 16000).unsigned_abs() <= 100);
}
#[test]
fn test_probe_duration_wav_reports_declared_seconds() {
let wav = make_wav_bytes(&vec![0i16; 16000], 16000); let probed = probe_duration_bytes(Bytes::from(wav)).unwrap();
assert!(
matches!(probed, Some(s) if (s - 1.0).abs() < 1e-6),
"expected ~1.0 s, got {probed:?}"
);
}
#[test]
fn test_probe_duration_agrees_with_decoded_length() {
let wav = make_wav_bytes(&vec![0i16; 24000], 16000); let probed = probe_duration_bytes(Bytes::from(wav.clone()))
.unwrap()
.expect("WAV declares its duration");
let decoded_s = decode_audio_bytes_shared(Bytes::from(wav)).unwrap().len() as f64 / 16_000.0;
assert!(
(probed - decoded_s).abs() < 1e-3,
"probe {probed} vs decode {decoded_s}"
);
}
#[test]
fn test_probe_duration_non_container_does_not_claim_duration() {
let r = probe_duration_bytes(Bytes::from_static(b"definitely not audio"));
assert!(
r.is_err() || matches!(r, Ok(None)),
"garbage bytes must be Err or Ok(None), got {r:?}"
);
}
use std::io::{Read, Seek, SeekFrom};
#[test]
fn bytes_media_source_read_full() {
let data = Bytes::from_static(b"hello world");
let mut src = BytesMediaSource::new(data.clone());
let mut buf = vec![0u8; data.len()];
let n = src.read(&mut buf).unwrap();
assert_eq!(n, data.len());
assert_eq!(buf, data.as_ref());
let mut more = [0u8; 4];
assert_eq!(src.read(&mut more).unwrap(), 0);
}
#[test]
fn bytes_media_source_seek_end() {
let data = Bytes::from_static(b"abcdefgh");
let mut src = BytesMediaSource::new(data);
let pos = src.seek(SeekFrom::End(0)).unwrap();
assert_eq!(pos, 8);
let mut buf = [0u8; 4];
assert_eq!(src.read(&mut buf).unwrap(), 0);
}
#[test]
fn bytes_media_source_seek_past_end_ok() {
let data = Bytes::from_static(b"abc");
let mut src = BytesMediaSource::new(data);
let pos = src.seek(SeekFrom::Start(42)).unwrap();
assert_eq!(pos, 42);
let mut buf = [0u8; 4];
assert_eq!(src.read(&mut buf).unwrap(), 0);
}
#[test]
fn bytes_media_source_seek_before_start_err() {
let data = Bytes::from_static(b"abc");
let mut src = BytesMediaSource::new(data);
let err = src.seek(SeekFrom::Start(2)).unwrap();
assert_eq!(err, 2);
let result = src.seek(SeekFrom::Current(-100));
assert!(result.is_err(), "seek before start should error");
}
#[test]
fn bytes_media_source_partial_read_progress() {
let data = Bytes::from_static(b"abcdefghij");
let mut src = BytesMediaSource::new(data.clone());
let mut out = Vec::new();
let mut chunk = [0u8; 3];
loop {
let n = src.read(&mut chunk).unwrap();
if n == 0 {
break;
}
out.extend_from_slice(&chunk[..n]);
}
assert_eq!(out, data.as_ref());
}
#[test]
fn bytes_media_source_byte_len_matches() {
use symphonia::core::io::MediaSource as _;
let data = Bytes::from_static(b"0123456789");
let src = BytesMediaSource::new(data.clone());
assert_eq!(src.byte_len(), Some(data.len() as u64));
assert!(src.is_seekable());
}
#[test]
fn decode_audio_shim_matches_shared() {
let silence: Vec<i16> = vec![0; 16000];
let wav = make_wav_bytes(&silence, 16000);
let via_shim = decode_audio_bytes(&wav).unwrap();
let via_shared = decode_audio_bytes_shared(Bytes::copy_from_slice(&wav)).unwrap();
assert_eq!(via_shim.len(), via_shared.len());
for (a, b) in via_shim.iter().zip(via_shared.iter()) {
assert!((a - b).abs() < f32::EPSILON);
}
}
#[test]
fn test_parse_pcm16_basic() {
let data: &[u8] = &[0x00, 0x40, 0x00, 0xC0]; let mut pending: Option<u8> = None;
let samples = parse_pcm16_with_carry(data, &mut pending);
assert_eq!(samples.len(), 2);
assert!(pending.is_none());
assert!((samples[0] - 0.5).abs() < 0.001);
assert!((samples[1] + 0.5).abs() < 0.001);
}
#[test]
fn test_parse_pcm16_odd_length_carry() {
let mut pending: Option<u8> = None;
let samples = parse_pcm16_with_carry(&[0x00, 0x00, 0xFF], &mut pending);
assert_eq!(samples.len(), 1);
assert_eq!(pending, Some(0xFF));
let samples = parse_pcm16_with_carry(&[0x7F], &mut pending);
assert_eq!(samples.len(), 1);
assert!(pending.is_none());
}
#[test]
fn test_parse_pcm16_empty() {
let mut pending: Option<u8> = None;
let samples = parse_pcm16_with_carry(&[], &mut pending);
assert!(samples.is_empty());
assert!(pending.is_none());
}
#[test]
fn test_sample_budget_pure() {
assert_eq!(max_samples_for_secs(None, 16000), usize::MAX);
assert_eq!(max_samples_for_secs(Some(0.0), 16000), usize::MAX);
assert_eq!(max_samples_for_secs(Some(-5.0), 16000), usize::MAX);
assert_eq!(max_samples_for_secs(Some(1800.0), 16000), 1800 * 16000);
assert_eq!(max_samples_for_secs(Some(1800.0), 96_000), 1800 * 96_000);
assert_eq!(
max_samples_for_secs(Some(1800.0), 192_000),
4 * max_samples_for_secs(Some(1800.0), 48_000),
);
}
#[test]
fn test_whole_buffer_limit_clamps_but_only_downward() {
assert_eq!(whole_buffer_limit_secs(None), WHOLE_BUFFER_MAX_AUDIO_SECS);
assert_eq!(
whole_buffer_limit_secs(Some(0.0)),
WHOLE_BUFFER_MAX_AUDIO_SECS
);
assert_eq!(whole_buffer_limit_secs(Some(300.0)), 300.0);
assert_eq!(
whole_buffer_limit_secs(Some(10_000.0)),
WHOLE_BUFFER_MAX_AUDIO_SECS
);
}
#[test]
fn test_sample_rate_new_zero_errors() {
let result = SampleRate::new(0);
assert!(result.is_err(), "zero sample rate must error");
}
#[test]
fn test_sample_rate_new_positive_ok() {
let sr = SampleRate::new(16000).unwrap();
assert_eq!(sr.get(), 16000);
assert_eq!(sr.0, 16000);
}
fn make_stereo_wav_from_frames(frames: &[(i16, i16)], sample_rate: u32) -> Vec<u8> {
let data_size = (frames.len() * 4) as u32; let file_size = 36 + data_size;
let mut buf = Vec::new();
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&file_size.to_le_bytes());
buf.extend_from_slice(b"WAVE");
buf.extend_from_slice(b"fmt ");
buf.extend_from_slice(&16u32.to_le_bytes()); buf.extend_from_slice(&1u16.to_le_bytes()); buf.extend_from_slice(&2u16.to_le_bytes()); buf.extend_from_slice(&sample_rate.to_le_bytes());
buf.extend_from_slice(&(sample_rate * 4).to_le_bytes()); buf.extend_from_slice(&4u16.to_le_bytes()); buf.extend_from_slice(&16u16.to_le_bytes()); buf.extend_from_slice(b"data");
buf.extend_from_slice(&data_size.to_le_bytes());
for &(l, r) in frames {
buf.extend_from_slice(&l.to_le_bytes());
buf.extend_from_slice(&r.to_le_bytes());
}
buf
}
fn make_stereo_wav_bytes(left: &[i16], right: &[i16], sample_rate: u32) -> Vec<u8> {
assert_eq!(left.len(), right.len());
let num_samples = left.len();
let data_size = (num_samples * 4) as u32; let file_size = 36 + data_size;
let mut buf = Vec::with_capacity(file_size as usize);
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&file_size.to_le_bytes());
buf.extend_from_slice(b"WAVE");
buf.extend_from_slice(b"fmt ");
buf.extend_from_slice(&16u32.to_le_bytes()); buf.extend_from_slice(&1u16.to_le_bytes()); buf.extend_from_slice(&2u16.to_le_bytes()); buf.extend_from_slice(&sample_rate.to_le_bytes());
buf.extend_from_slice(&(sample_rate * 4).to_le_bytes()); buf.extend_from_slice(&4u16.to_le_bytes()); buf.extend_from_slice(&16u16.to_le_bytes()); buf.extend_from_slice(b"data");
buf.extend_from_slice(&data_size.to_le_bytes());
for i in 0..num_samples {
buf.extend_from_slice(&left[i].to_le_bytes());
buf.extend_from_slice(&right[i].to_le_bytes());
}
buf
}
#[test]
fn test_decode_stereo_mixes_to_mono() {
let frames: Vec<(i16, i16)> = vec![(16384, -16384); 16000];
let wav = make_stereo_wav_from_frames(&frames, 16000);
let samples = decode_audio_bytes(&wav).unwrap();
assert!(!samples.is_empty());
assert!((samples.len() as i64 - 16000).unsigned_abs() <= 100);
for &s in &samples {
assert!(s.abs() < 0.01, "stereo mix should cancel to ~0, got {s}");
}
}
#[test]
fn test_decode_stereo_constant_preserves_value() {
let frames: Vec<(i16, i16)> = vec![(8192, 8192); 8000];
let wav = make_stereo_wav_from_frames(&frames, 16000);
let samples = decode_audio_bytes(&wav).unwrap();
assert!(!samples.is_empty());
for &s in &samples {
assert!((s - 0.25).abs() < 0.01, "expected ~0.25, got {s}");
}
}
#[test]
#[cfg_attr(miri, ignore = "rubato sinc resampler is too slow under Miri")]
fn test_decode_wav_resamples_to_16k() {
let silence: Vec<i16> = vec![0; 48000]; let wav = make_wav_bytes(&silence, 48000);
let samples = decode_audio_bytes(&wav).unwrap();
assert!(!samples.is_empty());
assert!(
samples.len() > 14000 && samples.len() < 17000,
"expected ~16000 after resample, got {}",
samples.len()
);
}
#[test]
fn test_decode_audio_bytes_shared_channels_8khz() {
let sample_rate = 8000u32;
let num_samples = sample_rate as usize;
let left: Vec<i16> = (0..num_samples)
.map(|i| ((i as f32 / num_samples as f32) * 6000.0) as i16)
.collect();
let right: Vec<i16> = (0..num_samples)
.map(|i| ((1.0 - i as f32 / num_samples as f32) * 6000.0) as i16)
.collect();
let wav = make_stereo_wav_bytes(&left, &right, sample_rate);
let channels = decode_audio_bytes_shared_channels(Bytes::from(wav)).unwrap();
assert_eq!(channels.len(), 2);
assert!(channels[0].len() > num_samples * 15 / 10 && channels[0].len() < num_samples * 25 / 10);
assert!(channels[1].len() > num_samples * 15 / 10 && channels[1].len() < num_samples * 25 / 10);
assert!((channels[0][1000] - channels[1][1000]).abs() > 0.01);
}
#[test]
fn test_is_dual_mono_identical_channels() {
let samples: Vec<f32> = (0..1000).map(|i| (i as f32 * 0.01).sin()).collect();
assert!(is_dual_mono(&[samples.clone(), samples]));
}
#[test]
fn test_is_dual_mono_independent_channels() {
let left: Vec<f32> = (0..1000).map(|i| (i as f32 * 0.01).sin()).collect();
let right: Vec<f32> = (0..1000).map(|i| (i as f32 * 0.03).cos()).collect();
assert!(!is_dual_mono(&[left, right]));
}
#[test]
fn test_mix_channels_to_mono() {
let left = vec![1.0_f32];
let right = vec![-1.0_f32];
let mono = mix_channels_to_mono(&[left, right]);
assert_eq!(mono.len(), 1);
assert!(mono[0].abs() < 0.001);
}
#[test]
fn test_is_dual_mono_empty_channels_returns_false() {
assert!(!is_dual_mono(&[]));
}
#[test]
fn test_is_dual_mono_single_channel_returns_false() {
let samples: Vec<f32> = (0..100).map(|i| (i as f32 * 0.01).sin()).collect();
assert!(!is_dual_mono(&[samples]));
}
#[test]
fn test_mix_channels_to_mono_empty_input() {
let mono = mix_channels_to_mono(&[]);
assert!(mono.is_empty());
}
#[test]
fn test_decode_audio_bytes_shared_channels_mono_input() {
let samples: Vec<i16> = (0..8000).map(|i| (i as f32 * 0.1).sin() as i16).collect();
let wav = make_wav_bytes(&samples, 16000);
let mono = decode_audio_bytes(&wav).unwrap();
let channels = decode_audio_bytes_shared_channels(Bytes::copy_from_slice(&wav)).unwrap();
assert_eq!(channels.len(), 1);
assert_eq!(channels[0].len(), mono.len());
for (a, b) in channels[0].iter().zip(mono.iter()) {
assert!(
(a - b).abs() < 1e-5,
"split mono decode diverged: {a} vs {b}"
);
}
}
#[test]
fn test_resample_with_cache_empty_clears_buffer() {
let mut cache: Option<rubato::Async<f32>> = None;
let mut out = vec![1.0, 2.0, 3.0];
resample_with_cache(
Vec::new(),
SampleRate(48000),
SampleRate(16000),
&mut cache,
&mut out,
)
.unwrap();
assert!(out.is_empty(), "empty input must clear the output buffer");
assert!(cache.is_none(), "no resampler created for empty input");
}
#[test]
fn test_resample_with_cache_zero_rate_clears_buffer() {
let mut cache: Option<rubato::Async<f32>> = None;
let mut out = vec![9.0];
resample_with_cache(
vec![1.0, 2.0],
SampleRate(0),
SampleRate(16000),
&mut cache,
&mut out,
)
.unwrap();
assert!(out.is_empty());
let mut out2 = vec![9.0];
resample_with_cache(
vec![1.0, 2.0],
SampleRate(16000),
SampleRate(0),
&mut cache,
&mut out2,
)
.unwrap();
assert!(out2.is_empty());
}
#[test]
fn test_resample_with_cache_same_rate_passthrough() {
let mut cache: Option<rubato::Async<f32>> = None;
let input = vec![1.0, 2.0, 3.0, 4.0];
let mut out = Vec::new();
resample_with_cache(
input.clone(),
SampleRate(16000),
SampleRate(16000),
&mut cache,
&mut out,
)
.unwrap();
assert_eq!(out, input, "same rate must pass through unchanged");
assert!(
cache.is_none(),
"no resampler created for same-rate passthrough"
);
}
#[test]
#[cfg_attr(miri, ignore = "rubato sinc resampler is too slow under Miri")]
fn test_resample_with_cache_sanitizes_non_finite() {
let mut cache: Option<rubato::Async<f32>> = None;
let mut input = vec![0.5_f32; 480];
input[10] = f32::NAN;
input[20] = f32::INFINITY;
input[30] = f32::NEG_INFINITY;
let mut out = Vec::new();
resample_with_cache(
input,
SampleRate(48000),
SampleRate(16000),
&mut cache,
&mut out,
)
.unwrap();
assert!(!out.is_empty());
assert!(
cache.is_some(),
"resampler should be cached after first use"
);
for &s in &out {
assert!(
s.is_finite(),
"non-finite values must be sanitized, got {s}"
);
}
}
#[test]
#[cfg_attr(miri, ignore = "rubato sinc resampler is too slow under Miri")]
fn test_resample_with_cache_growing_chunks_match_one_shot() {
use std::f32::consts::PI;
let n = 48_000usize;
let signal: Vec<f32> = (0..n)
.map(|i| {
let t = i as f32 / 48_000.0;
0.5 * (2.0 * PI * 440.0 * t).sin() + 0.3 * (2.0 * PI * 1_200.0 * t).sin()
})
.collect();
let reference = resample(&signal, SampleRate(48_000), SampleRate(16_000)).unwrap();
let mut cache: Option<rubato::Async<f32>> = None;
let mut out = Vec::new();
let mut streamed = Vec::new();
let mut pos = 0usize;
let mut chunk = 480usize;
while pos < signal.len() {
let end = (pos + chunk).min(signal.len());
resample_with_cache(
signal[pos..end].to_vec(),
SampleRate(48_000),
SampleRate(16_000),
&mut cache,
&mut out,
)
.unwrap();
streamed.extend_from_slice(&out);
pos = end;
chunk += 480;
}
assert!(streamed.iter().all(|s| s.is_finite()));
let len_diff = reference.len().abs_diff(streamed.len());
assert!(
len_diff <= 2,
"chunked stream diverged from one-shot reference: {} vs {} samples",
streamed.len(),
reference.len()
);
let skip = 128;
let cmp_len = reference.len().min(streamed.len());
assert!(cmp_len > skip + 1_000, "not enough overlap to compare");
let mut max_diff = 0.0f32;
let mut max_at = 0usize;
for i in skip..cmp_len {
let d = (reference[i] - streamed[i]).abs();
if d > max_diff {
max_diff = d;
max_at = i;
}
}
assert!(
max_diff < 1e-3,
"seam discontinuity: max |streamed - reference| = {max_diff} at sample {max_at}"
);
}
#[test]
#[cfg_attr(miri, ignore = "rubato sinc resampler is too slow under Miri")]
fn test_resample_with_cache_growth_keeps_instance() {
let mut cache: Option<rubato::Async<f32>> = None;
let mut out = Vec::new();
let feed = |cache: &mut Option<rubato::Async<f32>>, out: &mut Vec<f32>, n: usize, seed: f32| {
let input: Vec<f32> = (0..n).map(|i| (i as f32 * seed).sin()).collect();
resample_with_cache(input, SampleRate(48_000), SampleRate(16_000), cache, out).unwrap();
};
feed(&mut cache, &mut out, 480, 0.01);
let capacity = cache.as_ref().unwrap().input_frames_max();
assert!(capacity >= 480);
feed(&mut cache, &mut out, 960, 0.02);
assert_eq!(
cache.as_ref().unwrap().input_frames_max(),
capacity,
"resampler recreated on frame growth"
);
feed(&mut cache, &mut out, 2_000, 0.03);
assert_eq!(cache.as_ref().unwrap().input_frames_max(), capacity);
feed(&mut cache, &mut out, capacity + 1_001, 0.01);
assert_eq!(
cache.as_ref().unwrap().input_frames_max(),
capacity,
"oversized frame must be split, not trigger recreation"
);
assert!(out.iter().all(|s| s.is_finite()));
feed(&mut cache, &mut out, capacity + 1, 0.02);
assert_eq!(cache.as_ref().unwrap().input_frames_max(), capacity);
assert!(out.iter().all(|s| s.is_finite()));
}
#[test]
#[cfg_attr(miri, ignore = "rubato sinc resampler is too slow under Miri")]
fn test_resample_with_cache_reuses_across_chunk_sizes() {
let mut cache: Option<rubato::Async<f32>> = None;
let mut out = Vec::new();
let input1: Vec<f32> = (0..480).map(|i| (i as f32 * 0.01).sin()).collect();
resample_with_cache(
input1,
SampleRate(48000),
SampleRate(16000),
&mut cache,
&mut out,
)
.unwrap();
assert!(cache.is_some());
let len_first = out.len();
assert!(len_first > 0);
let input2: Vec<f32> = (0..480).map(|i| (i as f32 * 0.02).cos()).collect();
resample_with_cache(
input2,
SampleRate(48000),
SampleRate(16000),
&mut cache,
&mut out,
)
.unwrap();
assert!(cache.is_some());
assert!(!out.is_empty());
let input3: Vec<f32> = (0..960).map(|i| (i as f32 * 0.01).sin()).collect();
resample_with_cache(
input3,
SampleRate(48000),
SampleRate(16000),
&mut cache,
&mut out,
)
.unwrap();
assert!(cache.is_some());
assert!(!out.is_empty());
for &s in &out {
assert!(s.is_finite());
}
}
#[test]
fn test_decode_rejects_adversarial_sample_rate() {
let silence: Vec<i16> = vec![0; 16]; let result = decode_audio_bytes(&make_wav_bytes(&silence, MAX_SAMPLE_RATE + 1));
assert!(
result.is_err(),
"sample_rate above MAX_SAMPLE_RATE must be rejected"
);
let result = decode_audio_bytes(&make_wav_bytes(&silence, 1_000_000_000));
assert!(result.is_err(), "absurd sample_rate must be rejected");
}
fn make_compressed_wav(tag: u16, sample_rate: u32, byte_rate: u32, payload: &[u8]) -> Vec<u8> {
let data_size = payload.len() as u32;
let mut buf = Vec::with_capacity(46 + payload.len());
buf.extend_from_slice(b"RIFF");
buf.extend_from_slice(&(38 + data_size).to_le_bytes());
buf.extend_from_slice(b"WAVE");
buf.extend_from_slice(b"fmt ");
buf.extend_from_slice(&18u32.to_le_bytes()); buf.extend_from_slice(&tag.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes()); buf.extend_from_slice(&sample_rate.to_le_bytes());
buf.extend_from_slice(&byte_rate.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes()); buf.extend_from_slice(&8u16.to_le_bytes()); buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(b"data");
buf.extend_from_slice(&data_size.to_le_bytes());
buf.extend_from_slice(payload);
buf
}
fn test_tone_8k(n_samples: usize) -> Vec<i16> {
(0..n_samples)
.map(|i| ((i as f32 * 0.05).sin() * 12000.0) as i16)
.collect()
}
#[test]
fn test_telephony_codec_from_name() {
assert_eq!(
TelephonyCodec::from_name("pcmu"),
Some(TelephonyCodec::Pcmu)
);
assert_eq!(
TelephonyCodec::from_name("PCMU"),
Some(TelephonyCodec::Pcmu)
);
assert_eq!(
TelephonyCodec::from_name("ulaw"),
Some(TelephonyCodec::Pcmu)
);
assert_eq!(
TelephonyCodec::from_name("pcma"),
Some(TelephonyCodec::Pcma)
);
assert_eq!(
TelephonyCodec::from_name("alaw"),
Some(TelephonyCodec::Pcma)
);
assert_eq!(
TelephonyCodec::from_name("G722"),
Some(TelephonyCodec::G722)
);
assert_eq!(TelephonyCodec::from_name("g729"), None);
assert_eq!(TelephonyCodec::from_name(""), None);
}
#[test]
fn test_telephony_codec_validate_sample_rate() {
assert!(TelephonyCodec::Pcmu.validate_sample_rate(8000).is_ok());
assert!(TelephonyCodec::Pcma.validate_sample_rate(16000).is_ok());
assert!(TelephonyCodec::Pcma.validate_sample_rate(48000).is_ok());
assert!(TelephonyCodec::Pcmu.validate_sample_rate(7999).is_err());
assert!(TelephonyCodec::Pcma.validate_sample_rate(48001).is_err());
assert!(TelephonyCodec::G722.validate_sample_rate(8000).is_ok());
assert!(TelephonyCodec::G722.validate_sample_rate(16000).is_ok());
assert!(TelephonyCodec::G722.validate_sample_rate(44100).is_err());
}
#[test]
#[cfg_attr(miri, ignore = "rubato sinc resampler is too slow under Miri")]
fn test_decode_telephony_raw_pcmu_roundtrip() {
let source = test_tone_8k(8000);
let mut encoder = audio_codec::pcmu::PcmuEncoder::new();
let encoded = audio_codec::Encoder::encode(&mut encoder, &source);
assert_eq!(encoded.len(), source.len(), "G.711 is one byte per sample");
let decoded = decode_telephony_raw(&encoded, TelephonyCodec::Pcmu, 8000).unwrap();
assert!(
decoded.len() > 12_000 && decoded.len() <= 16_000,
"unexpected decoded length {}",
decoded.len()
);
let expected = resample(
&source
.iter()
.map(|&s| f32::from(s) / 32768.0)
.collect::<Vec<_>>(),
SampleRate(8000),
SampleRate(16000),
)
.unwrap();
let n = decoded.len().min(expected.len());
let mse: f64 = decoded[..n]
.iter()
.zip(&expected[..n])
.map(|(a, b)| f64::from((a - b) * (a - b)))
.sum::<f64>()
/ n as f64;
assert!(
mse.sqrt() < 0.02,
"G.711 μ-law roundtrip RMSE {}",
mse.sqrt()
);
}
#[test]
#[cfg_attr(miri, ignore = "rubato sinc resampler is too slow under Miri")]
fn test_decode_telephony_raw_pcma_roundtrip() {
let source = test_tone_8k(8000);
let mut encoder = audio_codec::pcma::PcmaEncoder::new();
let encoded = audio_codec::Encoder::encode(&mut encoder, &source);
let decoded = decode_telephony_raw(&encoded, TelephonyCodec::Pcma, 8000).unwrap();
assert!(decoded.len() > 12_000 && decoded.len() <= 16_000);
assert!(decoded.iter().all(|s| s.is_finite()));
}
fn best_lag_rmse(a: &[f32], b: &[f32], max_lag: usize) -> f64 {
let mut best = f64::INFINITY;
for lag in 0..=max_lag {
for (a_slice, b_slice) in [
(a.get(lag..).unwrap_or(&[]), b),
(a, b.get(lag..).unwrap_or(&[])),
] {
let n = a_slice.len().min(b_slice.len());
if n < 100 {
continue;
}
let mse = a_slice[..n]
.iter()
.zip(&b_slice[..n])
.map(|(x, y)| {
let d = f64::from(x - y);
d * d
})
.sum::<f64>()
/ n as f64;
best = best.min(mse.sqrt());
}
}
best
}
#[test]
fn test_decode_telephony_raw_g722_roundtrip() {
let source: Vec<i16> = (0..16000)
.map(|i| ((i as f32 * 0.03).sin() * 10000.0) as i16)
.collect();
let mut encoder = audio_codec::g722::G722Encoder::new();
let encoded = audio_codec::Encoder::encode(&mut encoder, &source);
assert_eq!(encoded.len(), source.len() / 2, "64 kbit/s over 16 kHz");
let decoded = decode_telephony_raw(&encoded, TelephonyCodec::G722, 8000).unwrap();
assert_eq!(decoded.len(), source.len(), "G.722 stays at native 16 kHz");
let source_f32: Vec<f32> = source.iter().map(|&s| f32::from(s) / 32768.0).collect();
let rmse = best_lag_rmse(&decoded, &source_f32, 64);
assert!(rmse < 0.05, "G.722 roundtrip best-lag RMSE {rmse}");
}
#[test]
fn test_decode_telephony_raw_empty_errors() {
assert!(decode_telephony_raw(&[], TelephonyCodec::Pcmu, 8000).is_err());
assert!(decode_telephony_raw(&[], TelephonyCodec::G722, 16000).is_err());
}
#[test]
fn test_decode_telephony_raw_invalid_rate_errors() {
let payload = vec![0xFFu8; 160];
assert!(decode_telephony_raw(&payload, TelephonyCodec::Pcmu, 4000).is_err());
assert!(decode_telephony_raw(&payload, TelephonyCodec::G722, 44100).is_err());
}
#[test]
#[cfg_attr(miri, ignore = "rubato sinc resampler is too slow under Miri")]
fn test_decode_audio_bytes_g711_alaw_wav() {
let source = test_tone_8k(8000);
let mut encoder = audio_codec::pcma::PcmaEncoder::new();
let encoded = audio_codec::Encoder::encode(&mut encoder, &source);
let wav = make_compressed_wav(0x0006, 8000, 8000, &encoded);
let decoded = decode_audio_bytes(&wav).unwrap();
assert!(
decoded.len() > 12_000 && decoded.len() <= 16_000,
"unexpected decoded length {}",
decoded.len()
);
assert!(decoded.iter().all(|s| s.is_finite()));
}
#[test]
#[cfg_attr(miri, ignore = "rubato sinc resampler is too slow under Miri")]
fn test_decode_audio_bytes_g711_mulaw_wav() {
let source = test_tone_8k(8000);
let mut encoder = audio_codec::pcmu::PcmuEncoder::new();
let encoded = audio_codec::Encoder::encode(&mut encoder, &source);
let wav = make_compressed_wav(0x0007, 8000, 8000, &encoded);
let decoded = decode_audio_bytes(&wav).unwrap();
assert!(
decoded.len() > 12_000 && decoded.len() <= 16_000,
"unexpected decoded length {}",
decoded.len()
);
assert!(decoded.iter().all(|s| s.is_finite()));
}
#[test]
fn test_decode_audio_bytes_g722_wav_fallback() {
let source: Vec<i16> = (0..16000)
.map(|i| ((i as f32 * 0.03).sin() * 10000.0) as i16)
.collect();
let mut encoder = audio_codec::g722::G722Encoder::new();
let encoded = audio_codec::Encoder::encode(&mut encoder, &source);
for tag in [0x0064u16, 0x028F] {
let wav = make_compressed_wav(tag, 16000, 8000, &encoded);
let decoded = decode_audio_bytes(&wav).unwrap_or_else(|e| {
panic!("G.722 WAV (tag {tag:#06x}) must decode via the fallback: {e}")
});
assert_eq!(
decoded.len(),
source.len(),
"G.722 WAV must decode to native 16 kHz (tag {tag:#06x})"
);
}
}
#[test]
fn test_try_decode_g722_wav_malformed_inputs() {
assert!(try_decode_g722_wav(b"not a wave file", None).is_none());
let pcm_wav = make_wav_bytes(&[0i16; 32], 16000);
assert!(try_decode_g722_wav(&pcm_wav, None).is_none());
let mut header_only = make_compressed_wav(0x0064, 16000, 8000, &[]);
header_only.truncate(38); let result = try_decode_g722_wav(&header_only, None);
assert!(
matches!(result, Some(Err(_))),
"expected Some(Err), got {result:?}"
);
let mut enc = audio_codec::g722::G722Encoder::new();
let encoded = audio_codec::Encoder::encode(&mut enc, &[0i16; 320]);
let mut wav = make_compressed_wav(0x0064, 16000, 8000, &encoded);
wav.truncate(wav.len() - 3);
let result = try_decode_g722_wav(&wav, None);
assert!(
matches!(result, Some(Ok(_))),
"truncated data must not panic"
);
}
#[test]
fn test_decode_audio_bytes_g722_wav_ffmpeg_fixture_matches_reference() {
let wav = include_bytes!("../../../tests/fixtures/telephony/g722_tone.wav");
let reference_pcm = include_bytes!("../../../tests/fixtures/telephony/g722_tone_ffmpeg.pcm");
let ours = decode_audio_bytes(wav).expect("ffmpeg G.722 WAV must decode");
let reference: Vec<f32> = reference_pcm
.chunks_exact(2)
.map(|c| f32::from(i16::from_le_bytes([c[0], c[1]])) / 32768.0)
.collect();
assert_eq!(
ours.len(),
reference.len(),
"sample count must match ffmpeg's decode exactly"
);
let mse: f64 = ours
.iter()
.zip(reference.iter())
.map(|(a, b)| {
let d = f64::from(a - b);
d * d
})
.sum::<f64>()
/ ours.len() as f64;
assert!(
mse.sqrt() < 0.01,
"G.722 decode diverged from ffmpeg reference: RMSE {}",
mse.sqrt()
);
}
#[cfg(feature = "file-decode")]
fn eager_opus_reference(bytes: &[u8]) -> anyhow::Result<Vec<f32>> {
use symphonia::core::formats::probe::Hint;
use symphonia::core::formats::{FormatOptions, TrackType};
use symphonia::core::io::MediaSourceStream;
use symphonia::core::meta::MetadataOptions;
let source = BytesMediaSource::new(bytes::Bytes::copy_from_slice(bytes));
let mss = MediaSourceStream::new(Box::new(source), Default::default());
let mut format = symphonia::default::get_probe().probe(
&Hint::new(),
mss,
FormatOptions::default(),
MetadataOptions::default(),
)?;
let (track_id, sample_rate, channels) = {
let track = format
.default_track(TrackType::Audio)
.ok_or_else(|| anyhow::anyhow!("no audio track"))?;
let p = track
.codec_params
.as_ref()
.and_then(|p| p.audio())
.ok_or_else(|| anyhow::anyhow!("no audio params"))?;
(
track.id,
p.sample_rate.ok_or_else(|| anyhow::anyhow!("no rate"))?,
p.channels.as_ref().map(|c| c.count()).unwrap_or(1),
)
};
let mono = mix_channels_to_mono(&super::opus::decode_opus_channels(
&mut *format,
track_id,
channels,
usize::MAX,
f64::INFINITY,
)?);
let mut resampler = super::resample::ResampleTo16k::new(SampleRate(sample_rate), None);
for piece in mono.chunks(super::resample::RESAMPLE_STAGING_FRAMES) {
resampler.stage().extend_from_slice(piece);
resampler.flush_full()?;
}
let mut out = Vec::new();
resampler.finish_into(&mut out)?;
Ok(out)
}
#[test]
fn test_opus_streaming_decode_matches_whole_buffer() {
for (name, bytes) in [
(
"opus_tone.ogg",
&include_bytes!("../../../tests/fixtures/opus/opus_tone.ogg")[..],
),
(
"opus_tone_no_eos.ogg",
&include_bytes!("../../../tests/fixtures/opus/opus_tone_no_eos.ogg")[..],
),
(
"opus_tone_60ms.ogg",
&include_bytes!("../../../tests/fixtures/opus/opus_tone_60ms.ogg")[..],
),
] {
let streamed = decode_audio_bytes(bytes).expect("streaming decode");
let eager = eager_opus_reference(bytes).expect("whole-buffer decode");
assert!(!streamed.is_empty(), "{name} decoded to nothing");
assert_eq!(streamed, eager, "{name}: streaming decode diverged");
}
}
#[test]
fn test_opus_streaming_windows_match_slice_over_flat_decode() {
let bytes =
bytes::Bytes::from_static(include_bytes!("../../../tests/fixtures/opus/opus_tone.ogg"));
let spec = WindowSpec::new(16_000, 16_000, 3_200);
let flat = FileWindows::from_bytes(bytes.clone(), WindowSpec::flat(), None)
.expect("open flat")
.drain_to_vec()
.expect("drain");
let mut src = FileWindows::from_bytes(bytes, spec, None).expect("open windows");
let mut got = Vec::new();
while let Some(w) = src.next_window().expect("window") {
got.push((w.start_sample, w.samples.to_vec()));
}
let mut want = Vec::new();
let mut sw = SliceWindows::new(&flat, spec);
while let Some(w) = sw.next_window().expect("slice window") {
want.push((w.start_sample, w.samples.to_vec()));
}
assert!(
got.len() > 1,
"expected the windowed regime, got {}",
got.len()
);
assert_eq!(got, want);
assert_eq!(src.total_16k_samples(), flat.len());
}
#[test]
fn test_push_mono_mix_matches_mix_channels_to_mono() {
for channels in [1usize, 2] {
let frames = 97;
let pcm: Vec<f32> = (0..frames * channels)
.map(|i| ((i as f32) * 0.37).sin() * 0.8 - 0.13)
.collect();
let mut got = Vec::new();
super::opus::push_mono_mix(&pcm, channels, frames, &mut got);
let per_channel: Vec<Vec<f32>> = (0..channels)
.map(|c| (0..frames).map(|f| pcm[f * channels + c]).collect())
.collect();
assert_eq!(
got,
mix_channels_to_mono(&per_channel),
"channels={channels}"
);
}
}
#[test]
fn test_is_recoverable_packet_eof_matches_unexpected_eof_only() {
use std::io::{Error as IoError, ErrorKind};
use symphonia::core::errors::Error as SymError;
let eof = SymError::IoError(IoError::new(
ErrorKind::UnexpectedEof,
"unexpected end of file",
));
assert!(is_recoverable_packet_eof(&eof));
let other_io = SymError::IoError(IoError::other("disk full"));
assert!(!is_recoverable_packet_eof(&other_io));
let decode = SymError::DecodeError("bad page");
assert!(!is_recoverable_packet_eof(&decode));
let unsupported = SymError::Unsupported("codec");
assert!(!is_recoverable_packet_eof(&unsupported));
}
#[test]
fn test_decode_audio_bytes_opus_ogg_missing_eos_succeeds() {
let no_eos = include_bytes!("../../../tests/fixtures/opus/opus_tone_no_eos.ogg");
let with_eos = include_bytes!("../../../tests/fixtures/opus/opus_tone.ogg");
let decoded_no_eos = decode_audio_bytes(no_eos).expect("OGG/Opus without EOS must decode");
let decoded_with_eos =
decode_audio_bytes(with_eos).expect("OGG/Opus with EOS must still decode");
assert!(
!decoded_no_eos.is_empty(),
"missing-EOS stream must yield non-empty PCM"
);
let delta = (decoded_no_eos.len() as i64 - decoded_with_eos.len() as i64).unsigned_abs();
assert!(
delta <= 2,
"no-EOS length {} diverged from with-EOS length {}",
decoded_no_eos.len(),
decoded_with_eos.len()
);
let start = decoded_no_eos.len().min(decoded_with_eos.len()) / 4;
let end = start + 1000;
for (a, b) in decoded_no_eos[start..end]
.iter()
.zip(decoded_with_eos[start..end].iter())
{
assert!((a - b).abs() < f32::EPSILON);
}
}
#[test]
fn test_decode_audio_file_opus_missing_eos_matches_bytes() {
let no_eos = include_bytes!("../../../tests/fixtures/opus/opus_tone_no_eos.ogg");
let mut tmp = tempfile::NamedTempFile::with_suffix(".ogg").expect("temp file");
std::io::Write::write_all(&mut tmp, no_eos).expect("write temp file");
let via_file = decode_audio_file(tmp.path().to_str().expect("utf-8 path"))
.expect("missing-EOS OGG/Opus file must decode");
let via_bytes = decode_audio_bytes(no_eos).expect("missing-EOS bytes must decode");
assert_eq!(via_file.len(), via_bytes.len());
for (a, b) in via_file.iter().zip(via_bytes.iter()) {
assert!((a - b).abs() < f32::EPSILON);
}
}
#[test]
fn test_decode_audio_bytes_truncated_opus_headers_only_still_errors() {
let full = include_bytes!("../../../tests/fixtures/opus/opus_tone.ogg");
let mut pages = Vec::new();
let mut i = 0usize;
let data = full;
while i + 27 <= data.len() {
if &data[i..i + 4] != b"OggS" {
break;
}
let nseg = data[i + 26] as usize;
let body: usize = data[i + 27..i + 27 + nseg]
.iter()
.map(|&s| s as usize)
.sum();
let page_end = i + 27 + nseg + body;
pages.push(page_end);
i = page_end;
if pages.len() == 2 {
break;
}
}
assert!(pages.len() >= 2, "fixture must have header pages");
let headers_only = &data[..pages[1]];
let err = decode_audio_bytes(headers_only).expect_err("headers-only Opus must fail");
let msg = format!("{err:#}");
assert!(
msg.contains("packet")
|| msg.contains("end of file")
|| msg.contains("audio")
|| msg.contains("Decode")
|| msg.contains("Unsupported")
|| msg.contains("malformed")
|| msg.contains("Opus")
|| msg.contains("track")
|| msg.contains("empty")
|| msg.contains("No "),
"unexpected error for headers-only: {msg}"
);
}
#[test]
fn test_decode_audio_bytes_random_bytes_still_errors() {
let junk = [0u8; 64];
assert!(decode_audio_bytes(&junk).is_err());
}
#[test]
fn test_decode_audio_bytes_opus_ogg_matches_ffmpeg_reference() {
let ogg = include_bytes!("../../../tests/fixtures/opus/opus_tone.ogg");
let reference_pcm = include_bytes!("../../../tests/fixtures/opus/opus_tone_ffmpeg.pcm");
let ours = decode_audio_bytes(ogg).expect("OGG/Opus must decode");
let reference: Vec<f32> = reference_pcm
.chunks_exact(2)
.map(|c| f32::from(i16::from_le_bytes([c[0], c[1]])) / 32768.0)
.collect();
assert!(
ours.len() > 46_000 && ours.len() < 50_000,
"unexpected decoded length {}",
ours.len()
);
let rmse = best_lag_rmse(&ours, &reference, 1024);
assert!(
rmse < 0.02,
"Opus decode diverged from ffmpeg reference: RMSE {rmse}"
);
}
#[test]
fn test_decode_audio_bytes_opus_code3_multiframe_matches_ffmpeg_reference() {
let ogg = include_bytes!("../../../tests/fixtures/opus/opus_tone_60ms.ogg");
let reference_pcm = include_bytes!("../../../tests/fixtures/opus/opus_tone_60ms_ffmpeg.pcm");
let ours = decode_audio_bytes(ogg).expect("multi-frame OGG/Opus must decode");
let reference: Vec<f32> = reference_pcm
.chunks_exact(2)
.map(|c| f32::from(i16::from_le_bytes([c[0], c[1]])) / 32768.0)
.collect();
assert!(
ours.len() > 46_000 && ours.len() < 50_000,
"unexpected decoded length {}",
ours.len()
);
let rmse = best_lag_rmse(&ours, &reference, 1024);
assert!(
rmse < 0.02,
"multi-frame Opus decode diverged from ffmpeg reference: RMSE {rmse}"
);
}
#[test]
fn test_decode_audio_bytes_webm_opus_live_matches_ffmpeg_reference() {
let webm = include_bytes!("../../../tests/fixtures/opus/opus_tone_webm_live.webm");
let reference_pcm =
include_bytes!("../../../tests/fixtures/opus/opus_tone_webm_live_ffmpeg.pcm");
let ours = decode_audio_bytes(webm).expect("live WebM/Opus must decode");
let reference: Vec<f32> = reference_pcm
.chunks_exact(2)
.map(|c| f32::from(i16::from_le_bytes([c[0], c[1]])) / 32768.0)
.collect();
assert!(
ours.len() > 46_000 && ours.len() < 50_000,
"unexpected decoded length {}",
ours.len()
);
let rmse = best_lag_rmse(&ours, &reference, 1024);
assert!(
rmse < 0.02,
"WebM/Opus decode diverged from ffmpeg reference: RMSE {rmse}"
);
}
#[test]
fn test_decode_audio_file_webm_extension_matches_bytes() {
let webm = include_bytes!("../../../tests/fixtures/opus/opus_tone_webm_live.webm");
let mut tmp = tempfile::NamedTempFile::with_suffix(".webm").expect("temp file");
std::io::Write::write_all(&mut tmp, webm).expect("write temp file");
let via_file =
decode_audio_file(tmp.path().to_str().expect("utf-8 path")).expect("WebM file must decode");
let via_bytes = decode_audio_bytes(webm).expect("WebM bytes must decode");
assert_eq!(via_file.len(), via_bytes.len());
for (a, b) in via_file.iter().zip(via_bytes.iter()) {
assert!((a - b).abs() < f32::EPSILON);
}
}
#[test]
fn test_decode_audio_file_opus_extension_matches_bytes() {
let ogg = include_bytes!("../../../tests/fixtures/opus/opus_tone.ogg");
let mut tmp = tempfile::NamedTempFile::with_suffix(".opus").expect("temp file");
std::io::Write::write_all(&mut tmp, ogg).expect("write temp file");
let via_file = decode_audio_file(tmp.path().to_str().expect("utf-8 path"))
.expect("OGG/Opus file must decode");
let via_bytes = decode_audio_bytes(ogg).expect("OGG/Opus bytes must decode");
assert_eq!(via_file.len(), via_bytes.len());
for (a, b) in via_file.iter().zip(via_bytes.iter()) {
assert!((a - b).abs() < f32::EPSILON);
}
}
#[test]
fn test_encode_wav_pcm16_roundtrip() {
let source: Vec<f32> = (0..16000).map(|i| (i as f32 * 0.02).sin() * 0.5).collect();
let wav = encode_wav_pcm16(&source, 16000);
let decoded = decode_audio_bytes(&wav).unwrap();
assert_eq!(decoded.len(), source.len());
for (a, b) in decoded.iter().zip(source.iter()) {
assert!((a - b).abs() < 1e-3, "PCM16 roundtrip drift: {a} vs {b}");
}
}
#[test]
fn test_encode_wav_pcm16_clamps_and_sanitizes() {
let samples = [2.0f32, -2.0, f32::NAN, 0.5];
let wav = encode_wav_pcm16(&samples, 16000);
let decoded = decode_audio_bytes(&wav).unwrap();
assert!((decoded[0] - 1.0).abs() < 1e-3, "must clamp to +1");
assert!((decoded[1] + 1.0).abs() < 1e-3, "must clamp to -1");
assert!(decoded[2].abs() < 1e-3, "NaN must become silence");
assert!((decoded[3] - 0.5).abs() < 1e-3);
}
fn fixture_tone_pcm() -> Vec<i16> {
let wav = include_bytes!("../../../tests/fixtures/telephony/tone_src.wav");
let data = super::telephony::find_riff_chunk(wav, b"data").expect("fixture data chunk");
data.chunks_exact(2)
.map(|b| i16::from_le_bytes([b[0], b[1]]))
.collect()
}
fn sweep_pcm(rate: u32, seconds: f32) -> Vec<i16> {
let n = (rate as f32 * seconds) as usize;
(0..n)
.map(|i| {
let t = i as f32 / rate as f32;
let f = 50.0 + (0.45 * rate as f32 - 50.0) * (t / seconds);
(0.8 * (std::f32::consts::PI * f * t).sin() * 32000.0) as i16
})
.collect()
}
fn tone_pcm(rate: u32, seconds: f64, freq: f64) -> Vec<i16> {
let n = (f64::from(rate) * seconds) as usize;
(0..n)
.map(|i| {
let t = i as f64 / f64::from(rate);
(0.8 * (std::f64::consts::TAU * freq * t).sin() * 32000.0) as i16
})
.collect()
}
fn max_phase_drift_16k(samples: &[f32], freq: f64) -> f64 {
const WINDOW: usize = 16_000;
let phase_of = |start: usize| {
let mut re = 0.0f64;
let mut im = 0.0f64;
for (i, &v) in samples[start..start + WINDOW].iter().enumerate() {
let w = std::f64::consts::TAU * freq * ((start + i) as f64 / 16_000.0);
re += f64::from(v) * w.cos();
im += f64::from(v) * w.sin();
}
im.atan2(re)
};
let first = phase_of(0);
let mut worst = 0.0f64;
for w in 0..samples.len() / WINDOW {
let mut d = phase_of(w * WINDOW) - first;
d -= std::f64::consts::TAU * (d / std::f64::consts::TAU).round();
worst = worst.max(d.abs());
}
worst
}
fn signal_to_error_db(reference: &[f32], candidate: &[f32]) -> f64 {
let mut err = 0.0f64;
let mut sig = 0.0f64;
for (&r, &c) in reference.iter().zip(candidate) {
let d = f64::from(r) - f64::from(c);
err += d * d;
sig += f64::from(r) * f64::from(r);
}
if err == 0.0 {
return f64::INFINITY;
}
10.0 * (sig / err).log10()
}
fn assert_matches_whole_buffer(streamed: &[f32], reference: &[f32], what: &str) {
let len_diff = streamed.len().abs_diff(reference.len());
assert!(
len_diff <= 1,
"{what}: length diverged, streaming {} vs whole-buffer {}",
streamed.len(),
reference.len()
);
let cmp = streamed.len().min(reference.len());
assert!(cmp > 0, "{what}: nothing to compare");
let mut max_diff = 0.0f32;
let mut max_at = 0usize;
for i in 0..cmp {
let d = (streamed[i] - reference[i]).abs();
if d > max_diff {
max_diff = d;
max_at = i;
}
}
assert!(
max_diff <= 1e-4,
"{what}: max |streaming - whole-buffer| = {max_diff} at sample {max_at}"
);
}
fn check_mono_equivalence(pcm: &[i16], rate: u32, what: &str) {
let at_source = decode_audio_bytes(&make_wav_bytes(pcm, 16000)).unwrap();
let reference = resample(&at_source, SampleRate(rate), SampleRate(16000)).unwrap();
let streamed = decode_audio_bytes(&make_wav_bytes(pcm, rate)).unwrap();
assert_matches_whole_buffer(&streamed, &reference, what);
}
fn check_stereo_equivalence(left: &[i16], right: &[i16], rate: u32, what: &str) {
let mixed_at_source = decode_audio_bytes(&make_stereo_wav_bytes(left, right, 16000)).unwrap();
let mixed_reference = resample(&mixed_at_source, SampleRate(rate), SampleRate(16000)).unwrap();
let mixed_streamed = decode_audio_bytes(&make_stereo_wav_bytes(left, right, rate)).unwrap();
assert_matches_whole_buffer(&mixed_streamed, &mixed_reference, &format!("{what} mixed"));
let split_at_source =
decode_audio_bytes_shared_channels(Bytes::from(make_stereo_wav_bytes(left, right, 16000)))
.unwrap();
let split_streamed =
decode_audio_bytes_shared_channels(Bytes::from(make_stereo_wav_bytes(left, right, rate)))
.unwrap();
assert_eq!(split_streamed.len(), split_at_source.len());
for (c, (streamed, source)) in split_streamed.iter().zip(&split_at_source).enumerate() {
let reference = resample(source, SampleRate(rate), SampleRate(16000)).unwrap();
assert_matches_whole_buffer(streamed, &reference, &format!("{what} channel {c}"));
}
}
#[test]
#[cfg_attr(miri, ignore = "rubato sinc resampler is too slow under Miri")]
fn test_streaming_decode_matches_whole_buffer_resample_48k() {
let pcm = sweep_pcm(48_000, 2.5);
check_mono_equivalence(&pcm, 48_000, "48k sweep mono");
}
#[test]
#[cfg_attr(miri, ignore = "rubato sinc resampler is too slow under Miri")]
fn test_streaming_decode_matches_whole_buffer_resample_44k1() {
let pcm = sweep_pcm(44_100, 2.5);
check_mono_equivalence(&pcm, 44_100, "44.1k sweep mono");
}
#[test]
#[cfg_attr(miri, ignore = "rubato sinc resampler is too slow under Miri")]
fn test_streaming_decode_matches_whole_buffer_resample_stereo_48k() {
let left = sweep_pcm(48_000, 2.5);
let right: Vec<i16> = left.iter().rev().copied().collect();
check_stereo_equivalence(&left, &right, 48_000, "48k sweep stereo");
}
#[test]
#[cfg_attr(miri, ignore = "rubato sinc resampler is too slow under Miri")]
fn test_streaming_decode_matches_whole_buffer_resample_stereo_44k1() {
let left = sweep_pcm(44_100, 2.5);
let right: Vec<i16> = left.iter().rev().copied().collect();
check_stereo_equivalence(&left, &right, 44_100, "44.1k sweep stereo");
}
#[test]
#[ignore = "~50 s in debug; long-duration numeric gate, run on main push"]
#[cfg_attr(miri, ignore = "rubato sinc resampler is too slow under Miri")]
fn test_streaming_decode_long_44k1_input_holds_phase_better_than_whole_buffer() {
const FREQ: f64 = 1_000.0;
let pcm = tone_pcm(44_100, 300.0, FREQ);
let at_source = decode_audio_bytes(&make_wav_bytes(&pcm, 16000)).unwrap();
let reference = resample(&at_source, SampleRate(44_100), SampleRate(16000)).unwrap();
let streamed = decode_audio_bytes(&make_wav_bytes(&pcm, 44_100)).unwrap();
assert_eq!(
streamed.len(),
reference.len(),
"long 44.1k input: length diverged"
);
let snr = signal_to_error_db(&reference, &streamed);
assert!(
snr >= 70.0,
"long 44.1k input: streaming vs whole-buffer SNR {snr:.1} dB below the 70 dB floor"
);
let streamed_drift = max_phase_drift_16k(&streamed, FREQ);
let reference_drift = max_phase_drift_16k(&reference, FREQ);
assert!(
streamed_drift <= reference_drift,
"long 44.1k input: staged path drifted {streamed_drift:.3e} rad, \
more than the whole-buffer reference's {reference_drift:.3e} rad"
);
assert!(
streamed_drift <= 2e-4,
"long 44.1k input: staged path phase drift {streamed_drift:.3e} rad exceeds 2e-4"
);
}
#[test]
#[cfg_attr(miri, ignore = "rubato sinc resampler is too slow under Miri")]
fn test_streaming_decode_matches_whole_buffer_resample_fixture() {
let pcm = [fixture_tone_pcm(), fixture_tone_pcm()].concat();
assert!(
pcm.len() > super::resample::RESAMPLE_STAGING_FRAMES,
"fixture must span more than one staging flush, got {} samples",
pcm.len()
);
check_mono_equivalence(&pcm, 48_000, "fixture 48k");
check_mono_equivalence(&pcm, 44_100, "fixture 44.1k");
}
#[test]
fn test_streaming_decode_16k_input_is_bit_identical() {
let pcm = fixture_tone_pcm();
let mono = decode_audio_bytes(&make_wav_bytes(&pcm, 16000)).unwrap();
assert_eq!(mono.len(), pcm.len());
for (i, (&raw, &got)) in pcm.iter().zip(&mono).enumerate() {
let expected = f32::from(raw) / 32768.0;
assert_eq!(
got.to_bits(),
expected.to_bits(),
"sample {i} was filtered: {got} vs {expected}"
);
}
let right: Vec<i16> = pcm.iter().rev().copied().collect();
let channels =
decode_audio_bytes_shared_channels(Bytes::from(make_stereo_wav_bytes(&pcm, &right, 16000)))
.unwrap();
assert_eq!(channels.len(), 2);
for (c, raw) in [&pcm, &right].iter().enumerate() {
assert_eq!(channels[c].len(), raw.len());
for (i, (&r, &got)) in raw.iter().zip(&channels[c]).enumerate() {
let expected = f32::from(r) / 32768.0;
assert_eq!(
got.to_bits(),
expected.to_bits(),
"channel {c} sample {i} was filtered: {got} vs {expected}"
);
}
}
}
#[test]
#[cfg_attr(miri, ignore = "rubato sinc resampler is too slow under Miri")]
fn test_telephony_raw_streaming_matches_whole_buffer_resample() {
let pcm = sweep_pcm(8_000, 12.5);
assert!(
pcm.len() > super::resample::RESAMPLE_STAGING_FRAMES,
"clip must span more than one staging flush, got {} samples",
pcm.len()
);
let mut encoder = audio_codec::pcmu::PcmuEncoder::new();
let encoded = audio_codec::Encoder::encode(&mut encoder, &pcm);
let mut decoder = audio_codec::pcmu::PcmuDecoder::new();
let round_tripped = audio_codec::Decoder::decode(&mut decoder, &encoded);
let at_source: Vec<f32> = round_tripped
.iter()
.map(|&s| f32::from(s) / 32768.0)
.collect();
let reference = resample(&at_source, SampleRate(8_000), SampleRate(16_000)).unwrap();
let streamed = decode_telephony_raw(&encoded, TelephonyCodec::Pcmu, 8_000).unwrap();
assert_matches_whole_buffer(&streamed, &reference, "pcmu 8k");
}
#[test]
fn test_audio_chunks_match_flat_decode_chunked() {
for &n in &[1usize, 999, 16_000, 16_001, 48_000, 120_000] {
for &chunk in &[16_000usize, 640, 7_000] {
let src: Vec<f32> = (0..n)
.map(|i| 0.4 * ((i as f32) * 0.017).sin() + 0.2 * ((i as f32) * 0.0031).sin())
.collect();
let wav = bytes::Bytes::from(encode_wav_pcm16(&src, 16000));
let flat = decode_audio_bytes(&wav).expect("flat decode");
let mut chunks = AudioChunks::from_bytes(wav, chunk, None).expect("open chunks");
let mut got: Vec<Vec<f32>> = Vec::new();
while let Some(c) = chunks.next_chunk().expect("chunk") {
got.push(c.to_vec());
}
let want: Vec<Vec<f32>> = flat.chunks(chunk).map(<[f32]>::to_vec).collect();
assert_eq!(got, want, "n={n} chunk={chunk}");
assert_eq!(
chunks.total_16k_samples(),
flat.len(),
"n={n} chunk={chunk}"
);
}
}
}
#[test]
fn test_audio_chunks_honour_max_audio_secs() {
let src = vec![0.1f32; 16_000 * 5];
let wav = bytes::Bytes::from(encode_wav_pcm16(&src, 16000));
let mut ok = AudioChunks::from_bytes(wav.clone(), 16_000, None).expect("open");
let mut total = 0;
while let Some(c) = ok.next_chunk().expect("chunk") {
total += c.len();
}
assert_eq!(total, src.len());
let mut capped = AudioChunks::from_bytes(wav, 16_000, Some(1.0)).expect("open");
let err = loop {
match capped.next_chunk() {
Ok(Some(_)) => continue,
Ok(None) => panic!("a 5 s clip must not drain under a 1 s limit"),
Err(e) => break e,
}
};
assert!(
matches!(
err.downcast_ref::<crate::error::GigasttError>(),
Some(crate::error::GigasttError::AudioTooLong { .. })
),
"expected a typed AudioTooLong, got: {err:#}"
);
}
#[test]
fn test_dual_mono_detector_matches_batch_correlation() {
let n = 40_000;
let base: Vec<f32> = (0..n)
.map(|i| 0.5 * ((i as f32) * 0.013).sin() + 0.2 * ((i as f32) * 0.0007).cos())
.collect();
let other: Vec<f32> = (0..n)
.map(|i| 0.5 * ((i as f32) * 0.031 + 1.7).sin() - 0.3 * ((i as f32) * 0.0021).cos())
.collect();
let cases: Vec<(&str, Vec<f32>, Vec<f32>)> = vec![
("identical", base.clone(), base.clone()),
(
"attenuated",
base.clone(),
base.iter().map(|v| v * 0.85).collect(),
),
(
"mostly-same",
base.clone(),
base.iter()
.zip(&other)
.map(|(l, r)| l * 0.9 + r * 0.1)
.collect(),
),
("independent", base.clone(), other.clone()),
("inverted", base.clone(), base.iter().map(|v| -v).collect()),
("right-silent", base.clone(), vec![0.0; n]),
("both-silent", vec![0.0; n], vec![0.0; n]),
(
"dc-offset",
base.iter().map(|v| v + 0.7).collect(),
base.iter().map(|v| v + 0.7).collect(),
),
];
for (label, left, right) in cases {
let batch = super::decode::normalized_correlation_for_test(&left, &right);
let mut det = DualMonoDetector::new();
let mut i = 0;
let mut step = 1;
while i < left.len() {
let end = (i + step).min(left.len());
det.push(&left[i..end], &right[i..end]);
i = end;
step = step % 1_237 + 1;
}
assert!(
(det.correlation() - batch).abs() < 1e-6,
"{label}: streaming {} vs batch {batch}",
det.correlation()
);
let want = is_dual_mono(&[left, right]);
assert_eq!(det.is_dual_mono(), want, "{label}: verdict diverged");
}
}
#[test]
fn test_dual_mono_detector_empty_is_not_dual_mono() {
let det = DualMonoDetector::new();
assert!(!det.is_dual_mono());
assert_eq!(det.correlation(), 0.0);
let mut det = DualMonoDetector::new();
det.push(&[], &[]);
assert!(!det.is_dual_mono());
}
#[test]
fn test_dual_mono_detector_uses_the_overlap_only() {
let a = vec![0.3f32, -0.4, 0.5, 0.9, -0.1];
let b = vec![0.3f32, -0.4, 0.5];
let mut det = DualMonoDetector::new();
det.push(&a, &b);
let batch = super::decode::normalized_correlation_for_test(&a[..3], &b);
assert!((det.correlation() - batch).abs() < 1e-9);
}
#[cfg(feature = "file-decode")]
fn stereo_wav(left: &[f32], right: &[f32], rate: u32) -> bytes::Bytes {
let frames = left.len().min(right.len());
let data_bytes = (frames * 4) as u32;
let mut w = Vec::with_capacity(44 + data_bytes as usize);
w.extend_from_slice(b"RIFF");
w.extend_from_slice(&(36 + data_bytes).to_le_bytes());
w.extend_from_slice(b"WAVE");
w.extend_from_slice(b"fmt ");
w.extend_from_slice(&16u32.to_le_bytes());
w.extend_from_slice(&1u16.to_le_bytes());
w.extend_from_slice(&2u16.to_le_bytes());
w.extend_from_slice(&rate.to_le_bytes());
w.extend_from_slice(&(rate * 4).to_le_bytes());
w.extend_from_slice(&4u16.to_le_bytes());
w.extend_from_slice(&16u16.to_le_bytes());
w.extend_from_slice(b"data");
w.extend_from_slice(&data_bytes.to_le_bytes());
let q = |s: f32| (s.clamp(-1.0, 1.0) * i16::MAX as f32) as i16;
for i in 0..frames {
w.extend_from_slice(&q(left[i]).to_le_bytes());
w.extend_from_slice(&q(right[i]).to_le_bytes());
}
bytes::Bytes::from(w)
}
#[cfg(feature = "file-decode")]
#[test]
fn test_scan_channels_matches_batch_dual_mono_verdict() {
for rate in [16_000u32, 48_000] {
let n = rate as usize * 2;
let a: Vec<f32> = (0..n)
.map(|i| 0.5 * ((i as f32) * 0.011).sin() + 0.15 * ((i as f32) * 0.0009).cos())
.collect();
let b: Vec<f32> = (0..n)
.map(|i| 0.45 * ((i as f32) * 0.029 + 0.9).sin())
.collect();
for (label, left, right) in [
("identical", a.clone(), a.clone()),
("attenuated", a.clone(), a.iter().map(|v| v * 0.8).collect()),
("stereo", a.clone(), b.clone()),
] {
let wav = stereo_wav(&left, &right, rate);
let batch = decode_audio_bytes_shared_channels(wav.clone()).expect("batch");
let want = is_dual_mono(&batch);
let scan = scan_channels(wav, None).expect("scan");
assert_eq!(scan.channels, 2, "{label} @{rate}");
assert_eq!(
scan.dual_mono, want,
"{label} @{rate}: scan verdict diverged from batch"
);
assert_eq!(
scan.mono_fallback_reason().is_some(),
want,
"{label} @{rate}"
);
}
}
}
#[cfg(feature = "file-decode")]
#[test]
fn test_scan_channels_non_stereo_is_header_only() {
let mono = encode_wav_pcm16(&vec![0.2f32; 16_000], 16000);
let scan = scan_channels(bytes::Bytes::from(mono), None).expect("scan mono");
assert_eq!(scan.channels, 1);
assert!(!scan.dual_mono);
assert_eq!(scan.mono_fallback_reason(), Some("mono audio"));
}
#[cfg(feature = "file-decode")]
#[test]
fn test_channel_scan_fallback_reasons() {
let r = |channels, dual_mono| {
ChannelScan {
channels,
dual_mono,
}
.mono_fallback_reason()
};
assert_eq!(r(0, false), Some("no channels"));
assert_eq!(r(1, false), Some("mono audio"));
assert_eq!(r(2, true), Some("dual-mono audio"));
assert_eq!(r(2, false), None);
assert_eq!(r(6, false), Some("more than two channels"));
}