mod support;
use ffmpeg_next::{
ChannelLayout,
format::{Sample, sample::Type},
};
use mediadecode::{
decoder::AudioStreamDecoder,
demuxer::{DemuxedPacket, Demuxer, TrackKind},
resampler::AudioResampler,
};
use mediadecode_ffmpeg::{
FfmpegAudioStreamDecoder, FfmpegDemuxer, FfmpegResampler, ResampleError, ResampleSpec,
empty_audio_frame,
};
use support::Corpus;
struct Converted {
samples: Vec<i16>,
frames: Vec<(i64, u32)>,
tail_frames: usize,
channels: u8,
planes: u8,
}
fn run(path: &std::path::Path, target: ResampleSpec) -> Converted {
let mut demuxer = FfmpegDemuxer::open(path).expect("open");
let track = demuxer
.tracks()
.iter()
.position(|t| t.kind() == TrackKind::Audio)
.expect("an audio track");
let info = &demuxer.tracks()[track];
let mut decoder = FfmpegAudioStreamDecoder::open(
info
.extra()
.clone_parameters()
.expect("the checked handoff"),
info.timebase(),
)
.expect("open decoder");
let source = ResampleSpec::from_decoder(decoder.inner()).expect("the decoder names its shape");
assert_eq!(
ResampleSpec::from_parameters(info.extra().parameters()),
Some(source),
"for PCM the declared spec and the decoder's own spec are the same",
);
let mut resampler = FfmpegResampler::new(source, target).expect("open resampler");
let mut decoded = empty_audio_frame();
let mut out = empty_audio_frame();
let mut converted = Converted {
samples: Vec::new(),
frames: Vec::new(),
tail_frames: 0,
channels: 0,
planes: 0,
};
let collect = |converted: &mut Converted, frame: &mediadecode_ffmpeg::AudioFrame| {
converted.channels = frame.channel_count();
converted.planes = frame.plane_count();
converted.frames.push((
frame.pts().expect("every output frame is stamped").pts(),
frame.nb_samples(),
));
let valid = frame.nb_samples() as usize * frame.channel_count() as usize * 2;
let bytes = &frame.planes()[0].data_ref().as_ref()[..valid];
converted.samples.extend(
bytes
.as_chunks::<2>()
.0
.iter()
.copied()
.map(i16::from_le_bytes),
);
};
while let Some(packet) = demuxer.next_packet().expect("pull") {
let DemuxedPacket::Audio(p) = packet else {
continue;
};
let (t, packet) = p.into_parts();
if t.get() != track {
continue;
}
decoder.send_packet(&packet).expect("send_packet");
while decoder.receive_frame(&mut decoded).is_ok() {
resampler.send_frame(&decoded).expect("send_frame");
while resampler.receive_frame(&mut out).is_ok() {
collect(&mut converted, &out);
}
}
}
decoder.send_eof().expect("decoder eof");
while decoder.receive_frame(&mut decoded).is_ok() {
resampler.send_frame(&decoded).expect("send_frame");
while resampler.receive_frame(&mut out).is_ok() {
collect(&mut converted, &out);
}
}
resampler.send_eof().expect("resampler eof");
while resampler.receive_frame(&mut out).is_ok() {
converted.tail_frames += 1;
collect(&mut converted, &out);
}
converted
}
fn zero_crossings(samples: &[i16]) -> usize {
samples
.windows(2)
.filter(|w| (w[0] >= 0) != (w[1] >= 0))
.count()
}
fn mono_16k() -> ResampleSpec {
ResampleSpec::new(16_000, Sample::I16(Type::Packed), ChannelLayout::MONO)
}
#[test]
fn forty_eight_to_sixteen_keeps_the_tone_and_the_length() {
let Some(corpus) = Corpus::new() else { return };
let path = corpus.sine_wav("sine48.wav", 48_000, 1, 440, 1.0);
let out = run(&path, mono_16k());
assert!(
(15_960..=16_040).contains(&out.samples.len()),
"16000 samples expected, got {}",
out.samples.len(),
);
let crossings = zero_crossings(&out.samples);
assert!(
(860..=900).contains(&crossings),
"440 Hz means ~880 zero crossings, got {crossings}",
);
assert_eq!(out.channels, 1);
}
#[test]
fn a_fractional_ratio_still_lands_on_length_and_leaves_a_tail() {
let Some(corpus) = Corpus::new() else { return };
let path = corpus.sine_wav("sine44.wav", 44_100, 1, 440, 1.0);
let out = run(&path, mono_16k());
assert!(
(15_960..=16_040).contains(&out.samples.len()),
"16000 samples expected, got {}",
out.samples.len(),
);
let crossings = zero_crossings(&out.samples);
assert!(
(860..=900).contains(&crossings),
"440 Hz means ~880 zero crossings, got {crossings}",
);
assert!(
out.tail_frames > 0,
"send_eof drained nothing — every file would lose its last tens of milliseconds",
);
}
#[test]
fn stereo_folds_to_mono() {
let Some(corpus) = Corpus::new() else { return };
let path = corpus.sine_wav("sine48-stereo.wav", 48_000, 2, 440, 1.0);
let source = {
let demuxer = FfmpegDemuxer::open(&path).expect("open");
ResampleSpec::from_parameters(demuxer.tracks()[0].extra().parameters()).expect("audio spec")
};
assert_eq!(source.channels(), 2, "the file really is stereo");
let out = run(&path, mono_16k());
assert_eq!(out.channels, 1, "the target layout is what comes out");
assert_eq!(out.planes, 1, "packed s16 mono is one plane");
assert!(
(15_960..=16_040).contains(&out.samples.len()),
"one sample per output frame per channel, and there is one channel now: got {}",
out.samples.len(),
);
}
#[test]
fn output_timestamps_are_continuous_through_the_tail() {
let Some(corpus) = Corpus::new() else { return };
let path = corpus.sine_wav("sine44.wav", 44_100, 1, 440, 1.0);
let out = run(&path, mono_16k());
assert!(out.frames.len() > 1, "more than one frame to compare");
assert_eq!(
out.frames[0].0, 0,
"the source starts at zero, so does the output"
);
for pair in out.frames.windows(2) {
let (pts, samples) = pair[0];
let (next, _) = pair[1];
assert_eq!(
next,
pts + i64::from(samples),
"a gap or an overlap in the output timeline at pts {pts}",
);
}
let (last_pts, last_len) = *out.frames.last().expect("at least one frame");
assert_eq!(
last_pts + i64::from(last_len),
out.samples.len() as i64,
"the timeline ends where the samples do",
);
}
#[test]
fn a_mid_stream_format_change_is_refused_by_name() {
support::init_ffmpeg();
let source = ResampleSpec::new(48_000, Sample::I16(Type::Packed), ChannelLayout::STEREO);
let mut resampler = FfmpegResampler::new(source, mono_16k()).expect("open resampler");
let good = mediadecode_ffmpeg::AudioFrame::new(
48_000,
0,
2,
mediadecode_ffmpeg::SampleFormat::S16,
mediadecode_ffmpeg::channel_layout_description_from_ffmpeg(&ChannelLayout::STEREO),
std::array::from_fn(|_| {
mediadecode::frame::Plane::new(mediadecode_ffmpeg::FfmpegBuffer::empty(), 0)
}),
1,
Default::default(),
);
resampler
.send_frame(&good)
.expect("the declared source spec");
let changed = mediadecode_ffmpeg::AudioFrame::new(
44_100,
0,
2,
mediadecode_ffmpeg::SampleFormat::S16,
mediadecode_ffmpeg::channel_layout_description_from_ffmpeg(&ChannelLayout::STEREO),
std::array::from_fn(|_| {
mediadecode::frame::Plane::new(mediadecode_ffmpeg::FfmpegBuffer::empty(), 0)
}),
1,
Default::default(),
);
let err = resampler
.send_frame(&changed)
.expect_err("the face never silently reconfigures");
match err {
ResampleError::SourceChanged(p) => {
assert_eq!((p.expected_rate(), p.found_rate()), (48_000, 44_100));
}
other => panic!("expected a named refusal, got {other:?}"),
}
let mono = mediadecode_ffmpeg::AudioFrame::new(
48_000,
0,
1,
mediadecode_ffmpeg::SampleFormat::S16,
mediadecode_ffmpeg::channel_layout_description_from_ffmpeg(&ChannelLayout::MONO),
std::array::from_fn(|_| {
mediadecode::frame::Plane::new(mediadecode_ffmpeg::FfmpegBuffer::empty(), 0)
}),
1,
Default::default(),
);
assert!(matches!(
resampler.send_frame(&mono),
Err(ResampleError::SourceChanged(_)),
));
}
#[test]
fn the_needs_more_signal_is_an_error_variant() {
support::init_ffmpeg();
let source = ResampleSpec::new(48_000, Sample::I16(Type::Packed), ChannelLayout::STEREO);
let mut resampler = FfmpegResampler::new(source, mono_16k()).expect("open resampler");
let mut dst = empty_audio_frame();
let err = resampler
.receive_frame(&mut dst)
.expect_err("nothing has been sent");
assert!(err.is_again(), "got {err:?}");
resampler.send_eof().expect("eof");
assert!(
resampler
.receive_frame(&mut dst)
.expect_err("an empty tail")
.is_again()
);
let frame = mediadecode_ffmpeg::AudioFrame::new(
48_000,
0,
2,
mediadecode_ffmpeg::SampleFormat::S16,
mediadecode_ffmpeg::channel_layout_description_from_ffmpeg(&ChannelLayout::STEREO),
std::array::from_fn(|_| {
mediadecode::frame::Plane::new(mediadecode_ffmpeg::FfmpegBuffer::empty(), 0)
}),
1,
Default::default(),
);
assert!(matches!(
resampler.send_frame(&frame),
Err(ResampleError::AfterEof),
));
resampler.flush().expect("flush");
resampler.send_frame(&frame).expect("reusable after flush");
}
fn stereo_frame(
samples: u32,
plane_len: usize,
pts: Option<i64>,
) -> mediadecode_ffmpeg::AudioFrame {
filled_frame(
48_000,
samples,
2,
ChannelLayout::STEREO,
&vec![0u8; plane_len],
pts,
)
}
fn mono_frame(
rate: u32,
samples: u32,
amplitude: i16,
pts: Option<i64>,
) -> mediadecode_ffmpeg::AudioFrame {
let bytes: Vec<u8> = std::iter::repeat_n(amplitude.to_le_bytes(), samples as usize)
.flatten()
.collect();
filled_frame(rate, samples, 1, ChannelLayout::MONO, &bytes, pts)
}
fn filled_frame(
rate: u32,
samples: u32,
channels: u8,
layout: ChannelLayout,
bytes: &[u8],
pts: Option<i64>,
) -> mediadecode_ffmpeg::AudioFrame {
let plane = mediadecode_ffmpeg::FfmpegBuffer::copy_from_slice(bytes).expect("plane allocation");
let planes = std::array::from_fn(|index| {
mediadecode::frame::Plane::new(
if index == 0 {
plane.clone()
} else {
mediadecode_ffmpeg::FfmpegBuffer::empty()
},
0,
)
});
mediadecode_ffmpeg::AudioFrame::new(
rate,
samples,
channels,
mediadecode_ffmpeg::SampleFormat::S16,
mediadecode_ffmpeg::channel_layout_description_from_ffmpeg(&layout),
planes,
1,
Default::default(),
)
.with_pts(pts.map(|pts| {
mediadecode::Timestamp::new(
pts,
mediadecode::Timebase::new(
1,
std::num::NonZeroI32::new(rate as i32).expect("a real rate"),
),
)
}))
}
#[test]
fn a_custom_layout_is_refused_by_name() {
support::init_ffmpeg();
let mut raw: ffmpeg_next::ffi::AVChannelLayout = unsafe { std::mem::zeroed() };
let rc = unsafe { ffmpeg_next::ffi::av_channel_layout_custom_init(&mut raw, 2) };
assert_eq!(rc, 0, "av_channel_layout_custom_init");
assert_eq!(
raw.order,
ffmpeg_next::ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM,
"the layout under test really is a custom one",
);
let custom = ChannelLayout(raw);
let hazardous = ResampleSpec::new(48_000, Sample::I16(Type::Packed), custom);
match FfmpegResampler::new(hazardous, mono_16k()) {
Err(ResampleError::UnsupportedLayout(p)) => {
assert_eq!(p.end().to_string(), "source");
assert_eq!(p.channels(), 2);
}
Err(other) => panic!("expected UnsupportedLayout, got {other:?}"),
Ok(_) => panic!("a custom layout must not reach swr or a staged frame"),
}
let source = ResampleSpec::new(48_000, Sample::I16(Type::Packed), ChannelLayout::STEREO);
assert!(matches!(
FfmpegResampler::new(
source,
ResampleSpec::new(16_000, Sample::I16(Type::Packed), custom)
),
Err(ResampleError::UnsupportedLayout(p)) if p.end() == mediadecode_ffmpeg::SpecEnd::Target,
));
unsafe { ffmpeg_next::ffi::av_channel_layout_uninit(&mut raw) };
assert!(matches!(
FfmpegResampler::new(
ResampleSpec::new(0, Sample::I16(Type::Packed), ChannelLayout::STEREO),
mono_16k(),
),
Err(ResampleError::UnsupportedRate(p)) if p.rate() == 0,
));
assert!(matches!(
FfmpegResampler::new(
ResampleSpec::new(48_000, Sample::None, ChannelLayout::STEREO),
mono_16k(),
),
Err(ResampleError::UnsupportedFormat(_)),
));
let mut empty: ffmpeg_next::ffi::AVChannelLayout = unsafe { std::mem::zeroed() };
empty.nb_channels = 0;
assert!(matches!(
FfmpegResampler::new(
ResampleSpec::new(48_000, Sample::I16(Type::Packed), ChannelLayout(empty)),
mono_16k(),
),
Err(ResampleError::UnsupportedLayout(p)) if p.channels() == 0,
));
}
fn tone_in_one_channel(
rate: u32,
samples: u32,
layout: ChannelLayout,
channel: usize,
) -> mediadecode_ffmpeg::AudioFrame {
let channels = layout.channels() as usize;
let mut bytes = Vec::with_capacity(samples as usize * channels * 2);
for n in 0..samples {
let phase = f64::from(n) * 2.0 * std::f64::consts::PI * 440.0 / f64::from(rate);
let value = (phase.sin() * 20_000.0) as i16;
for ch in 0..channels {
bytes.extend_from_slice(&if ch == channel { value } else { 0 }.to_le_bytes());
}
}
filled_frame(rate, samples, channels as u8, layout, &bytes, None)
}
fn converted_rms(resampler: &mut FfmpegResampler, frame: &mediadecode_ffmpeg::AudioFrame) -> f64 {
let mut out = empty_audio_frame();
let mut energy = 0f64;
let mut count = 0usize;
for _ in 0..3 {
resampler.send_frame(frame).expect("send_frame");
while resampler.receive_frame(&mut out).is_ok() {
let valid = out.nb_samples() as usize * out.channel_count() as usize * 2;
for chunk in out.planes()[0].data_ref().as_ref()[..valid]
.as_chunks::<2>()
.0
{
let sample = f64::from(i16::from_le_bytes(*chunk));
energy += sample * sample;
count += 1;
}
}
}
if count == 0 {
0.0
} else {
(energy / count as f64).sqrt()
}
}
#[test]
fn no_accepted_conversion_silently_drops_a_channel() {
support::init_ffmpeg();
let rate = 48_000;
let samples = 4_800;
let source = ResampleSpec::new(rate, Sample::I16(Type::Packed), ChannelLayout::_7POINT1);
for channel in 0..8 {
let mut resampler = FfmpegResampler::new(source, mono_16k()).expect("7.1 -> mono opens");
let rms = converted_rms(
&mut resampler,
&tone_in_one_channel(rate, samples, ChannelLayout::_7POINT1, channel),
);
if channel == 3 {
assert!(rms < 1.0, "LFE unexpectedly mixed at {rms}");
continue;
}
assert!(rms > 100.0, "source channel {channel} vanished (rms {rms})");
}
let big = ChannelLayout::_22POINT2;
let source = ResampleSpec::new(rate, Sample::I16(Type::Packed), big);
let target = ResampleSpec::new(16_000, Sample::I16(Type::Packed), big);
for channel in 0..24 {
let mut resampler = FfmpegResampler::new(source, target).expect("22.2 -> 22.2 opens");
let rms = converted_rms(
&mut resampler,
&tone_in_one_channel(rate, samples, big, channel),
);
assert!(
rms > 10.0,
"source channel {channel} vanished from a same-layout conversion (rms {rms})",
);
}
}
#[test]
fn a_rematrix_that_would_drop_channels_is_refused_by_name() {
support::init_ffmpeg();
let source = ResampleSpec::new(48_000, Sample::I16(Type::Packed), ChannelLayout::_22POINT2);
match FfmpegResampler::new(source, mono_16k()) {
Err(ResampleError::ChannelDropped(p)) => {
assert_eq!((p.source_channels(), p.target_channels()), (24, 1));
assert_eq!(
p.channel(),
9,
"the first channel swr's matrix cannot route"
);
}
Err(other) => panic!("expected ChannelDropped, got {other:?}"),
Ok(_) => panic!("a conversion that drops fifteen channels must not open"),
}
assert!(
matches!(
FfmpegResampler::new(
ResampleSpec::new(48_000, Sample::I16(Type::Packed), ChannelLayout::CUBE),
ResampleSpec::new(16_000, Sample::I16(Type::Packed), ChannelLayout::STEREO),
)
.map(|_| ()),
Err(ResampleError::ChannelDropped(p)) if p.channel() == 6,
),
"eight channels is not a safe count, it is just a small one",
);
for (name, source_layout, target_layout) in [
("7.1 -> mono", ChannelLayout::_7POINT1, ChannelLayout::MONO),
(
"octagonal -> mono",
ChannelLayout::OCTAGONAL,
ChannelLayout::MONO,
),
(
"7.1.2 -> stereo",
ChannelLayout::_7POINT1POINT2,
ChannelLayout::STEREO,
),
(
"stereo -> 22.2",
ChannelLayout::STEREO,
ChannelLayout::_22POINT2,
),
(
"22.2 -> 22.2",
ChannelLayout::_22POINT2,
ChannelLayout::_22POINT2,
),
(
"5.1 -> 7.1",
ChannelLayout::_5POINT1,
ChannelLayout::_7POINT1,
),
] {
FfmpegResampler::new(
ResampleSpec::new(48_000, Sample::I16(Type::Packed), source_layout),
ResampleSpec::new(16_000, Sample::I16(Type::Packed), target_layout),
)
.unwrap_or_else(|e| panic!("{name} must still open: {e}"));
}
}
fn raw_swr_rms(source_layout: ChannelLayout, channel: usize) -> f64 {
use ffmpeg_next::{frame, software::resampling::Context};
let channels = source_layout.channels() as usize;
let samples = 4_800usize;
let mut context = Context::get(
Sample::I16(Type::Packed),
source_layout,
48_000,
Sample::I16(Type::Packed),
ChannelLayout::MONO,
16_000,
)
.expect("swresample opens this pair happily — that is the whole problem");
let staged = if source_layout.is_empty() {
ChannelLayout::default(source_layout.channels())
} else {
source_layout
};
let mut input = frame::Audio::new(Sample::I16(Type::Packed), samples, staged);
input.set_rate(48_000);
{
let plane = input.data_mut(0);
for n in 0..samples {
let phase = n as f64 * 2.0 * std::f64::consts::PI * 440.0 / 48_000.0;
let value = (phase.sin() * 20_000.0) as i16;
for ch in 0..channels {
let offset = (n * channels + ch) * 2;
let sample = if ch == channel { value } else { 0 };
plane[offset..offset + 2].copy_from_slice(&sample.to_le_bytes());
}
}
}
let mut out = frame::Audio::new(Sample::I16(Type::Packed), samples, ChannelLayout::MONO);
out.set_rate(16_000);
context.run(&input, &mut out).expect("convert");
let produced = out.samples();
if produced == 0 {
return 0.0;
}
let bytes = &out.data(0)[..produced * 2];
let energy: f64 = bytes
.as_chunks::<2>()
.0
.iter()
.map(|chunk| {
let sample = f64::from(i16::from_le_bytes(*chunk));
sample * sample
})
.sum();
(energy / produced as f64).sqrt()
}
#[test]
fn an_unspecified_layout_cannot_smuggle_a_lossy_conversion_past_the_pair_check() {
support::init_ffmpeg();
assert_eq!(
ChannelLayout::default(24),
ChannelLayout::_22POINT2,
"the resolution that made the door: 24 unspecified channels are 22.2",
);
let unspec24 = ResampleSpec::unspecified_layout(24);
assert!(
raw_swr_rms(unspec24, 0) > 100.0,
"channel 0 must survive, or the probe measures nothing",
);
for channel in [9, 23] {
assert_eq!(
raw_swr_rms(unspec24, channel),
0.0,
"source channel {channel} was expected to vanish through raw swr",
);
}
let source = ResampleSpec::new(48_000, Sample::I16(Type::Packed), unspec24);
for target_layout in [ChannelLayout::MONO, ChannelLayout::STEREO] {
let target = ResampleSpec::new(16_000, Sample::I16(Type::Packed), target_layout);
match FfmpegResampler::new(source, target).map(|_| ()) {
Err(ResampleError::ChannelDropped(p)) => {
assert_eq!(p.source_channels(), 24);
assert_eq!(
p.channel(),
9,
"the refusal names the channel the tone probe found silent",
);
}
other => panic!("unspecified 24 channels must not open: {other:?}"),
}
}
}
#[test]
fn the_maskless_wav_population_still_converts() {
support::init_ffmpeg();
let rate = 48_000;
let samples = 4_800;
let mono = ResampleSpec::new(
rate,
Sample::I16(Type::Packed),
ResampleSpec::unspecified_layout(1),
);
let mut resampler = FfmpegResampler::new(mono, mono_16k()).expect("maskless mono still opens");
let rms = converted_rms(
&mut resampler,
&tone_in_one_channel(rate, samples, ResampleSpec::unspecified_layout(1), 0),
);
assert!(
rms > 100.0,
"maskless mono converted to silence (rms {rms})"
);
let stereo_layout = ResampleSpec::unspecified_layout(2);
let stereo = ResampleSpec::new(rate, Sample::I16(Type::Packed), stereo_layout);
for channel in 0..2 {
let mut resampler =
FfmpegResampler::new(stereo, mono_16k()).expect("maskless stereo still opens");
let rms = converted_rms(
&mut resampler,
&tone_in_one_channel(rate, samples, stereo_layout, channel),
);
assert!(
rms > 100.0,
"maskless stereo lost channel {channel} (rms {rms})",
);
}
FfmpegResampler::new(
ResampleSpec::new(
rate,
Sample::I16(Type::Packed),
ResampleSpec::unspecified_layout(24),
),
ResampleSpec::new(
16_000,
Sample::I16(Type::Packed),
ResampleSpec::unspecified_layout(24),
),
)
.expect("nothing is being rematrixed here");
}
#[test]
fn a_planar_layout_past_eight_channels_is_refused_at_construction() {
support::init_ffmpeg();
let planar_22_2 = ResampleSpec::new(48_000, Sample::F32(Type::Planar), ChannelLayout::_22POINT2);
assert_eq!(planar_22_2.channels(), 24, "22.2 really is 24 channels");
match FfmpegResampler::new(planar_22_2, mono_16k()) {
Err(ResampleError::TooManyPlanes(p)) => {
assert_eq!(p.end().to_string(), "source");
assert_eq!((p.channels(), p.limit()), (24, 8));
}
Err(other) => panic!("expected TooManyPlanes for the source, got {other:?}"),
Ok(_) => panic!("a source no frame can represent must not open"),
}
let stereo = ResampleSpec::new(48_000, Sample::I16(Type::Packed), ChannelLayout::STEREO);
assert!(
matches!(
FfmpegResampler::new(stereo, planar_22_2).map(|_| ()),
Err(ResampleError::TooManyPlanes(p))
if p.end() == mediadecode_ffmpeg::SpecEnd::Target && p.channels() == 24,
),
"the target end is the one that used to fail mid-stream",
);
FfmpegResampler::new(
ResampleSpec::new(48_000, Sample::I16(Type::Packed), ChannelLayout::_22POINT2),
ResampleSpec::new(16_000, Sample::I16(Type::Packed), ChannelLayout::_22POINT2),
)
.expect("packed 22.2 is one plane");
assert!(
matches!(
FfmpegResampler::new(
ResampleSpec::new(48_000, Sample::F32(Type::Planar), ChannelLayout::_22POINT2),
ResampleSpec::new(16_000, Sample::F32(Type::Planar), ChannelLayout::_22POINT2),
)
.map(|_| ()),
Err(ResampleError::TooManyPlanes(_)),
),
"and the same conversion planar is refused for its planes, not its pair",
);
FfmpegResampler::new(
ResampleSpec::new(48_000, Sample::F32(Type::Planar), ChannelLayout::_7POINT1),
mono_16k(),
)
.expect("eight planar channels is exactly the limit");
}
#[test]
fn a_forged_frame_geometry_is_refused_before_it_can_allocate() {
support::init_ffmpeg();
let source = ResampleSpec::new(48_000, Sample::I16(Type::Packed), ChannelLayout::STEREO);
let mut resampler = FfmpegResampler::new(source, mono_16k()).expect("open resampler");
let forged = stereo_frame(u32::MAX, 16, None);
match resampler.send_frame(&forged) {
Err(ResampleError::PlaneCount(p)) => {
assert_eq!(
p.found(),
16,
"the plane's real length is what was compared"
);
assert!(p.expected() > p.found());
}
other => panic!("expected a geometry refusal, got {other:?}"),
}
let planeless = mediadecode_ffmpeg::AudioFrame::new(
48_000,
128,
2,
mediadecode_ffmpeg::SampleFormat::S16,
mediadecode_ffmpeg::channel_layout_description_from_ffmpeg(&ChannelLayout::STEREO),
std::array::from_fn(|_| {
mediadecode::frame::Plane::new(mediadecode_ffmpeg::FfmpegBuffer::empty(), 0)
}),
0,
Default::default(),
);
assert!(matches!(
resampler.send_frame(&planeless),
Err(ResampleError::PlaneCount(p)) if p.expected() == 1 && p.found() == 0,
));
resampler
.send_frame(&stereo_frame(480, 480 * 2 * 2, Some(0)))
.expect("an honest frame");
}
#[test]
fn a_refused_frame_does_not_stamp_the_next_good_one() {
support::init_ffmpeg();
let source = ResampleSpec::new(48_000, Sample::I16(Type::Packed), ChannelLayout::STEREO);
let mut resampler = FfmpegResampler::new(source, mono_16k()).expect("open resampler");
let forged = stereo_frame(4_800, 16, Some(48_000 * 60));
assert!(matches!(
resampler.send_frame(&forged),
Err(ResampleError::PlaneCount(_)),
));
resampler
.send_frame(&stereo_frame(4_800, 4_800 * 2 * 2, Some(0)))
.expect("an honest frame");
let mut out = empty_audio_frame();
resampler.receive_frame(&mut out).expect("converted output");
assert_eq!(
out.pts().expect("stamped").pts(),
0,
"the rejected frame's timestamp must not have anchored the timeline",
);
}
#[test]
fn a_timestamp_that_cannot_be_rescaled_is_refused_before_anything_moves() {
support::init_ffmpeg();
let source = ResampleSpec::new(48_000, Sample::I16(Type::Packed), ChannelLayout::STEREO);
let target = ResampleSpec::new(192_000, Sample::I16(Type::Packed), ChannelLayout::MONO);
let mut resampler = FfmpegResampler::new(source, target).expect("open resampler");
let samples = 480;
let bytes = samples as usize * 2 * 2;
for pts in [i64::MAX - 1, i64::MIN + 1, i64::MIN] {
match resampler.send_frame(&stereo_frame(samples, bytes, Some(pts))) {
Err(ResampleError::TimestampOutOfRange(p)) => {
assert_eq!(p.pts(), pts, "the refusal names the timestamp it read");
}
other => panic!("expected TimestampOutOfRange for {pts}, got {other:?}"),
}
assert_eq!(
resampler.delay(),
0,
"a refused timestamp left input inside the filter",
);
let mut dst = empty_audio_frame();
assert!(
resampler
.receive_frame(&mut dst)
.expect_err("nothing was converted")
.is_again(),
);
}
resampler
.send_frame(&stereo_frame(samples, bytes, Some(0)))
.expect("an honest frame");
let mut out = empty_audio_frame();
resampler.receive_frame(&mut out).expect("converted output");
assert_eq!(
out.pts().expect("stamped").pts(),
0,
"a refused timestamp anchored the timeline anyway",
);
}
#[test]
fn the_output_timeline_refuses_to_overflow() {
support::init_ffmpeg();
let source = ResampleSpec::new(48_000, Sample::I16(Type::Packed), ChannelLayout::STEREO);
let target = ResampleSpec::new(48_000, Sample::I16(Type::Packed), ChannelLayout::MONO);
let mut resampler = FfmpegResampler::new(source, target).expect("open resampler");
let samples = 4_800;
let frame = stereo_frame(samples, samples as usize * 2 * 2, Some(i64::MAX - 8));
match resampler.send_frame(&frame) {
Err(ResampleError::TimestampOverflow(p)) => {
assert_eq!(p.pts(), i64::MAX - 8);
assert!(p.samples() > 8, "the count that would not fit");
}
other => panic!("expected TimestampOverflow, got {other:?}"),
}
assert_eq!(
resampler.delay(),
0,
"the refused frame was consumed before the timeline was checked",
);
let mut dst = empty_audio_frame();
assert!(
resampler
.receive_frame(&mut dst)
.expect_err("nothing was converted")
.is_again(),
"a refused frame left output ready",
);
resampler
.send_frame(&stereo_frame(samples, samples as usize * 2 * 2, Some(0)))
.expect("the session survived the refusal");
resampler
.receive_frame(&mut dst)
.expect("and converts the next frame");
assert_eq!(dst.pts().expect("stamped").pts(), 0);
}
#[test]
fn flush_leaves_nothing_of_the_previous_stream_behind() {
support::init_ffmpeg();
let source = ResampleSpec::new(44_100, Sample::I16(Type::Packed), ChannelLayout::MONO);
let mut resampler = FfmpegResampler::new(source, mono_16k()).expect("open resampler");
let mut out = empty_audio_frame();
for index in 0..4 {
let pts = index * 4_410;
resampler
.send_frame(&mono_frame(44_100, 4_410, 20_000, Some(pts)))
.expect("send_frame");
while resampler.receive_frame(&mut out).is_ok() {}
}
assert!(
resampler.delay() > 0,
"the filter has to be holding something for the reset to matter",
);
resampler.flush().expect("flush");
assert_eq!(
resampler.delay(),
0,
"a flush that reports success cannot leave the old delay line inside swr",
);
let mut loudest = 0i16;
let mut first_pts = None;
for index in 0..4 {
resampler
.send_frame(&mono_frame(44_100, 4_410, 0, Some(index * 4_410)))
.expect("send_frame");
while resampler.receive_frame(&mut out).is_ok() {
first_pts.get_or_insert(out.pts().expect("stamped").pts());
let valid = out.nb_samples() as usize * 2;
for chunk in out.planes()[0].data_ref().as_ref()[..valid]
.as_chunks::<2>()
.0
{
loudest = loudest.max(i16::from_le_bytes(*chunk).abs());
}
}
}
assert_eq!(first_pts, Some(0), "the new stream owns the new timeline");
assert!(
loudest < 100,
"silence came out at amplitude {loudest}: the previous stream's tail survived the flush",
);
}