use core::{ffi::c_char, slice, str::FromStr};
use ffmpeg_next::{ChannelLayout as AvChannelLayout, ffi};
use mediaframe::audio::{ChannelLayout, ChannelLayoutDescription, ChannelOrder, ChannelSpec};
use smol_str::SmolStr;
use std::vec::Vec;
pub fn channel_layout_from_ffmpeg(value: &AvChannelLayout) -> ChannelLayout {
mapped_constant(value).unwrap_or_else(|| channel_layout_from_describe(&describe_layout(value)))
}
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 {
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,
}
}
pub fn channel_layout_description_from_ffmpeg(value: &AvChannelLayout) -> ChannelLayoutDescription {
unsafe { channel_layout_description_from_raw_ptr(&value.0 as *const ffi::AVChannelLayout) }
}
pub unsafe fn channel_layout_description_from_raw_ptr(
ptr: *const ffi::AVChannelLayout,
) -> ChannelLayoutDescription {
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 order {
ChannelOrder::Native | ChannelOrder::Ambisonic => {
let mask = unsafe { (*ptr).u.mask };
if mask != 0 { Some(mask) } else { None }
}
_ => None,
};
let (known_kind, text) = if matches!(order, ChannelOrder::Unspecified) {
(ChannelLayout::default(), SmolStr::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) };
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)
}
unsafe fn custom_channels_raw(
ptr: *const ffi::AVChannelLayout,
order: ChannelOrder,
) -> Vec<ChannelSpec> {
use core::ptr::{addr_of, read_unaligned};
if !matches!(order, ChannelOrder::Custom) {
return Vec::new();
}
let count = unsafe { (*ptr).nb_channels }.max(0) as usize;
if count == 0 {
return Vec::new();
}
let map_ptr = unsafe { (*ptr).u.map };
if map_ptr.is_null() {
return Vec::new();
}
let mut out = Vec::with_capacity(count);
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));
}
out
}
unsafe fn custom_channel_label_raw(entry_ptr: *const ffi::AVChannelCustom) -> SmolStr {
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 SmolStr::default();
}
SmolStr::new(std::string::String::from_utf8_lossy(&bytes[..end]))
}
#[allow(dead_code)]
fn custom_channels(layout: &AvChannelLayout) -> Vec<ChannelSpec> {
use core::ptr::{addr_of, read_unaligned};
let order_raw = unsafe { read_unaligned(addr_of!(layout.0.order) as *const i32) };
if order_raw != ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM as i32 {
return Vec::new();
}
let count = layout.0.nb_channels.max(0) as usize;
if count == 0 {
return Vec::new();
}
let ptr = unsafe { layout.0.u.map };
if ptr.is_null() {
return Vec::new();
}
let slice_ref = unsafe { slice::from_raw_parts(ptr, count) };
slice_ref
.iter()
.enumerate()
.map(|(index, channel)| {
let raw_id = unsafe { read_unaligned(addr_of!(channel.id) as *const i32) };
ChannelSpec::new(index as u32, raw_id as u32).with_label(custom_channel_label(channel))
})
.collect()
}
fn custom_channel_label(channel: &ffi::AVChannelCustom) -> SmolStr {
let bytes =
unsafe { slice::from_raw_parts(channel.name.as_ptr() as *const u8, channel.name.len()) };
let end = bytes
.iter()
.position(|byte| *byte == 0)
.unwrap_or(bytes.len());
if end == 0 {
return SmolStr::default();
}
SmolStr::new(std::string::String::from_utf8_lossy(&bytes[..end]))
}
fn describe_layout(layout: &AvChannelLayout) -> SmolStr {
let mut buf = std::vec![0 as c_char; 128];
let mut needed =
unsafe { ffi::av_channel_layout_describe(&layout.0 as *const _, buf.as_mut_ptr(), buf.len()) };
if needed < 0 {
return SmolStr::default();
}
if needed as usize >= buf.len() {
buf.resize(needed as usize + 1, 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 SmolStr::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);
if end == 0 {
return SmolStr::default();
}
SmolStr::new(std::string::String::from_utf8_lossy(&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).as_str(),
slug,
"FFmpeg must name {slug} for the rung to have a word to read"
);
assert_eq!(
channel_layout_from_ffmpeg(&layout),
expected,
"{slug} must reach its named variant through the rung"
);
let described = channel_layout_description_from_ffmpeg(&layout);
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).as_str(), "5.1.4");
assert_eq!(
channel_layout_from_ffmpeg(&layout),
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), expected, "{name}");
}
}
#[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);
assert!(
rendering.contains("TFL"),
"FFmpeg should list the channels it cannot name: {rendering:?}"
);
assert_eq!(
channel_layout_from_ffmpeg(&layout),
ChannelLayout::default(),
"an unnamed layout must land on the absent sentinel"
);
let described = channel_layout_description_from_ffmpeg(&layout);
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"
);
}
}
}