use std::path::PathBuf;
use rff::codec::CodecParams;
use rff::core::{CodecId, Error, Frame, SampleFormat};
use rff::Engine;
fn testdata(name: &str) -> PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("testdata")
.join(name)
}
struct Decoded {
codec: CodecId,
channels: usize,
sample_rate: u32,
samples: Vec<f32>,
}
impl Decoded {
fn frames(&self) -> usize {
self.samples.len() / self.channels
}
fn channel(&self, c: usize) -> impl Iterator<Item = f64> + '_ {
self.samples
.iter()
.skip(c)
.step_by(self.channels)
.map(|&s| s as f64)
}
fn rms(&self, c: usize) -> f64 {
(self.channel(c).map(|s| s * s).sum::<f64>() / self.frames() as f64).sqrt()
}
}
fn decode(name: &str) -> Decoded {
decode_bytes(std::fs::read(testdata(name)).expect("fixture"))
}
fn damaged(name: &str, seq: u32) -> Vec<u8> {
let mut file = std::fs::read(testdata(name)).expect("fixture");
let mut at = 0;
while file[at..].starts_with(b"OggS") {
let nsegs = file[at + 26] as usize;
let body_at = at + 27 + nsegs;
let body: usize = file[at + 27..body_at].iter().map(|&l| l as usize).sum();
if u32::from_le_bytes(file[at + 18..at + 22].try_into().unwrap()) == seq {
file[body_at + body / 2] ^= 0x01;
return file;
}
at = body_at + body;
}
panic!("{name}: no page {seq}");
}
fn decode_bytes(file: Vec<u8>) -> Decoded {
let engine = Engine::new();
let mut dem = engine
.formats
.open_demuxer("ogg", Box::new(std::io::Cursor::new(file)))
.unwrap();
let streams = dem.read_header().unwrap();
let s = &streams[0];
assert_eq!(
s.sample_format,
Some(SampleFormat::F32),
"advertised format"
);
let mut dec = engine.codecs.find_decoder(s.codec_id).unwrap();
dec.configure(&CodecParams {
codec_id: s.codec_id,
sample_rate: s.sample_rate,
channels: s.channels,
sample_format: s.sample_format,
extradata: s.extradata.clone(),
..Default::default()
})
.unwrap();
let mut samples = Vec::new();
let mut drain = |dec: &mut Box<dyn rff::codec::Decoder>| loop {
match dec.receive_frame() {
Ok(Frame::Audio(af)) => {
assert_eq!(af.format, SampleFormat::F32);
assert_eq!(af.channels, s.channels);
assert_eq!(af.planes[0].len(), af.samples * af.channels as usize * 4);
samples.extend(
af.planes[0]
.chunks_exact(4)
.map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])),
);
}
Ok(_) => panic!("video frame from an audio decoder"),
Err(Error::Again) | Err(Error::Eof) => break,
Err(e) => panic!("decode: {e}"),
}
};
loop {
match dem.read_packet() {
Ok(p) => {
dec.send_packet(&p).unwrap();
drain(&mut dec);
}
Err(Error::Eof) => break,
Err(e) => panic!("demux: {e}"),
}
}
dec.flush();
drain(&mut dec);
Decoded {
codec: s.codec_id,
channels: s.channels as usize,
sample_rate: s.sample_rate,
samples,
}
}
fn assert_rms(d: &Decoded, want: &[f64]) {
assert_eq!(d.channels, want.len());
for (c, &w) in want.iter().enumerate() {
let got = d.rms(c);
assert!(
(got - w).abs() < 1e-6,
"ch{c} rms {got:.9} vs FFmpeg {w:.9}"
);
}
}
fn power_at(x: impl Iterator<Item = f64>, freq: f64, rate: f64) -> f64 {
let k = 2.0 * (2.0 * std::f64::consts::PI * freq / rate).cos();
let (mut s1, mut s2) = (0.0, 0.0);
for v in x {
let s0 = v + k * s1 - s2;
s2 = s1;
s1 = s0;
}
s1 * s1 + s2 * s2 - k * s1 * s2
}
#[test]
fn vorbis_multi_page_ends_at_the_final_granule() {
let d = decode("vorbis_stereo_43972.ogg");
assert_eq!(d.codec, CodecId::Vorbis);
assert_eq!(d.frames(), 43_972, "FFmpeg decodes 43,972 frames");
assert_rms(&d, &[0.088984951, 0.08902861]);
let off_grid = d
.samples
.iter()
.filter(|s| (**s * 32768.0).fract() != 0.0)
.count();
assert!(
off_grid > d.samples.len() / 2,
"{off_grid} off the s16 grid"
);
}
#[test]
fn vorbis_matches_lewtons_own_ogg_reader() {
let d = decode("vorbis_stereo_43972.ogg");
let file = std::fs::File::open(testdata("vorbis_stereo_43972.ogg")).unwrap();
let mut rdr = lewton::inside_ogg::OggStreamReader::new(file).unwrap();
let mut want = Vec::new();
while let Some(pcm) = rdr.read_dec_packet_generic::<Vec<Vec<f32>>>().unwrap() {
for i in 0..pcm.first().map_or(0, Vec::len) {
want.extend(pcm.iter().map(|ch| ch[i]));
}
}
assert_eq!(d.samples.len(), want.len(), "sample count");
let max_diff = d
.samples
.iter()
.zip(&want)
.map(|(a, b)| (a - b).abs())
.fold(0.0f32, f32::max);
assert!(max_diff <= 1e-6, "max abs diff vs lewton {max_diff:e}");
}
#[test]
fn vorbis_5_1_comes_out_in_ffmpeg_channel_order() {
let d = decode("vorbis_5_1_58368.ogg");
assert_eq!(d.frames(), 58_368);
assert_rms(
&d,
&[
0.088831679,
0.088941945,
0.089055486,
0.004386011,
0.08904442,
0.088951527,
],
);
let tones = [300.0, 400.0, 200.0, 600.0, 700.0];
for (c, want) in [(0, 300.0), (1, 400.0), (2, 200.0), (4, 600.0), (5, 700.0)] {
let loudest = tones
.iter()
.copied()
.max_by(|&a, &b| {
let pa = power_at(d.channel(c), a, d.sample_rate as f64);
let pb = power_at(d.channel(c), b, d.sample_rate as f64);
pa.total_cmp(&pb)
})
.unwrap();
assert_eq!(loudest, want, "channel {c}");
}
let quietest = (0..6).min_by(|&a, &b| d.rms(a).total_cmp(&d.rms(b)));
assert_eq!(quietest, Some(3));
}
#[test]
fn vorbis_single_page_matches_ffmpeg() {
let d = decode("vorbis_one_page.ogg");
assert_eq!(d.frames(), 13_102);
assert_rms(&d, &[0.08895936]);
}
#[test]
fn vorbis_end_trim_after_an_early_long_block_tail() {
let d = decode("vorbis_early_tail_2400.ogg");
assert_eq!(d.frames(), 2_400, "FFmpeg decodes 2,400 frames");
assert_rms(&d, &[0.177820907]);
}
#[test]
fn opus_pre_skip_and_end_trim() {
let d = decode("opus_stereo_96000.ogg");
assert_eq!(d.codec, CodecId::Opus);
assert_eq!(d.sample_rate, 48_000);
assert_eq!(d.frames(), 96_000);
assert_rms(&d, &[0.088061477, 0.08893406]);
}
#[test]
fn opus_odd_length_matches_ffmpeg() {
let d = decode("opus_stereo_59259.opus");
assert_eq!(d.frames(), 59_259);
assert_rms(&d, &[0.088196083, 0.088469157]);
}
#[test]
fn opus_single_page_matches_ffmpeg() {
let d = decode("opus_one_page.opus");
assert_eq!(d.frames(), 4_800); assert_rms(&d, &[0.088420073]);
}
#[test]
fn vorbis_damaged_page_is_dropped_like_ffmpeg() {
let d = decode_bytes(damaged("vorbis_stereo_43972.ogg", 5));
assert_eq!(d.frames(), 38_852, "FFmpeg decodes 38,852 frames");
assert_rms(&d, &[0.089206119, 0.08941548]);
let peak = d.samples.iter().fold(0f32, |m, s| m.max(s.abs()));
assert!((peak - 0.167_075_8).abs() < 1e-6, "peak {peak}");
}
#[test]
fn opus_damaged_first_audio_page() {
let d = decode_bytes(damaged("opus_stereo_96000.ogg", 2));
assert_eq!(d.frames(), 48_000);
assert_rms(&d, &[0.086382628, 0.086827187]);
}
#[test]
fn opus_damaged_last_full_page() {
let d = decode_bytes(damaged("opus_stereo_96000.ogg", 3));
assert_eq!(d.frames(), 48_648);
assert_rms(&d, &[0.087359089, 0.088309663]);
}