use std::ffi::c_int;
use audiopus::Channels as OpusChannels;
use audiopus::SampleRate;
use audiopus::coder::Decoder as OpusDecoderInner;
use audiopus::ffi;
use crate::audio::{AudioCodec, AudioEncoder, AudioEncoderConfig, AudioFrame};
use super::{OpusEncoder, build_dops, surround_mapping_family_1};
fn config_stereo_48k() -> AudioEncoderConfig {
AudioEncoderConfig {
codec: AudioCodec::Opus,
sample_rate: 48_000,
channels: 2,
bitrate: 96_000,
}
}
fn config_mono_48k() -> AudioEncoderConfig {
AudioEncoderConfig {
codec: AudioCodec::Opus,
sample_rate: 48_000,
channels: 1,
bitrate: 64_000,
}
}
fn config_multi_48k(channels: u8) -> AudioEncoderConfig {
AudioEncoderConfig {
codec: AudioCodec::Opus,
sample_rate: 48_000,
channels,
bitrate: 0, }
}
#[test]
fn opus_encoder_constructs_for_mono_48k_with_1_channel_dops() {
let enc = OpusEncoder::new(config_mono_48k()).expect("constructs");
assert_eq!(enc.channels, 1);
assert!(enc.resampler.is_none());
assert_eq!(enc.extra_data[1], 1);
}
#[test]
fn opus_encoder_uses_default_bitrate_when_caller_passes_zero() {
let mut cfg = config_stereo_48k();
cfg.bitrate = 0;
let _enc = OpusEncoder::new(cfg).expect("constructs with bitrate=0");
}
fn config_stereo_44100() -> AudioEncoderConfig {
AudioEncoderConfig {
codec: AudioCodec::Opus,
sample_rate: 44_100,
channels: 2,
bitrate: 96_000,
}
}
fn make_silence(channels: u8, frames: usize, sample_rate: u32) -> AudioFrame {
AudioFrame {
samples: vec![0.0f32; frames * channels as usize],
sample_rate,
channels,
pts: 0,
}
}
fn make_sine_1k(channels: u8, frames: usize, sample_rate: u32, amp: f32) -> AudioFrame {
let mut samples = Vec::with_capacity(frames * channels as usize);
let two_pi = std::f32::consts::PI * 2.0;
let freq = 1000.0f32;
for i in 0..frames {
let t = i as f32 / sample_rate as f32;
let v = (two_pi * freq * t).sin() * amp;
for _ in 0..channels {
samples.push(v);
}
}
AudioFrame {
samples,
sample_rate,
channels,
pts: 0,
}
}
#[test]
fn opus_encoder_constructs_for_stereo_48k() {
let enc = OpusEncoder::new(config_stereo_48k()).expect("constructs");
assert_eq!(enc.channels, 2);
assert_eq!(enc.in_rate, 48000);
assert!(enc.resampler.is_none(), "no resampler at native rate");
assert_eq!(enc.extra_data.len(), 11, "dOps body must be 11 bytes");
assert_eq!(enc.extra_data[0], 0);
assert_eq!(enc.extra_data[1], 2);
assert_eq!(enc.extra_data[10], 0);
}
#[test]
fn opus_encoder_resamples_44100_to_48k_internally() {
let enc = OpusEncoder::new(config_stereo_44100()).expect("constructs");
assert!(enc.resampler.is_some(), "resampler engaged at 44.1k input");
let r = enc.resampler.as_ref().unwrap();
assert_eq!(r.in_rate(), 44100);
assert_eq!(r.out_rate(), 48000);
}
#[test]
fn opus_encoder_rejects_zero_channels() {
let mut bad = config_stereo_48k();
bad.channels = 0;
assert!(matches!(
OpusEncoder::new(bad),
Err(crate::audio::AudioError::Unsupported(_))
));
}
#[test]
fn opus_encoder_rejects_nine_channels() {
let mut bad9 = config_stereo_48k();
bad9.channels = 9;
assert!(matches!(
OpusEncoder::new(bad9),
Err(crate::audio::AudioError::Unsupported(_))
));
}
#[test]
fn opus_encoder_rejects_nine_channel_frame_at_runtime() {
let mut enc = OpusEncoder::new(config_stereo_48k()).expect("constructs");
let bad_frame = AudioFrame {
samples: vec![0.0; 960 * 9],
sample_rate: 48000,
channels: 9,
pts: 0,
};
let r = enc.encode(&bad_frame);
assert!(
matches!(r, Err(crate::audio::AudioError::Unsupported(_))),
"9-channel frame should be Unsupported, got {:?}",
r
);
}
#[test]
fn opus_pre_skip_in_48khz_ticks_is_nonzero() {
let enc = OpusEncoder::new(config_stereo_48k()).expect("constructs");
assert!(
enc.pre_skip() > 0,
"Opus encoder lookahead should be positive (libopus convention)"
);
assert!(
enc.pre_skip() < 2000,
"lookahead is bounded — typically <600 samples at 48 kHz"
);
}
#[test]
fn opus_dops_carries_correct_pre_skip_and_input_sample_rate_le() {
let enc = OpusEncoder::new(config_stereo_44100()).expect("constructs");
let d = enc.extra_data();
let ps = u16::from_le_bytes([d[2], d[3]]);
assert_eq!(ps, enc.pre_skip(), "dOps PreSkip matches encoder lookahead");
let isr = u32::from_le_bytes([d[4], d[5], d[6], d[7]]);
assert_eq!(
isr, 44100,
"dOps InputSampleRate is the source rate, not 48k"
);
let og = i16::from_le_bytes([d[8], d[9]]);
assert_eq!(og, 0);
}
#[test]
fn opus_encode_20ms_silence_produces_one_packet() {
let mut enc = OpusEncoder::new(config_stereo_48k()).expect("constructs");
let frame = make_silence(2, 960, 48_000);
let pkts = enc.encode(&frame).expect("encode");
assert_eq!(pkts.len(), 1, "exactly one Opus packet for one 20ms frame");
let pkt = &pkts[0];
assert!(!pkt.data.is_empty(), "packet should have bytes");
assert!(
pkt.data.len() < 200,
"silence packet at 96 kbps should be small, got {} bytes",
pkt.data.len()
);
assert_eq!(pkt.duration, 960, "20ms = 960 ticks at 48k");
}
#[test]
fn opus_encode_one_second_of_sine_produces_packets_with_reasonable_bitrate() {
let mut enc = OpusEncoder::new(config_stereo_48k()).expect("constructs");
let mut total_bytes = 0usize;
let mut total_packets = 0usize;
for i in 0..50 {
let mut frame = make_sine_1k(2, 960, 48_000, 0.3);
frame.pts = i * 20_000;
let pkts = enc.encode(&frame).expect("encode");
for p in &pkts {
total_bytes += p.data.len();
total_packets += 1;
}
}
let pkts_flush = enc.flush().expect("flush");
for p in &pkts_flush {
total_bytes += p.data.len();
total_packets += 1;
}
assert!(
total_packets >= 49 && total_packets <= 51,
"expected ~50 packets for 1 s of audio, got {total_packets}"
);
let observed_bps = (total_bytes as u64 * 8) as i64;
assert!(
observed_bps > 30_000 && observed_bps < 200_000,
"1s of 1kHz sine at 96 kbps should yield 30-200 kbps actual, got {observed_bps} bps ({total_bytes} bytes)"
);
}
#[test]
fn opus_pts_steps_by_20ms_per_packet() {
let mut enc = OpusEncoder::new(config_stereo_48k()).expect("constructs");
let frame_a = make_silence(2, 960, 48_000);
let mut frame_b = make_silence(2, 960, 48_000);
frame_b.pts = 20_000;
let pkts_a = enc.encode(&frame_a).expect("a");
let pkts_b = enc.encode(&frame_b).expect("b");
assert_eq!(pkts_a.len(), 1);
assert_eq!(pkts_b.len(), 1);
let dt = pkts_b[0].pts - pkts_a[0].pts;
assert_eq!(
dt, 20_000,
"PTS should step by 20_000 us per Opus packet (20 ms frame)"
);
}
#[test]
fn opus_round_trip_sine_wave_quality_is_acceptable() {
let mut enc = OpusEncoder::new(config_stereo_48k()).expect("constructs");
let frames_per_chunk = 960;
let n_chunks = 25; let total_frames = frames_per_chunk * n_chunks;
let mut all_samples = Vec::with_capacity(total_frames * 2);
let two_pi = std::f32::consts::PI * 2.0;
let freq = 1000.0f32;
for i in 0..total_frames {
let t = i as f32 / 48_000.0;
let v = (two_pi * freq * t).sin() * 0.5;
all_samples.push(v);
all_samples.push(v);
}
let mut packets = Vec::new();
for c in 0..n_chunks {
let chunk_samples =
all_samples[c * frames_per_chunk * 2..(c + 1) * frames_per_chunk * 2].to_vec();
let frame = AudioFrame {
samples: chunk_samples,
sample_rate: 48_000,
channels: 2,
pts: (c as i64) * 20_000,
};
packets.extend(enc.encode(&frame).expect("encode"));
}
packets.extend(enc.flush().expect("flush"));
assert!(!packets.is_empty(), "encode must produce packets");
let mut dec =
OpusDecoderInner::new(SampleRate::Hz48000, OpusChannels::Stereo).expect("dec");
let mut decoded = Vec::with_capacity(total_frames * 2);
let mut tmp = vec![0.0f32; frames_per_chunk * 2];
for p in &packets {
let pkt = audiopus::packet::Packet::try_from(p.data.as_slice()).expect("pkt");
let sig = audiopus::MutSignals::try_from(tmp.as_mut_slice()).expect("sig");
let n = dec
.decode_float(Some(pkt), sig, false)
.expect("decode_float");
decoded.extend_from_slice(&tmp[..n * 2]);
}
assert!(
decoded.len() >= (total_frames - 100) * 2,
"decoded length {} should approximate input length {}",
decoded.len(),
total_frames * 2
);
let pre_skip = enc.pre_skip() as usize;
let cmp_start = pre_skip + 480; let cmp_end = (decoded.len() / 2).min(total_frames - 100);
if cmp_end <= cmp_start {
panic!(
"round trip too short: cmp_start={cmp_start}, cmp_end={cmp_end}, decoded len/2={}",
decoded.len() / 2
);
}
let mut sum_sq_err = 0.0f64;
let mut sum_sq_sig = 0.0f64;
let mut n = 0usize;
for i in cmp_start..cmp_end {
let in_idx = i - pre_skip;
let l_in = all_samples[in_idx * 2];
let r_in = all_samples[in_idx * 2 + 1];
let l_out = decoded[i * 2];
let r_out = decoded[i * 2 + 1];
sum_sq_err += ((l_in - l_out) as f64).powi(2);
sum_sq_err += ((r_in - r_out) as f64).powi(2);
sum_sq_sig += (l_in as f64).powi(2);
sum_sq_sig += (r_in as f64).powi(2);
n += 2;
}
let rms_err = (sum_sq_err / n as f64).sqrt();
let rms_sig = (sum_sq_sig / n as f64).sqrt();
let snr_db = 20.0 * (rms_sig / rms_err.max(1e-12)).log10();
assert!(
snr_db > 15.0,
"round-trip SNR {snr_db:.2} dB too low — Opus quality regression?"
);
println!("opus_round_trip SNR = {snr_db:.2} dB, rms_err = {rms_err:.4}");
}
#[test]
fn dops_layout_matches_rfc_7845_for_mono_and_stereo() {
let d_mono = build_dops(1, 312, 48_000, None);
assert_eq!(d_mono.len(), 11);
assert_eq!(d_mono[0], 0); assert_eq!(d_mono[1], 1); assert_eq!(u16::from_le_bytes([d_mono[2], d_mono[3]]), 312); assert_eq!(
u32::from_le_bytes([d_mono[4], d_mono[5], d_mono[6], d_mono[7]]),
48000
); assert_eq!(i16::from_le_bytes([d_mono[8], d_mono[9]]), 0); assert_eq!(d_mono[10], 0);
let d_stereo = build_dops(2, 400, 44_100, None);
assert_eq!(d_stereo.len(), 11);
assert_eq!(d_stereo[1], 2);
assert_eq!(u16::from_le_bytes([d_stereo[2], d_stereo[3]]), 400);
assert_eq!(
u32::from_le_bytes([d_stereo[4], d_stereo[5], d_stereo[6], d_stereo[7]]),
44100
);
}
#[test]
fn surround_mapping_family_1_matches_rfc_7845_5_1_1_2() {
assert_eq!(
surround_mapping_family_1(3).unwrap(),
(2, 1, &[0, 2, 1][..])
);
assert_eq!(
surround_mapping_family_1(4).unwrap(),
(2, 2, &[0, 1, 2, 3][..])
);
assert_eq!(
surround_mapping_family_1(5).unwrap(),
(3, 2, &[0, 4, 1, 2, 3][..])
);
assert_eq!(
surround_mapping_family_1(6).unwrap(),
(4, 2, &[0, 4, 1, 2, 3, 5][..])
);
assert_eq!(
surround_mapping_family_1(7).unwrap(),
(4, 3, &[0, 4, 1, 2, 3, 5, 6][..])
);
assert_eq!(
surround_mapping_family_1(8).unwrap(),
(5, 3, &[0, 6, 1, 2, 3, 4, 5, 7][..])
);
assert!(surround_mapping_family_1(0).is_err());
assert!(surround_mapping_family_1(1).is_err()); assert!(surround_mapping_family_1(2).is_err());
assert!(surround_mapping_family_1(9).is_err());
}
#[test]
fn opus_encoder_constructs_for_3_0_through_7_1_with_family_1_dops() {
for &ch in &[3u8, 4, 5, 6, 7, 8] {
let enc = OpusEncoder::new(config_multi_48k(ch))
.unwrap_or_else(|e| panic!("constructs for {ch}ch: {e:?}"));
assert_eq!(enc.channels, ch);
assert!(enc.resampler.is_none(), "no resampler at native rate");
let d = enc.extra_data();
let expected_len = 11 + 2 + ch as usize;
assert_eq!(
d.len(),
expected_len,
"dOps body for {ch}ch should be {expected_len} bytes (11 preamble + 2 stream/coupled + N mapping); got {}",
d.len()
);
assert_eq!(
d[0], 0,
"Version=0 (dOps box version, not Opus stream version)"
);
assert_eq!(d[1], ch, "OutputChannelCount");
assert_eq!(d[10], 1, "ChannelMappingFamily=1 for surround");
let (exp_streams, exp_coupled, exp_mapping) = surround_mapping_family_1(ch).unwrap();
assert_eq!(d[11], exp_streams, "StreamCount for {ch}ch");
assert_eq!(d[12], exp_coupled, "CoupledCount for {ch}ch");
assert_eq!(
&d[13..13 + ch as usize],
exp_mapping,
"ChannelMapping for {ch}ch"
);
}
}
#[test]
fn opus_encoder_dops_5_1_hex_layout() {
let enc = OpusEncoder::new(config_multi_48k(6)).expect("5.1 constructs");
let d = enc.extra_data();
assert_eq!(d.len(), 19, "5.1 dOps body = 11 + 2 + 6 = 19 bytes");
let hex: String = d.iter().map(|b| format!("{b:02x} ")).collect();
println!(
"5.1 dOps body hex (LE-encoded, 19 bytes): {}",
hex.trim_end()
);
assert_eq!(d[0], 0); assert_eq!(d[1], 6); let ps = u16::from_le_bytes([d[2], d[3]]);
assert!(ps > 0 && ps < 2000);
assert_eq!(
u32::from_le_bytes([d[4], d[5], d[6], d[7]]),
48_000,
"InputSampleRate=48000"
);
assert_eq!(i16::from_le_bytes([d[8], d[9]]), 0); assert_eq!(d[10], 1); assert_eq!(d[11], 4); assert_eq!(d[12], 2); assert_eq!(&d[13..19], &[0u8, 4, 1, 2, 3, 5][..]); }
#[test]
fn opus_5_1_encode_20ms_silence_produces_one_packet() {
let mut enc = OpusEncoder::new(config_multi_48k(6)).expect("5.1 constructs");
let frame = make_silence(6, 960, 48_000);
let pkts = enc.encode(&frame).expect("encode 5.1 silence");
assert_eq!(pkts.len(), 1, "exactly one Opus packet for one 20ms frame");
let pkt = &pkts[0];
assert!(!pkt.data.is_empty());
assert!(
pkt.data.len() < 600,
"5.1 silence packet should still be under ~600 bytes, got {} bytes",
pkt.data.len()
);
assert_eq!(pkt.duration, 960);
}
#[test]
fn opus_5_1_round_trip_per_channel_snr_is_acceptable() {
let freqs = [440.0f32, 523.25, 659.25, 80.0, 880.0, 987.77];
let chans: u8 = 6;
let frames_per_chunk = 960;
let n_chunks = 30; let total_frames = frames_per_chunk * n_chunks;
let amp = 0.4f32;
let mut all = vec![0.0f32; total_frames * chans as usize];
let two_pi = std::f32::consts::PI * 2.0;
for i in 0..total_frames {
let t = i as f32 / 48_000.0;
for ch in 0..chans as usize {
all[i * chans as usize + ch] = (two_pi * freqs[ch] * t).sin() * amp;
}
}
let mut enc = OpusEncoder::new(config_multi_48k(chans)).expect("encoder");
let mut packets = Vec::new();
for c in 0..n_chunks {
let frame = AudioFrame {
samples: all[c * frames_per_chunk * chans as usize
..(c + 1) * frames_per_chunk * chans as usize]
.to_vec(),
sample_rate: 48_000,
channels: chans,
pts: (c as i64) * 20_000,
};
packets.extend(enc.encode(&frame).expect("encode"));
}
packets.extend(enc.flush().expect("flush"));
assert!(!packets.is_empty(), "must produce packets");
let (streams, coupled, mapping) = surround_mapping_family_1(chans).unwrap();
let mut err: c_int = 0;
let dec_state = unsafe {
ffi::opus_multistream_decoder_create(
48_000,
chans as c_int,
streams as c_int,
coupled as c_int,
mapping.as_ptr(),
&mut err,
)
};
assert!(
!dec_state.is_null() && err == ffi::OPUS_OK,
"MS decoder create"
);
let mut decoded = Vec::with_capacity(total_frames * chans as usize);
let mut tmp = vec![0.0f32; frames_per_chunk * chans as usize];
for p in &packets {
let n = unsafe {
ffi::opus_multistream_decode_float(
dec_state,
p.data.as_ptr(),
p.data.len() as i32,
tmp.as_mut_ptr(),
frames_per_chunk as c_int,
0,
)
};
assert!(n > 0, "MS decode_float returned {n}");
decoded.extend_from_slice(&tmp[..(n as usize) * chans as usize]);
}
unsafe { ffi::opus_multistream_decoder_destroy(dec_state) };
let pre_skip = enc.pre_skip() as usize;
let cmp_start = pre_skip + 480;
let cmp_end = (decoded.len() / chans as usize).min(total_frames - 200);
assert!(cmp_end > cmp_start, "round trip too short");
let mut snrs = Vec::with_capacity(chans as usize);
for ch in 0..chans as usize {
let mut sum_sq_err = 0.0f64;
let mut sum_sq_sig = 0.0f64;
for i in cmp_start..cmp_end {
let in_idx = i - pre_skip;
let s_in = all[in_idx * chans as usize + ch];
let s_out = decoded[i * chans as usize + ch];
sum_sq_err += ((s_in - s_out) as f64).powi(2);
sum_sq_sig += (s_in as f64).powi(2);
}
let n = (cmp_end - cmp_start) as f64;
let rms_err = (sum_sq_err / n).sqrt();
let rms_sig = (sum_sq_sig / n).sqrt();
let snr_db = 20.0 * (rms_sig / rms_err.max(1e-12)).log10();
snrs.push(snr_db);
}
println!("5.1 per-channel SNR (dB):");
for (i, snr) in snrs.iter().enumerate() {
let label = ["FL", "FR", "C", "LFE", "BL", "BR"][i];
println!(" ch{i} ({label}): {snr:.2} dB");
}
for (i, snr) in snrs.iter().enumerate() {
assert!(
*snr > 5.0,
"ch{i} SNR {snr:.2} dB too low — multistream quality regression?"
);
}
}
#[test]
fn dops_layout_for_5_1_matches_family_1_spec() {
let (streams, coupled, mapping) = surround_mapping_family_1(6).unwrap();
let d = build_dops(6, 312, 48_000, Some((streams, coupled, mapping)));
assert_eq!(d.len(), 11 + 2 + 6, "5.1 dOps = 19 bytes");
assert_eq!(d[0], 0); assert_eq!(d[1], 6); assert_eq!(u16::from_le_bytes([d[2], d[3]]), 312); assert_eq!(u32::from_le_bytes([d[4], d[5], d[6], d[7]]), 48_000); assert_eq!(i16::from_le_bytes([d[8], d[9]]), 0); assert_eq!(d[10], 1); assert_eq!(d[11], 4); assert_eq!(d[12], 2); assert_eq!(&d[13..19], &[0u8, 4, 1, 2, 3, 5][..]);
}
#[test]
fn opus_5_1_resamples_44100_to_48k() {
let mut cfg = config_multi_48k(6);
cfg.sample_rate = 44_100;
let enc = OpusEncoder::new(cfg).expect("5.1 @ 44.1k constructs");
assert!(enc.resampler.is_some(), "resampler engaged for 6ch @ 44.1k");
let r = enc.resampler.as_ref().unwrap();
assert_eq!(r.in_rate(), 44_100);
assert_eq!(r.out_rate(), 48_000);
assert_eq!(r.channels(), 6);
}