use core::{ffi::c_char, slice, str::FromStr};
use ffmpeg_next::{ChannelLayout as AvChannelLayout, ffi};
use mediaframe::audio::{ChannelLayout, ChannelLayoutDescription, ChannelOrder, ChannelSpec};
use smol_bytes::Utf8Bytes;
use std::vec::Vec;
pub fn channel_layout_from_ffmpeg(
value: &AvChannelLayout,
) -> Result<ChannelLayout, ChannelLayoutFault> {
Ok(
channel_layout_description_from_ffmpeg(value)?
.known_kind()
.clone(),
)
}
fn mapped_constant(value: &AvChannelLayout) -> Option<ChannelLayout> {
let named = match () {
() if value.eq(&AvChannelLayout::MONO) => ChannelLayout::Mono,
() if value.eq(&AvChannelLayout::STEREO) => ChannelLayout::Stereo,
() if value.eq(&AvChannelLayout::STEREO_DOWNMIX) => ChannelLayout::StereoDownmix,
() if value.eq(&AvChannelLayout::SURROUND) => ChannelLayout::Ch3_0,
() if value.eq(&AvChannelLayout::QUAD) => ChannelLayout::Quad,
() if value.eq(&AvChannelLayout::HEXAGONAL) => ChannelLayout::Hexagonal,
() if value.eq(&AvChannelLayout::OCTAGONAL) => ChannelLayout::Octagonal,
() if value.eq(&AvChannelLayout::HEXADECAGONAL) => ChannelLayout::Hexadecagonal,
() if value.eq(&AvChannelLayout::CUBE) => ChannelLayout::Cube,
() if value.eq(&AvChannelLayout::_2POINT1) => ChannelLayout::Ch2_1,
() if value.eq(&AvChannelLayout::_2_1) => ChannelLayout::Ch3_0Back,
() if value.eq(&AvChannelLayout::_2_2) => ChannelLayout::QuadSide,
() if value.eq(&AvChannelLayout::_3POINT1) => ChannelLayout::Ch3_1,
() if value.eq(&AvChannelLayout::_3POINT1POINT2) => ChannelLayout::Ch3_1_2,
() if value.eq(&AvChannelLayout::_4POINT0) => ChannelLayout::Ch4_0,
() if value.eq(&AvChannelLayout::_4POINT1) => ChannelLayout::Ch4_1,
() if value.eq(&AvChannelLayout::_5POINT0) => ChannelLayout::Ch5_0,
() if value.eq(&AvChannelLayout::_5POINT0_BACK) => ChannelLayout::Ch5_0Back,
() if value.eq(&AvChannelLayout::_5POINT1) => ChannelLayout::Ch5_1,
() if value.eq(&AvChannelLayout::_5POINT1_BACK) => ChannelLayout::Ch5_1Back,
() if value.eq(&AvChannelLayout::_5POINT1POINT2_BACK) => ChannelLayout::Ch5_1_2Back,
() if value.eq(&AvChannelLayout::_5POINT1POINT4_BACK) => ChannelLayout::Ch5_1_4Back,
() if value.eq(&AvChannelLayout::_6POINT0) => ChannelLayout::Ch6_0,
() if value.eq(&AvChannelLayout::_6POINT0_FRONT) => ChannelLayout::Ch6_0Front,
() if value.eq(&AvChannelLayout::_6POINT1) => ChannelLayout::Ch6_1,
() if value.eq(&AvChannelLayout::_6POINT1_BACK) => ChannelLayout::Ch6_1Back,
() if value.eq(&AvChannelLayout::_6POINT1_FRONT) => ChannelLayout::Ch6_1Front,
() if value.eq(&AvChannelLayout::_7POINT0) => ChannelLayout::Ch7_0,
() if value.eq(&AvChannelLayout::_7POINT0_FRONT) => ChannelLayout::Ch7_0Front,
() if value.eq(&AvChannelLayout::_7POINT1) => ChannelLayout::Ch7_1,
() if value.eq(&AvChannelLayout::_7POINT1_WIDE) => ChannelLayout::Ch7_1Wide,
() if value.eq(&AvChannelLayout::_7POINT1_WIDE_BACK) => ChannelLayout::Ch7_1WideBack,
() if value.eq(&AvChannelLayout::_7POINT1POINT2) => ChannelLayout::Ch7_1_2,
() if value.eq(&AvChannelLayout::_7POINT1POINT4_BACK) => ChannelLayout::Ch7_1_4Back,
() if value.eq(&AvChannelLayout::_7POINT2POINT3) => ChannelLayout::Ch7_2_3,
() if value.eq(&AvChannelLayout::_9POINT1POINT4_BACK) => ChannelLayout::Ch9_1_4Back,
() if value.eq(&AvChannelLayout::_22POINT2) => ChannelLayout::Ch22_2,
() => return None,
};
Some(named)
}
fn channel_layout_from_describe(rendered: &str) -> ChannelLayout {
if rendered.len() > smol_bytes::INLINE_CAP {
return ChannelLayout::default();
}
ChannelLayout::from_str(rendered)
.ok()
.filter(|layout| !matches!(layout, ChannelLayout::Other(_)))
.unwrap_or_default()
}
pub fn channel_order_from_ffmpeg(value: ffi::AVChannelOrder) -> ChannelOrder {
channel_order_from_raw(value as i32)
}
pub fn channel_order_from_raw(raw: i32) -> ChannelOrder {
match raw {
x if x == ffi::AVChannelOrder::AV_CHANNEL_ORDER_NATIVE as i32 => ChannelOrder::Native,
x if x == ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM as i32 => ChannelOrder::Custom,
x if x == ffi::AVChannelOrder::AV_CHANNEL_ORDER_AMBISONIC as i32 => ChannelOrder::Ambisonic,
_ => ChannelOrder::Unspecified,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ChannelLayoutFault {
#[error(
"a custom channel layout declaring {channels} channels reached a safe conversion, which \
cannot verify that its map has that many entries"
)]
UnverifiableCustomMap {
channels: i32,
},
#[error("a custom channel layout declares {channels} channels and carries no usable map")]
MalformedCustomMap {
channels: i32,
},
#[error(
"a channel layout of order {order} declaring {channels} channels is not a shape FFmpeg's \
own helpers can be given"
)]
MalformedLayout {
order: i32,
channels: i32,
},
#[error("out of memory describing a channel layout")]
Alloc,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub(crate) enum LayoutArm {
Mask,
Map,
Undefined,
}
impl LayoutArm {
pub(crate) const fn of(order_raw: i32) -> Self {
if order_raw == ffi::AVChannelOrder::AV_CHANNEL_ORDER_NATIVE as i32
|| order_raw == ffi::AVChannelOrder::AV_CHANNEL_ORDER_AMBISONIC as i32
{
Self::Mask
} else if order_raw == ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM as i32 {
Self::Map
} else {
Self::Undefined
}
}
}
const MAX_DECLARED_CHANNELS: i32 = 65_535;
pub(crate) unsafe fn layout_preflight(
ptr: *const ffi::AVChannelLayout,
) -> Result<(), ChannelLayoutFault> {
use core::ptr::{addr_of, read_unaligned};
let order = unsafe { read_unaligned(addr_of!((*ptr).order).cast::<i32>()) };
let channels = unsafe { (*ptr).nb_channels };
let malformed = Err(ChannelLayoutFault::MalformedLayout { order, channels });
if order == ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM as i32 {
if !(0..=MAX_DECLARED_CHANNELS).contains(&channels) {
return Err(ChannelLayoutFault::MalformedCustomMap { channels });
}
return match unsafe { custom_map_fault(ptr, order) } {
Some(channels) => Err(ChannelLayoutFault::MalformedCustomMap { channels }),
None => Ok(()),
};
}
if !(0..=MAX_DECLARED_CHANNELS).contains(&channels) {
return malformed;
}
if channels == 0 && !matches!(LayoutArm::of(order), LayoutArm::Undefined) {
return malformed;
}
if matches!(LayoutArm::of(order), LayoutArm::Undefined) {
return Ok(());
}
if order == ffi::AVChannelOrder::AV_CHANNEL_ORDER_NATIVE as i32 {
let mask = unsafe { (*ptr).u.mask };
return if i64::from(mask.count_ones()) == i64::from(channels) {
Ok(())
} else {
malformed
};
}
if order == ffi::AVChannelOrder::AV_CHANNEL_ORDER_AMBISONIC as i32 {
let mask = unsafe { (*ptr).u.mask };
return if i64::from(mask.count_ones()) < i64::from(channels) {
Ok(())
} else {
malformed
};
}
Ok(())
}
pub fn channel_layout_description_from_ffmpeg(
value: &AvChannelLayout,
) -> Result<ChannelLayoutDescription, ChannelLayoutFault> {
use core::ptr::{addr_of, read_unaligned};
let ptr = &value.0 as *const ffi::AVChannelLayout;
let order =
channel_order_from_raw(unsafe { read_unaligned(addr_of!((*ptr).order).cast::<i32>()) });
if matches!(order, ChannelOrder::Custom) {
let channels = unsafe { (*ptr).nb_channels };
return Err(ChannelLayoutFault::UnverifiableCustomMap { channels });
}
unsafe { channel_layout_description_from_raw_ptr(ptr) }
}
pub unsafe fn channel_layout_description_from_raw_ptr(
ptr: *const ffi::AVChannelLayout,
) -> Result<ChannelLayoutDescription, ChannelLayoutFault> {
use core::ptr::{addr_of, read_unaligned};
let order_raw = unsafe { read_unaligned(addr_of!((*ptr).order) as *const i32) };
let order = channel_order_from_raw(order_raw);
let nb_channels = unsafe { (*ptr).nb_channels };
let native_mask = match LayoutArm::of(order_raw) {
LayoutArm::Mask => {
let mask = unsafe { (*ptr).u.mask };
if mask != 0 { Some(mask) } else { None }
}
LayoutArm::Map | LayoutArm::Undefined => None,
};
unsafe { layout_preflight(ptr) }?;
let (known_kind, text) = if matches!(order, ChannelOrder::Unspecified) {
(ChannelLayout::default(), Utf8Bytes::default())
} else {
let layout_ref = unsafe { &*(ptr as *const AvChannelLayout) };
let text = describe_layout(layout_ref)?;
let known_kind =
mapped_constant(layout_ref).unwrap_or_else(|| channel_layout_from_describe(&text));
(known_kind, text)
};
let custom_channels_vec = unsafe { custom_channels_raw(ptr, order) }?;
Ok(
ChannelLayoutDescription::new(nb_channels.max(0) as u32)
.with_order(order)
.with_known_kind(known_kind)
.with_native_mask(native_mask)
.with_custom_channels(custom_channels_vec)
.with_text(text),
)
}
pub(crate) unsafe fn custom_map_fault(
ptr: *const ffi::AVChannelLayout,
order_raw: i32,
) -> Option<i32> {
use core::ptr::{addr_of, read_unaligned};
if order_raw != ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM as i32 {
return None;
}
let (map_ptr, channels) = unsafe { ((*ptr).u.map, (*ptr).nb_channels) };
if map_ptr.is_null() || channels <= 0 {
return Some(channels);
}
for index in 0..channels as usize {
let name = unsafe { read_unaligned(addr_of!((*map_ptr.add(index)).name).cast::<[u8; 16]>()) };
if !name.contains(&0) {
return Some(channels);
}
let id = unsafe { read_unaligned(addr_of!((*map_ptr.add(index)).id).cast::<i32>()) };
if id == ffi::AVChannel::AV_CHAN_NONE as i32 {
return Some(channels);
}
}
None
}
unsafe fn custom_channels_raw(
ptr: *const ffi::AVChannelLayout,
order: ChannelOrder,
) -> Result<Vec<ChannelSpec>, ChannelLayoutFault> {
use core::ptr::{addr_of, read_unaligned};
if !matches!(order, ChannelOrder::Custom) {
return Ok(Vec::new());
}
let count = unsafe { (*ptr).nb_channels }.max(0) as usize;
if count == 0 {
return Ok(Vec::new());
}
let map_ptr = unsafe { (*ptr).u.map };
if map_ptr.is_null() {
return Ok(Vec::new());
}
let mut out = Vec::new();
out
.try_reserve_exact(count)
.map_err(|_| ChannelLayoutFault::Alloc)?;
for index in 0..count {
let entry_ptr: *const ffi::AVChannelCustom = unsafe { map_ptr.add(index) };
let raw_id = unsafe { read_unaligned(addr_of!((*entry_ptr).id) as *const i32) };
let label = unsafe { custom_channel_label_raw(entry_ptr) }?;
out.push(ChannelSpec::new(index as u32, raw_id as u32).with_label(label));
}
Ok(out)
}
fn decode_text(bytes: &[u8]) -> Result<Utf8Bytes, ChannelLayoutFault> {
let decoded = crate::demuxer::lossy_len(bytes);
crate::demuxer::lossy_text(bytes, decoded).map_err(|_| ChannelLayoutFault::Alloc)
}
unsafe fn custom_channel_label_raw(
entry_ptr: *const ffi::AVChannelCustom,
) -> Result<Utf8Bytes, ChannelLayoutFault> {
use core::ptr::addr_of;
let name_ptr = unsafe { addr_of!((*entry_ptr).name) } as *const u8;
let bytes = unsafe { slice::from_raw_parts(name_ptr, 16) };
let end = bytes
.iter()
.position(|byte| *byte == 0)
.unwrap_or(bytes.len());
if end == 0 {
return Ok(Utf8Bytes::default());
}
decode_text(&bytes[..end])
}
fn describe_layout(layout: &AvChannelLayout) -> Result<Utf8Bytes, ChannelLayoutFault> {
let mut buf: std::vec::Vec<c_char> = std::vec::Vec::new();
buf
.try_reserve_exact(128)
.map_err(|_| ChannelLayoutFault::Alloc)?;
buf.resize(128, 0 as c_char);
let mut needed =
unsafe { ffi::av_channel_layout_describe(&layout.0 as *const _, buf.as_mut_ptr(), buf.len()) };
if needed < 0 {
return Ok(Utf8Bytes::default());
}
if needed as usize >= buf.len() {
let want = needed as usize + 1;
buf
.try_reserve_exact(want - buf.len())
.map_err(|_| ChannelLayoutFault::Alloc)?;
buf.resize(want, 0 as c_char);
needed = unsafe {
ffi::av_channel_layout_describe(&layout.0 as *const _, buf.as_mut_ptr(), buf.len())
};
if needed < 0 {
return Ok(Utf8Bytes::default());
}
}
let bytes = unsafe { slice::from_raw_parts(buf.as_ptr() as *const u8, buf.len()) };
let end = bytes
.iter()
.position(|byte| *byte == 0)
.unwrap_or(needed as usize)
.min(bytes.len());
if end == 0 {
return Ok(Utf8Bytes::default());
}
decode_text(&bytes[..end])
}
#[cfg(test)]
mod tests {
use super::*;
fn native(mask: u64) -> AvChannelLayout {
let mut raw: ffi::AVChannelLayout = unsafe { core::mem::zeroed() };
let rc = unsafe { ffi::av_channel_layout_from_mask(&mut raw, mask) };
assert_eq!(rc, 0, "av_channel_layout_from_mask({mask:#x}) failed");
AvChannelLayout(raw)
}
fn binaural_mask() -> u64 {
(1u64 << ffi::AVChannel::AV_CHAN_BINAURAL_LEFT as u64)
| (1u64 << ffi::AVChannel::AV_CHAN_BINAURAL_RIGHT as u64)
}
#[test]
fn orphan_layouts_are_named_through_the_describe_rung() {
let cases = [
(binaural_mask(), "binaural", ChannelLayout::Binaural),
(
ffi::AV_CH_LAYOUT_5POINT1 | ffi::AV_CH_TOP_FRONT_LEFT | ffi::AV_CH_TOP_FRONT_RIGHT,
"5.1.2",
ChannelLayout::Ch5_1_2,
),
(
ffi::AV_CH_LAYOUT_9POINT1POINT4_BACK | ffi::AV_CH_TOP_SIDE_LEFT | ffi::AV_CH_TOP_SIDE_RIGHT,
"9.1.6",
ChannelLayout::Ch9_1_6,
),
];
for (mask, slug, expected) in cases {
let layout = native(mask);
assert_eq!(
mapped_constant(&layout),
None,
"{slug} must fall off the constant table — that is what makes it an orphan"
);
assert_eq!(
describe_layout(&layout)
.expect("a well-formed layout describes")
.as_str(),
slug,
"FFmpeg must name {slug} for the rung to have a word to read"
);
assert_eq!(
channel_layout_from_ffmpeg(&layout).expect("a well-formed layout names"),
expected,
"{slug} must reach its named variant through the rung"
);
let described =
channel_layout_description_from_ffmpeg(&layout).expect("a well-formed layout describes");
assert_eq!(
described.known_kind(),
&expected,
"{slug} must be named on the description path too"
);
assert_eq!(described.text(), slug, "{slug} rendering rides `text`");
}
}
#[test]
fn ffmpeg_nines_own_5_1_4_is_named() {
let layout = native(
ffi::AV_CH_LAYOUT_5POINT1
| ffi::AV_CH_TOP_FRONT_LEFT
| ffi::AV_CH_TOP_FRONT_RIGHT
| ffi::AV_CH_TOP_BACK_LEFT
| ffi::AV_CH_TOP_BACK_RIGHT,
);
assert_eq!(
describe_layout(&layout)
.expect("a well-formed layout describes")
.as_str(),
"5.1.4"
);
assert_eq!(
channel_layout_from_ffmpeg(&layout).expect("a well-formed layout names"),
ChannelLayout::Ch5_1_4Back
);
}
#[test]
fn mapped_constants_are_answered_by_the_table_alone() {
let table = [
("MONO", AvChannelLayout::MONO, ChannelLayout::Mono),
("STEREO", AvChannelLayout::STEREO, ChannelLayout::Stereo),
(
"STEREO_DOWNMIX",
AvChannelLayout::STEREO_DOWNMIX,
ChannelLayout::StereoDownmix,
),
("SURROUND", AvChannelLayout::SURROUND, ChannelLayout::Ch3_0),
("_5POINT0", AvChannelLayout::_5POINT0, ChannelLayout::Ch5_0),
(
"_5POINT0_BACK",
AvChannelLayout::_5POINT0_BACK,
ChannelLayout::Ch5_0Back,
),
("_5POINT1", AvChannelLayout::_5POINT1, ChannelLayout::Ch5_1),
(
"_5POINT1_BACK",
AvChannelLayout::_5POINT1_BACK,
ChannelLayout::Ch5_1Back,
),
(
"_5POINT1POINT2_BACK",
AvChannelLayout::_5POINT1POINT2_BACK,
ChannelLayout::Ch5_1_2Back,
),
(
"_7POINT1_TOP_BACK",
AvChannelLayout::_7POINT1_TOP_BACK,
ChannelLayout::Ch5_1_2Back,
),
(
"_7POINT1_WIDE",
AvChannelLayout::_7POINT1_WIDE,
ChannelLayout::Ch7_1Wide,
),
(
"_7POINT1_WIDE_BACK",
AvChannelLayout::_7POINT1_WIDE_BACK,
ChannelLayout::Ch7_1WideBack,
),
(
"_22POINT2",
AvChannelLayout::_22POINT2,
ChannelLayout::Ch22_2,
),
];
for (name, layout, expected) in table {
assert_eq!(
mapped_constant(&layout),
Some(expected.clone()),
"{name} must be answered by the constant table, not by a rendering"
);
assert_eq!(
channel_layout_from_ffmpeg(&layout).expect("a well-formed layout names"),
expected,
"{name}"
);
}
}
#[test]
fn renderings_of_named_layouts_fit_the_inline_window() {
let mut opaque: *mut core::ffi::c_void = core::ptr::null_mut();
let mut seen = 0usize;
loop {
let layout = unsafe { ffi::av_channel_layout_standard(&mut opaque) };
if layout.is_null() {
break;
}
let rendered = unsafe { describe_layout(&*(layout as *const AvChannelLayout)) }
.expect("a standard layout describes");
assert!(
rendered.len() <= smol_bytes::INLINE_CAP,
"{rendered:?} is {} bytes, past the {} the parse bypass assumes",
rendered.len(),
smol_bytes::INLINE_CAP,
);
seen += 1;
}
assert!(seen > 10, "the standard roster should not be nearly empty");
}
#[test]
fn a_long_rendering_is_not_parsed_at_all() {
let long = "FL+FR+FC+LFE+BL+BR+FLC+FRC+BC+SL+SR+TC+TFL+TFC+TFR+TBL+TBC+TBR".repeat(4);
assert!(long.len() > smol_bytes::INLINE_CAP);
assert_eq!(
channel_layout_from_describe(&long),
ChannelLayout::default(),
"an unnameable rendering is absent, and nothing was copied to decide that",
);
assert_eq!(
channel_layout_from_describe("5.1(side)"),
ChannelLayout::Ch5_1
);
}
#[test]
fn an_unnamed_layout_stays_absent_with_its_rendering_in_text() {
let layout = native(ffi::AV_CH_FRONT_LEFT | ffi::AV_CH_FRONT_RIGHT | ffi::AV_CH_TOP_FRONT_LEFT);
assert_eq!(mapped_constant(&layout), None);
let rendering = describe_layout(&layout).expect("a well-formed layout describes");
assert!(
rendering.contains("TFL"),
"FFmpeg should list the channels it cannot name: {rendering:?}"
);
assert_eq!(
channel_layout_from_ffmpeg(&layout).expect("a well-formed layout names"),
ChannelLayout::default(),
"an unnamed layout must land on the absent sentinel"
);
let described =
channel_layout_description_from_ffmpeg(&layout).expect("a well-formed layout describes");
assert_eq!(described.known_kind(), &ChannelLayout::default());
assert_eq!(
described.text(),
rendering.as_str(),
"the rendering is what `text` carries"
);
}
#[test]
fn the_describe_rung_reads_names_and_refuses_everything_else() {
assert_eq!(
channel_layout_from_describe("binaural"),
ChannelLayout::Binaural
);
assert_eq!(
channel_layout_from_describe("5.1.2"),
ChannelLayout::Ch5_1_2
);
assert_eq!(
channel_layout_from_describe("9.1.6"),
ChannelLayout::Ch9_1_6
);
assert_eq!(
channel_layout_from_describe("5.1"),
ChannelLayout::Ch5_1Back
);
assert_eq!(
channel_layout_from_describe("5.1(side)"),
ChannelLayout::Ch5_1
);
assert_eq!(
channel_layout_from_describe("BINAURAL"),
ChannelLayout::Binaural
);
for unnamed in [
"",
"3 channels",
"3 channels (FL+FR+TFL)",
"FL@Left+FR@Right",
"ambisonic 2",
"not-a-layout",
] {
assert_eq!(
channel_layout_from_describe(unnamed),
ChannelLayout::default(),
"{unnamed:?} must stay absent"
);
}
}
}
#[cfg(test)]
mod null_map_tests {
use super::*;
#[test]
fn a_custom_layout_without_a_map_is_refused_before_ffmpeg_is_called() {
let mut layout: ffi::AVChannelLayout = unsafe { std::mem::zeroed() };
layout.order = ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM;
layout.nb_channels = 6;
let described =
unsafe { channel_layout_description_from_raw_ptr(&layout as *const ffi::AVChannelLayout) };
assert_eq!(
described,
Err(ChannelLayoutFault::MalformedCustomMap { channels: 6 }),
);
}
#[test]
fn a_safe_conversion_refuses_a_custom_layout_rather_than_trusting_its_count() {
let mut name = [0 as core::ffi::c_char; 16];
name[0] = b'F' as core::ffi::c_char;
name[1] = b'L' as core::ffi::c_char;
let map = [ffi::AVChannelCustom {
id: ffi::AVChannel::AV_CHAN_FRONT_LEFT,
name,
opaque: core::ptr::null_mut(),
}];
let mut inner: ffi::AVChannelLayout = unsafe { std::mem::zeroed() };
inner.order = ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM;
inner.nb_channels = 2;
inner.u.map = map.as_ptr().cast_mut();
let layout = AvChannelLayout(inner);
assert_eq!(
channel_layout_description_from_ffmpeg(&layout),
Err(ChannelLayoutFault::UnverifiableCustomMap { channels: 2 }),
"the safe description road must refuse a custom layout, not validate it",
);
assert_eq!(
channel_layout_from_ffmpeg(&layout),
Err(ChannelLayoutFault::UnverifiableCustomMap { channels: 2 }),
"and so must the safe naming road, which shares the implementation",
);
}
#[test]
fn a_safe_conversion_still_answers_for_a_native_layout() {
let layout = AvChannelLayout::STEREO;
let described =
channel_layout_description_from_ffmpeg(&layout).expect("a native layout describes");
assert_eq!(described.channels(), 2);
assert_eq!(
channel_layout_from_ffmpeg(&layout).expect("a native layout names"),
ChannelLayout::Stereo,
);
}
#[test]
fn a_non_custom_layout_with_a_broken_shape_is_refused() {
let build = |order: ffi::AVChannelOrder, channels: i32, mask: u64| unsafe {
let mut layout: ffi::AVChannelLayout = std::mem::zeroed();
layout.order = order;
layout.nb_channels = channels;
layout.u.mask = mask;
layout
};
let refused = |layout: &ffi::AVChannelLayout| {
unsafe { layout_preflight(layout as *const ffi::AVChannelLayout) }
};
let huge = build(ffi::AVChannelOrder::AV_CHANNEL_ORDER_AMBISONIC, i32::MAX, 0);
assert_eq!(
refused(&huge),
Err(ChannelLayoutFault::MalformedLayout {
order: ffi::AVChannelOrder::AV_CHANNEL_ORDER_AMBISONIC as i32,
channels: i32::MAX,
}),
);
assert_eq!(
refused(&build(
ffi::AVChannelOrder::AV_CHANNEL_ORDER_AMBISONIC,
5,
0
)),
Ok(()),
"an incomplete order is not an invalid layout",
);
assert_eq!(
refused(&build(
ffi::AVChannelOrder::AV_CHANNEL_ORDER_AMBISONIC,
4,
0
)),
Ok(()),
);
let stereo_mask = ffi::AV_CH_FRONT_LEFT | ffi::AV_CH_FRONT_RIGHT;
assert_eq!(
refused(&build(
ffi::AVChannelOrder::AV_CHANNEL_ORDER_AMBISONIC,
16,
stereo_mask,
)),
Ok(()),
);
for declared in [2, 1] {
assert!(
refused(&build(
ffi::AVChannelOrder::AV_CHANNEL_ORDER_AMBISONIC,
declared,
stereo_mask,
))
.is_err(),
"{declared} channels against a two-channel mask leaves no ambisonic part",
);
}
let lying = build(ffi::AVChannelOrder::AV_CHANNEL_ORDER_NATIVE, 7, stereo_mask);
assert_eq!(
refused(&lying),
Err(ChannelLayoutFault::MalformedLayout {
order: ffi::AVChannelOrder::AV_CHANNEL_ORDER_NATIVE as i32,
channels: 7,
}),
);
assert_eq!(
refused(&build(
ffi::AVChannelOrder::AV_CHANNEL_ORDER_NATIVE,
2,
stereo_mask,
)),
Ok(()),
"the same mask with its true count is a layout FFmpeg names",
);
assert!(refused(&build(ffi::AVChannelOrder::AV_CHANNEL_ORDER_UNSPEC, -1, 0)).is_err(),);
for order in [
ffi::AVChannelOrder::AV_CHANNEL_ORDER_NATIVE,
ffi::AVChannelOrder::AV_CHANNEL_ORDER_AMBISONIC,
ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM,
] {
assert!(
refused(&build(order, 0, 0)).is_err(),
"{order:?} with no channels is not a layout",
);
}
assert_eq!(
refused(&build(ffi::AVChannelOrder::AV_CHANNEL_ORDER_UNSPEC, 0, 0)),
Ok(()),
);
}
#[test]
fn a_custom_layout_with_no_channels_is_not_malformed() {
let mut layout: ffi::AVChannelLayout = unsafe { std::mem::zeroed() };
layout.order = ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM;
let described =
unsafe { channel_layout_description_from_raw_ptr(&layout as *const ffi::AVChannelLayout) };
assert!(
matches!(
described,
Err(ChannelLayoutFault::MalformedCustomMap { channels: 0 })
),
"a zero-channel custom layout carries no map either, and is refused the same way",
);
}
#[test]
fn a_custom_name_without_a_terminator_is_refused() {
let entries = [ffi::AVChannelCustom {
id: ffi::AVChannel::AV_CHAN_FRONT_LEFT,
name: [b'x' as core::ffi::c_char; 16],
opaque: core::ptr::null_mut(),
}];
let mut layout: ffi::AVChannelLayout = unsafe { std::mem::zeroed() };
layout.order = ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM;
layout.nb_channels = 1;
layout.u.map = entries.as_ptr().cast_mut();
let described =
unsafe { channel_layout_description_from_raw_ptr(&layout as *const ffi::AVChannelLayout) };
assert_eq!(
described,
Err(ChannelLayoutFault::MalformedCustomMap { channels: 1 }),
"a correctly sized map is not a describable one if a name never ends",
);
}
#[test]
fn a_map_entry_that_names_no_channel_is_refused() {
let mut name = [0 as core::ffi::c_char; 16];
name[0] = b'F' as core::ffi::c_char;
name[1] = b'L' as core::ffi::c_char;
let entries = [
ffi::AVChannelCustom {
id: ffi::AVChannel::AV_CHAN_FRONT_LEFT,
name,
opaque: core::ptr::null_mut(),
},
ffi::AVChannelCustom {
id: ffi::AVChannel::AV_CHAN_NONE,
name,
opaque: core::ptr::null_mut(),
},
];
let mut layout: ffi::AVChannelLayout = unsafe { std::mem::zeroed() };
layout.order = ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM;
layout.nb_channels = 2;
layout.u.map = entries.as_ptr().cast_mut();
let described =
unsafe { channel_layout_description_from_raw_ptr(&layout as *const ffi::AVChannelLayout) };
assert_eq!(
described,
Err(ChannelLayoutFault::MalformedCustomMap { channels: 2 }),
"an entry naming no channel is a hole in the map, not a channel",
);
let whole = [
entries[0],
ffi::AVChannelCustom {
id: ffi::AVChannel::AV_CHAN_FRONT_RIGHT,
name,
opaque: core::ptr::null_mut(),
},
];
let mut ok_layout: ffi::AVChannelLayout = unsafe { std::mem::zeroed() };
ok_layout.order = ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM;
ok_layout.nb_channels = 2;
ok_layout.u.map = whole.as_ptr().cast_mut();
unsafe { channel_layout_description_from_raw_ptr(&ok_layout as *const ffi::AVChannelLayout) }
.expect("a map with no holes describes");
}
#[test]
fn a_terminated_custom_name_describes() {
let mut name = [0 as core::ffi::c_char; 16];
name[0] = b'F' as core::ffi::c_char;
name[1] = b'L' as core::ffi::c_char;
let entries = [ffi::AVChannelCustom {
id: ffi::AVChannel::AV_CHAN_FRONT_LEFT,
name,
opaque: core::ptr::null_mut(),
}];
let mut layout: ffi::AVChannelLayout = unsafe { std::mem::zeroed() };
layout.order = ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM;
layout.nb_channels = 1;
layout.u.map = entries.as_ptr().cast_mut();
let described =
unsafe { channel_layout_description_from_raw_ptr(&layout as *const ffi::AVChannelLayout) }
.expect("a terminated name is describable");
assert_eq!(described.channels(), 1);
}
#[test]
fn text_is_decoded_whole_and_fallibly() {
for raw in [
&b""[..],
&b"FL"[..],
&b"\xff"[..],
&b"\xff\xfe\xfd"[..],
&b"ok\xffafter"[..],
&b"\xe2\x82"[..],
&[0xffu8; 16][..],
b"FrontLeftSurrnd",
b"5.1(side)",
b"FL+FR+FC+LFE+BL+BR+SL+SR",
&b"a very long rendering\xff\xff that is not text either"[..],
] {
let decoded = std::string::String::from_utf8_lossy(raw);
assert_eq!(
decode_text(raw)
.expect("an allocator that is not refusing")
.as_str(),
decoded.as_ref(),
"{raw:?} must be carried exactly as the lossy decoder reads it, whatever its length",
);
}
}
}