mod common;
use common::*;
use opus_pure::{
Application, Bandwidth, Error, OpusDecoder, RateControl, Repacketizer, Signal, packet,
repacketizer,
};
fn encode_frames(
rate: i32,
channels: usize,
app: Application,
bitrate: i32,
count: usize,
) -> Vec<Vec<u8>> {
encode_frames_of(rate, channels, app, bitrate, count, (rate / 50) as usize)
}
fn encode_frames_of(
rate: i32,
channels: usize,
app: Application,
bitrate: i32,
count: usize,
frame: usize,
) -> Vec<Vec<u8>> {
let mono = music_like(rate, frame * count);
let pcm = if channels == 2 {
interleave(&[mono.clone(), mono])
} else {
mono
};
Codec::new(rate, channels, app)
.frame_samples(frame)
.bitrate(bitrate)
.encode_all(&pcm)
}
#[test]
fn single_frame_packets_use_code_0() {
for &(rate, channels, app) in &[
(48_000i32, 1usize, Application::Audio),
(48_000, 2, Application::Audio),
(16_000, 1, Application::Voip),
] {
for pkt in encode_frames(rate, channels, app, 64_000, 10) {
assert_eq!(pkt[0] & 0x03, 0, "expected frame-packing code 0");
assert_eq!(packet_is_stereo(&pkt), channels == 2);
assert_eq!(packet::frame_count(&pkt).unwrap(), 1);
}
}
}
#[test]
fn toc_duration_matches_the_encoder_frame_size() {
for &rate in &[8_000i32, 12_000, 16_000, 24_000, 48_000] {
for &tenth_ms in &[25i32, 50, 100, 200, 400, 600, 800, 1000, 1200] {
let frame = (rate as i64 * tenth_ms as i64 / 10_000) as usize;
let pcm = music_like(rate, frame * 4);
let packets = Codec::new(rate, 1, Application::Audio)
.frame_samples(frame)
.bitrate(48_000)
.try_encode_all(&pcm)
.unwrap_or_else(|e| panic!("{rate} Hz / {frame} samples rejected: {e:?}"));
let per_frame = packet::samples_per_frame(&packets[0], rate).unwrap();
let frames = packet::frame_count(&packets[0]).unwrap();
assert_eq!(
frames * per_frame,
frame,
"{rate} Hz / {frame} samples: TOC declares {frames} x {per_frame}"
);
}
}
}
#[test]
fn repacketizer_combines_and_round_trips() {
let (rate, frame) = (48_000i32, 960usize);
let packets = encode_frames(rate, 1, Application::Audio, 64_000, 6);
for group in [2usize, 3] {
let mut rp = Repacketizer::new();
for pkt in packets.iter().take(group) {
rp.cat(pkt).expect("packets share a configuration");
}
let combined = rp.out_range(0, group).expect("repacketize");
assert_eq!(packet::frame_count(&combined).unwrap(), group);
assert_eq!(
combined[0] >> 3,
packets[0][0] >> 3,
"repacketizing must preserve the configuration"
);
let mut dec = OpusDecoder::new(rate, 1).unwrap();
let mut out = vec![0.0f32; frame * group];
let n = dec.decode(&combined, frame * group, &mut out).unwrap();
assert_eq!(n, frame * group);
assert!(out.iter().all(|s| s.is_finite()));
}
}
#[test]
fn repacketizer_stops_at_two_sixty_millisecond_frames() {
let (rate, frame) = (48_000i32, 2880usize);
let mono = music_like(rate, frame * 3);
let cases: [(&str, Codec, usize); 2] = [
(
"silk, one 60 ms frame",
Codec::new(rate, 1, Application::Voip)
.signal_type(Signal::Voice)
.bandwidth(Bandwidth::Wideband)
.bitrate(48_000)
.frame_samples(frame),
1,
),
(
"celt, three 20 ms frames",
Codec::new(rate, 1, Application::Audio)
.bitrate(64_000)
.frame_samples(frame),
3,
),
];
for (label, mut codec, per_packet) in cases {
let packets = codec.encode_all(&mono);
for pkt in &packets {
assert_eq!(packet_samples_48k(pkt), 2880, "{label}: not a 60 ms packet");
assert_eq!(
packet::frame_count(pkt).unwrap(),
per_packet,
"{label}: unexpected framing"
);
}
let mut rp = Repacketizer::new();
rp.cat(&packets[0]).expect("first 60 ms packet");
rp.cat(&packets[1])
.unwrap_or_else(|e| panic!("{label}: two 60 ms packets are exactly 120 ms: {e:?}"));
let combined = rp
.out_range(0, 2 * per_packet)
.unwrap_or_else(|e| panic!("{label}: repacketize 2 x 60 ms: {e:?}"));
assert_eq!(packet::frame_count(&combined).unwrap(), 2 * per_packet);
assert_eq!(packet_samples_48k(&combined), 5760, "{label}");
let mut dec = OpusDecoder::new(rate, 1).unwrap();
let mut out = vec![0.0f32; frame * 2];
assert_eq!(
dec.decode(&combined, frame * 2, &mut out).unwrap(),
frame * 2,
"{label}"
);
assert!(out.iter().all(|s| s.is_finite()), "{label}");
assert!(
rp.cat(&packets[2]).is_err(),
"{label}: the repacketizer accepted 180 ms in one packet"
);
}
}
#[test]
fn repacketizer_rejects_mixed_configurations() {
let a = encode_frames(48_000, 1, Application::Audio, 64_000, 1);
let b = encode_frames(16_000, 1, Application::Voip, 24_000, 1);
assert_ne!(
a[0][0] >> 3,
b[0][0] >> 3,
"test needs two different configurations"
);
let mut rp = Repacketizer::new();
rp.cat(&a[0]).unwrap();
assert!(
rp.cat(&b[0]).is_err(),
"mixing configurations must be refused"
);
}
#[test]
fn padding_is_transparent() {
let packets = encode_frames(48_000, 1, Application::Audio, 64_000, 4);
for pkt in &packets {
for extra in [1usize, 5, 64, 300] {
let mut padded = pkt.clone();
repacketizer::pad_packet(&mut padded, pkt.len() + extra).unwrap();
assert_eq!(padded.len(), pkt.len() + extra);
let unpadded = repacketizer::unpad_packet(&padded).unwrap();
assert_eq!(
&unpadded, pkt,
"unpadding did not recover the original packet"
);
let mut a = OpusDecoder::new(48_000, 1).unwrap();
let mut b = OpusDecoder::new(48_000, 1).unwrap();
let (mut oa, mut ob) = (vec![0.0f32; 960], vec![0.0f32; 960]);
a.decode(pkt, 960, &mut oa).unwrap();
b.decode(&padded, 960, &mut ob).unwrap();
assert_eq!(oa, ob, "padding changed the decoded audio");
}
}
}
#[test]
fn oversized_packet_duration_is_refused() {
let packets = encode_frames(48_000, 1, Application::Audio, 64_000, 3);
let mut rp = Repacketizer::new();
for p in &packets {
rp.cat(p).unwrap();
}
let combined = rp.out_range(0, 3).unwrap();
let mut dec = OpusDecoder::new(48_000, 1).unwrap();
let mut too_small = vec![0.0f32; 960]; assert!(matches!(
dec.decode(&combined, 960, &mut too_small),
Err(Error::BufferTooSmall { .. })
));
}
#[test]
fn malformed_packing_is_rejected() {
let mut dec = OpusDecoder::new(48_000, 1).unwrap();
let mut out = vec![0.0f32; 960 * 6];
assert!(matches!(
dec.decode(&[0x83, 0x00], 960 * 6, &mut out),
Err(Error::InvalidPacket(_))
));
assert!(matches!(
dec.decode(&[0x83, 0x30, 1], 960 * 6, &mut out),
Err(Error::InvalidPacket(_))
));
assert!(matches!(
dec.decode(&[0x81, 0x00], 960 * 6, &mut out),
Err(Error::InvalidPacket(_))
));
assert!(matches!(
dec.decode(&[0x82], 960 * 6, &mut out),
Err(Error::InvalidPacket(_))
));
}
#[test]
fn code_1_with_no_payload_is_two_dtx_frames() {
let mut dec = OpusDecoder::new(48_000, 1).unwrap();
let mut out = vec![0.0f32; 960 * 6];
let n = dec.decode(&[0x81], 960 * 6, &mut out).unwrap();
assert_eq!(n, 2 * 120);
assert!(out[..n].iter().all(|s| s.is_finite()));
}
#[test]
fn empty_packet_triggers_packet_loss_concealment() {
for &(rate, app, bitrate) in &[
(48_000i32, Application::Audio, 96_000i32),
(24_000, Application::Voip, 32_000),
(16_000, Application::Voip, 24_000),
(8_000, Application::Voip, 16_000),
] {
let frame = (rate / 50) as usize;
let packets = encode_frames(rate, 1, app, bitrate, 12);
let mut dec = OpusDecoder::new(rate, 1).unwrap();
let mut last = vec![0.0f32; frame];
for pkt in &packets {
dec.decode(pkt, frame, &mut last).unwrap();
}
let good = energy(&last);
let mut prev = good;
for lost in 0..5 {
let mut concealed = vec![0.0f32; frame];
let n = dec.decode(&[], frame, &mut concealed).expect("PLC");
assert_eq!(n, frame, "{rate} Hz: concealment must fill the whole frame");
assert!(
concealed.iter().all(|s| s.is_finite()),
"{rate} Hz: non-finite sample in concealed frame {lost}"
);
let e = energy(&concealed);
assert!(
e <= prev * 1.2,
"{rate} Hz: concealed frame {lost} is {:.2}x the frame before it",
e / prev.max(1e-20)
);
if lost == 0 {
assert!(
e >= good * 0.05,
"{rate} Hz: first concealed frame is {:.4} of the last good \
frame — concealment produced silence, not audio",
e / good.max(1e-20)
);
}
prev = e;
}
let mut recovered = vec![0.0f32; frame];
dec.decode(&packets[0], frame, &mut recovered).unwrap();
assert!(recovered.iter().all(|s| s.is_finite()));
}
}
#[test]
fn forced_bandwidth_is_capped_by_the_sampling_rate() {
let forced = [
Bandwidth::Narrowband,
Bandwidth::Mediumband,
Bandwidth::Wideband,
Bandwidth::Superwideband,
Bandwidth::Fullband,
];
for &rate in &[8_000i32, 12_000, 16_000, 24_000, 48_000] {
let nyquist = rate / 2;
for bw in forced {
let frame = (rate / 50) as usize;
let src = music_like(rate, frame * 12);
let packets = Codec::new(rate, 1, Application::Audio)
.bitrate(48_000)
.bandwidth(bw)
.encode_all(&src);
for pkt in &packets {
let coded = packet_bandwidth_hz(pkt);
if coded <= nyquist {
continue;
}
assert!(
packet_mode(pkt) == "celt" && bw != Bandwidth::Narrowband,
"{rate} Hz forcing {bw:?}: coded {coded} Hz, above the \
{nyquist} Hz the input can carry, and this is not the CELT \
mediumband widening"
);
assert_eq!(
coded, 8_000,
"{rate} Hz forcing {bw:?}: CELT has no mediumband, so the \
clamped request must widen to wideband and no further"
);
}
}
}
}
#[test]
fn no_frame_exceeds_the_1275_byte_limit() {
let mut oversized = Vec::new();
for &rate in &[8_000i32, 12_000, 16_000, 24_000, 48_000] {
for &tenth_ms in &[25i32, 50, 100, 200, 400, 600] {
let frame = (rate as i64 * tenth_ms as i64 / 10_000) as usize;
for &app in &[Application::Voip, Application::Audio] {
for &bitrate in &[64_000i32, 128_000, 192_000, 256_000, 510_000] {
for &channels in &[1usize, 2] {
for &cbr in &[false, true] {
let mono = music_like(rate, frame * 8);
let pcm = if channels == 2 {
interleave(&[mono.clone(), speech_like(rate, frame * 8)])
} else {
mono
};
let mut c = Codec::new(rate, channels, app)
.frame_samples(frame)
.bitrate(bitrate);
c.enc.rate_control = if cbr {
RateControl::Cbr
} else {
RateControl::ConstrainedVbr
};
let Ok(packets) = c.try_encode_all(&pcm) else {
continue;
};
let label = format!(
"{rate}Hz/{}ms/{app:?}/{bitrate}/{channels}ch/{}",
tenth_ms as f32 / 10.0,
if cbr { "cbr" } else { "vbr" }
);
for pkt in &packets {
let mut rp = repacketizer::Repacketizer::new();
match rp.cat(pkt) {
Ok(()) => {
for i in 0..rp.nb_frames() {
let len = match rp.out_range(i, i + 1) {
Ok(f) => f.len() - 1,
Err(e) => {
oversized.push(format!(
"{label}: frame {i} unreadable: {e:?}"
));
continue;
}
};
if len > 1275 {
oversized
.push(format!("{label}: frame of {len} bytes"));
}
}
}
Err(e) => oversized.push(format!(
"{label}: {}-byte packet rejected: {e:?}",
pkt.len()
)),
}
}
}
}
}
}
}
}
assert!(
oversized.is_empty(),
"{} configurations emitted a packet libopus would refuse:\n{}",
oversized.len(),
oversized.join("\n")
);
}