use std::{num::NonZeroI32, path::PathBuf};
use ffmpeg_next as ffmpeg;
use mediadecode::{Received, Sent, Timebase, decoder::AudioStreamDecoder};
use mediadecode_ffmpeg::{
FfmpegOwnedAudioStreamDecoder as FfmpegAudioStreamDecoder,
empty_owned_audio_frame as empty_audio_frame,
};
const FIXTURES: &[(&str, &str, u32, u8, u64)] = &[
("pcm_s16le", "02_pyannote_sample.wav", 16_000, 1, 480_000),
("pcm_s16le", "03_dual_speaker.wav", 16_000, 1, 960_000),
("pcm_s16le", "04_three_speaker.wav", 16_000, 1, 639_573),
("pcm_s16le", "05_four_speaker.wav", 16_000, 1, 960_000),
("pcm_s16le", "06_long_recording.wav", 16_000, 1, 15_643_627),
(
"pcm_s16le",
"07_yuhewei_dongbei_english.wav",
16_000,
1,
404_213,
),
(
"pcm_s16le",
"08_luyu_jinjing_freedom.wav",
16_000,
1,
22_675_308,
),
(
"pcm_s16le",
"09_mrbeast_dollar_date.wav",
16_000,
1,
16_671_744,
),
(
"pcm_s16le",
"10_mrbeast_clean_water.wav",
16_000,
1,
9_911_979,
),
(
"pcm_s16le",
"11_mrbeast_age_race.wav",
16_000,
1,
22_568_310,
),
("pcm_s16le", "12_mrbeast_schools.wav", 16_000, 1, 15_426_781),
(
"pcm_s16le",
"13_mrbeast_saved_animals.wav",
16_000,
1,
16_882_005,
),
(
"pcm_s16le",
"14_mrbeast_strongman_robot.wav",
16_000,
1,
17_648_640,
),
("pcm_f32le", "01_dialogue.wav", 16_000, 1, 3_631_361),
];
fn fixtures_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("workspace root")
.join("tests/fixtures/audio")
}
fn decode_clip(path: &std::path::Path, expected: (u32, u8, u64)) {
let (expected_sample_rate, expected_channels, expected_samples) = expected;
let mut input = ffmpeg::format::input(&path).expect("open input");
let stream = input
.streams()
.best(ffmpeg::media::Type::Audio)
.expect("audio stream");
let stream_index = stream.index();
let stream_tb = stream.time_base();
let time_base = Timebase::new(
stream_tb.numerator(),
NonZeroI32::new(stream_tb.denominator().max(1)).expect("non-zero den"),
);
let mut decoder = FfmpegAudioStreamDecoder::open(
stream.parameters(),
time_base,
mediadecode_ffmpeg::DecoderLimits::default(),
)
.expect("open audio decoder");
let mut frame = empty_audio_frame();
let mut total_samples: u64 = 0;
let mut frame_count: u64 = 0;
let mut observed_sample_rate: Option<u32> = None;
let mut observed_channels: Option<u8> = None;
for (s, av_packet) in input.packets() {
if s.index() != stream_index {
continue;
}
let Some(pkt) = mediadecode_ffmpeg::boundary::owned_audio_packet_from_ffmpeg_in(
&av_packet,
mediadecode::Timebase::default(),
mediadecode_ffmpeg::PacketLimits::default(),
)
.expect("a wrappable payload") else {
continue;
};
assert_eq!(
decoder.send_packet(&pkt).expect("audio send_packet"),
Sent::Accepted,
"these fixtures feed a session this loop has just drained",
);
while matches!(
decoder
.receive_frame(&mut frame)
.expect("audio receive_frame"),
Received::Frame
) {
total_samples = total_samples.saturating_add(frame.nb_samples() as u64);
frame_count = frame_count.saturating_add(1);
observed_sample_rate.get_or_insert(frame.sample_rate());
observed_channels.get_or_insert(frame.channel_count());
}
}
assert_eq!(decoder.send_eof().expect("send_eof"), Sent::Accepted);
loop {
match decoder
.receive_frame(&mut frame)
.expect("audio receive_frame")
{
Received::Frame => {
total_samples = total_samples.saturating_add(frame.nb_samples() as u64);
frame_count = frame_count.saturating_add(1);
}
Received::NeedsInput => panic!("a decoder at EOF asked for input"),
Received::Ended => break,
}
}
assert!(frame_count > 0, "no audio frames decoded for {path:?}");
assert_eq!(
observed_sample_rate,
Some(expected_sample_rate),
"sample rate drift on {path:?}",
);
assert_eq!(
observed_channels,
Some(expected_channels),
"channel count drift on {path:?}",
);
assert_eq!(
total_samples,
expected_samples,
"sample count drift on {} ({total_samples} got, {expected_samples} expected)",
path.file_name().unwrap_or_default().to_string_lossy(),
);
}
#[test]
fn decode_all_audio_fixtures() {
let root = fixtures_root();
if !root.exists() {
eprintln!(
"skip: {} not found — run `git submodule update --init --depth=1` \
to fetch the audio-fixtures submodule, then re-run this test.",
root.display()
);
return;
}
ffmpeg::init().expect("ffmpeg init");
for (codec_dir, name, sample_rate, channels, samples) in FIXTURES {
let path = root.join(codec_dir).join(name);
eprintln!("decoding {codec_dir}/{name}…");
decode_clip(&path, (*sample_rate, *channels, *samples));
}
eprintln!(
"decoded {} fixtures end-to-end through the trait surface",
FIXTURES.len(),
);
}
#[test]
fn the_audio_face_answers_the_same_vocabulary_without_a_park_refusal() {
let path = fixtures_root().join("pcm_s16le/02_pyannote_sample.wav");
if !path.exists() {
eprintln!("skipping: run `git submodule update --init` for {path:?}");
return;
}
ffmpeg::init().expect("ffmpeg init");
let mut input = ffmpeg::format::input(&path).expect("open input");
let stream = input
.streams()
.best(ffmpeg::media::Type::Audio)
.expect("audio stream");
let stream_index = stream.index();
let mut decoder = FfmpegAudioStreamDecoder::open(
stream.parameters(),
Timebase::default(),
mediadecode_ffmpeg::DecoderLimits::default(),
)
.expect("open audio decoder");
let mut packets = input
.packets()
.filter_map(|(s, p)| (s.index() == stream_index).then_some(p));
let mut take = || {
let av = packets.next().expect("the fixture has packets");
mediadecode_ffmpeg::boundary::owned_audio_packet_from_ffmpeg_in(
&av,
mediadecode::Timebase::default(),
mediadecode_ffmpeg::PacketLimits::default(),
)
.expect("a wrappable payload")
.expect("packet has a buffer")
};
assert_eq!(
decoder.send_packet(&take()).expect("no fault"),
Sent::Accepted,
);
assert_eq!(
decoder.send_packet(&take()).expect("no fault"),
Sent::Accepted,
"the audio road has one scratch, so a send under a park loses nothing",
);
let mut frame = empty_audio_frame();
let mut delivered = 0u32;
while matches!(
decoder.receive_frame(&mut frame).expect("no fault"),
Received::Frame
) {
delivered += 1;
}
assert!(delivered > 0, "the two packets produced no frame at all");
assert_eq!(decoder.send_eof().expect("no fault"), Sent::Accepted);
loop {
match decoder.receive_frame(&mut frame).expect("no fault") {
Received::Frame => {}
Received::NeedsInput => panic!("a decoder at EOF asked for input"),
Received::Ended => break,
}
}
}
#[test]
fn a_packet_after_eof_is_refused_by_the_audio_substrate() {
let path = fixtures_root().join("pcm_s16le/02_pyannote_sample.wav");
if !path.exists() {
eprintln!("skipping: run `git submodule update --init` for {path:?}");
return;
}
ffmpeg::init().expect("ffmpeg init");
let mut input = ffmpeg::format::input(&path).expect("open input");
let stream = input
.streams()
.best(ffmpeg::media::Type::Audio)
.expect("audio stream");
let stream_index = stream.index();
let mut decoder = FfmpegAudioStreamDecoder::open(
stream.parameters(),
Timebase::default(),
mediadecode_ffmpeg::DecoderLimits::default(),
)
.expect("open audio decoder");
let packet = input
.packets()
.find_map(|(s, p)| (s.index() == stream_index).then_some(p))
.expect("the fixture has packets");
let pkt = mediadecode_ffmpeg::boundary::owned_audio_packet_from_ffmpeg_in(
&packet,
mediadecode::Timebase::default(),
mediadecode_ffmpeg::PacketLimits::default(),
)
.expect("a wrappable payload")
.expect("packet has a buffer");
assert_eq!(decoder.send_packet(&pkt).expect("no fault"), Sent::Accepted);
assert_eq!(decoder.send_eof().expect("no fault"), Sent::Accepted);
let mut frame = empty_audio_frame();
loop {
match decoder.receive_frame(&mut frame).expect("no fault") {
Received::Frame => {}
Received::NeedsInput => panic!("a decoder at EOF asked for input"),
Received::Ended => break,
}
}
for _ in 0..2 {
let refused = decoder.send_packet(&pkt);
assert!(
refused.is_err(),
"a packet after end-of-stream must be a fault, got {refused:?}",
);
}
assert_eq!(
decoder.receive_frame(&mut frame).expect("no fault"),
Received::Ended,
"the refused packet must not have re-armed the decoder",
);
decoder.flush().expect("flush");
assert_eq!(decoder.send_packet(&pkt).expect("no fault"), Sent::Accepted);
}