Files
craft-libs/crates/audio/tests/codecs/opus/robustness.rs
861c5e172d craft-audio: Opus decoder (7/10) (#445)
* craft-audio: Opus decoder

FilmCraft's `filmcraft-opus` decoder as `craft_audio::opus` behind
`opus-decode`: SILK, hybrid and CELT, every frame size, PLC and FEC,
multistream (`Decoder`) and single stream (`StreamDecoder`), 8-48 kHz
output.

- `TrackDecoder` decodes Opus; mono and stereo without an `OpusHead`
  (RTP, MPEG-TS) get a default one. Lost packets use Opus's own
  concealment.
- The native read path takes Ogg Opus (pre-skip and end trimming per
  RFC 7845), and Opus in Matroska/WebM and MP4, where packet starts
  after a seek come from the packets' own lengths.

The RFC 8251 conformance test runs all 36 vectors.
`scripts/fetch-opus-vectors.sh` downloads them (SHA-256 checked) and
the CI test job caches them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-audio: review fixes, round 1 (#445)

- CELT concealment keeps its pitch in its own field. It reused postfilter_period_old, which the
  next good frame's post-filter cross-fades from.
- Matroska Opus seeks: a failed packet read no longer caches a wrong start; a one-byte packet
  is read as one byte.
- A malformed OpusHead in Ogg is reported as malformed instead of falling back.
- The libopus oracle tests skip through the testkit, so CRAFT_MEDIA_REQUIRE_ORACLES fails them.
- Decoder::reset documents that it re-arms pre-skip trimming.
- fetch-opus-vectors.sh retries its size probe.
- CI: the vectors are fetched after ffmpeg is installed, and apt's timeout runs as root so a
  stalled apt-get is killed and frees the dpkg lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* craft-audio: review fixes, round 2 (#445)

- CELT: the noise-based concealment sets skip_plc, as libopus does, so
  pitch-based concealment waits for two good frames in a row instead of
  extending history that holds noise.
- SILK: decode_only_middle is per call, as in libopus. A lost or mono
  frame no longer reuses the mid-only flag from an earlier frame.
- A damaged Opus packet is concealed at its own length (from its TOC and
  framing) rather than the previous packet's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
2026-11-11 07:36:57 -05:00

168 lines
5.9 KiB
Rust

//! Garbage, truncated and bit-flipped packets must never panic; concealment must produce
//! well-formed output.
use std::path::PathBuf;
use craft_audio::opus::{Decoder, OpusHead, StreamDecoder};
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u32 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
(self.0 >> 16) as u32
}
}
fn check_finite(out: &[f32]) {
assert!(out.iter().all(|v| v.is_finite()), "non-finite output");
}
#[test]
fn random_garbage_never_panics() {
let mut rng = Rng(0x1234_5678_9abc_def0);
for &rate in &[48000u32, 24000, 16000, 12000, 8000] {
for channels in 1..=2 {
let mut dec = StreamDecoder::new(rate, channels).unwrap();
let mut out = Vec::new();
for i in 0..3000 {
let len = (rng.next() % 400) as usize;
let mut pkt: Vec<u8> = (0..len).map(|_| rng.next() as u8).collect();
if let Some(t) = pkt.first_mut() {
// Cycle through every TOC configuration.
*t = ((i % 32) as u8) << 3 | (*t & 7);
}
out.clear();
let _ = dec.decode_interleaved(Some(&pkt), &mut out);
check_finite(&out);
if i % 17 == 0 {
out.clear();
let _ = dec.decode_interleaved(None, &mut out);
check_finite(&out);
}
}
}
}
}
#[test]
fn multistream_garbage_never_panics() {
let mut h = b"OpusHead".to_vec();
h.extend([1, 6, 0x38, 0x01, 0x80, 0xBB, 0, 0, 0, 0, 1, 4, 2, 0, 4, 1, 2, 3, 5]);
let mut dec = Decoder::new(&h).unwrap();
let mut rng = Rng(42);
for _ in 0..2000 {
let len = (rng.next() % 600) as usize;
let pkt: Vec<u8> = (0..len).map(|_| rng.next() as u8).collect();
if let Ok(out) = dec.decode(Some(&pkt)) {
assert_eq!(out.len(), 6);
for c in &out {
check_finite(c);
}
}
}
let out = dec.decode(None).unwrap();
assert_eq!(out.len(), 6);
}
#[test]
fn concealment_before_and_after_packets() {
let mut dec = Decoder::from_head(OpusHead::simple(2).unwrap(), 48000).unwrap();
// Loss before any packet: silence of a default duration.
let out = dec.decode(None).unwrap();
assert_eq!(out.len(), 2);
assert!(out[0].iter().all(|&v| v == 0.0));
}
/// The RFC 8251 vectors, or `None` after printing `SKIPPED` (a failure with
/// `CRAFT_MEDIA_REQUIRE_ORACLES=1`). `scripts/fetch-opus-vectors.sh` fetches them.
fn vectors_dir() -> Option<PathBuf> {
let d = crate::common::fixture_subdir("opus").join("vectors");
if d.join("testvector01.bit").exists() {
return Some(d);
}
crate::common::skip(&format!("the Opus RFC 8251 vectors are not in {} (scripts/fetch-opus-vectors.sh fetches them)", d.display()))
}
/// Real packets (from the conformance vectors) truncated and bit-flipped, interleaved with losses.
#[test]
fn corrupted_real_packets() {
let Some(dir) = vectors_dir() else {
return;
};
let mut rng = Rng(7);
for v in [1, 2, 5, 12] {
let data = std::fs::read(dir.join(format!("testvector{v:02}.bit"))).unwrap();
let mut dec = StreamDecoder::new(48000, 2).unwrap();
let mut out = Vec::new();
let mut p = 0;
let mut n = 0;
while p + 8 <= data.len() && n < 1500 {
let len = u32::from_be_bytes(data[p..p + 4].try_into().unwrap()) as usize;
p += 8;
let mut pkt = data[p..(p + len).min(data.len())].to_vec();
p += len;
n += 1;
match rng.next() % 4 {
0 => {
let keep = (rng.next() as usize) % (pkt.len() + 1);
pkt.truncate(keep);
}
1 if !pkt.is_empty() => {
for _ in 0..3 {
let i = rng.next() as usize % pkt.len();
pkt[i] ^= 1 << (rng.next() % 8);
}
}
2 => pkt.clear(),
_ => {}
}
out.clear();
let _ = dec.decode_interleaved(if pkt.is_empty() { None } else { Some(&pkt) }, &mut out);
check_finite(&out);
}
}
}
/// An Ogg page holding `packet` whole (RFC 3533), with its CRC.
fn ogg_page(packet: &[u8], flags: u8, granule: u64, seq: u32) -> Vec<u8> {
let mut p = b"OggS\0".to_vec();
p.push(flags);
p.extend_from_slice(&granule.to_le_bytes());
p.extend_from_slice(&1u32.to_le_bytes());
p.extend_from_slice(&seq.to_le_bytes());
p.extend_from_slice(&[0; 4]);
let mut lacing = vec![255u8; packet.len() / 255];
lacing.push((packet.len() % 255) as u8);
p.push(lacing.len() as u8);
p.extend_from_slice(&lacing);
p.extend_from_slice(packet);
let mut crc = 0u32;
for &b in &p {
crc ^= u32::from(b) << 24;
for _ in 0..8 {
crc = if crc & 0x8000_0000 != 0 { (crc << 1) ^ 0x04C1_1DB7 } else { crc << 1 };
}
}
p[22..26].copy_from_slice(&crc.to_le_bytes());
p
}
#[test]
fn ogg_with_a_malformed_opus_head_reports_it() {
// Mapping family 0 with three channels: RFC 7845 §5.1.1 allows at most two.
let mut head = b"OpusHead\x01\x03".to_vec();
head.extend_from_slice(&312u16.to_le_bytes());
head.extend_from_slice(&48_000u32.to_le_bytes());
head.extend_from_slice(&[0, 0, 0]);
let mut file = ogg_page(&head, 2, 0, 0);
file.extend(ogg_page(b"OpusTags\0\0\0\0\0\0\0\0", 0, 0, 1));
// One 20 ms silent CELT frame.
file.extend(ogg_page(&[0xF8, 0xFF, 0xFE], 4, 960, 2));
match craft_audio::decode(&file, Some("opus")) {
Err(craft_audio::AudioError::Malformed(m)) => assert!(m.contains("family 0"), "{m}"),
other => panic!("expected a malformed-header error, got {:?}", other.map(|(info, _)| info)),
}
}