mediadecode_ffmpeg/channel_layout.rs
1//! Conversions from FFmpeg's [`ffmpeg_next::ChannelLayout`] /
2//! [`ffmpeg_next::ffi::AVChannelOrder`] to the channel-layout vocabulary
3//! [`mediaframe`] owns ([`ChannelLayout`], [`ChannelOrder`],
4//! [`ChannelSpec`], [`ChannelLayoutDescription`]).
5//!
6//! These live as **free functions** (not `From` trait impls) because of
7//! Rust's orphan rule: this crate owns neither `From` nor
8//! `mediaframe::audio::*`, so we can't write the `impl` here. Calling
9//! `mediadecode_ffmpeg::channel_layout_description_from_ffmpeg(layout)`
10//! is the ergonomic boundary instead.
11//!
12//! FFmpeg's own type is imported as [`AvChannelLayout`] so the name
13//! [`ChannelLayout`] can stay with the vocabulary these functions
14//! produce.
15
16use core::{ffi::c_char, slice, str::FromStr};
17
18use ffmpeg_next::{ChannelLayout as AvChannelLayout, ffi};
19use mediaframe::audio::{ChannelLayout, ChannelLayoutDescription, ChannelOrder, ChannelSpec};
20use smol_str::SmolStr;
21use std::vec::Vec;
22
23/// Maps an FFmpeg [`AvChannelLayout`] to the named
24/// [`ChannelLayout`] vocabulary.
25///
26/// Two rungs, in order:
27///
28/// 1. the **constant-arm table** — exactly `ffmpeg_next`'s
29/// `ChannelLayout` constant set, compared through
30/// `av_channel_layout_compare`;
31/// 2. the **describe rung** — for a layout that falls off the table,
32/// FFmpeg names it via `av_channel_layout_describe` and that name
33/// goes through [`ChannelLayout`]'s own total door (`FromStr`).
34///
35/// The second rung is what makes `binaural` / `5.1.2` / `9.1.6`
36/// reachable: [`ChannelLayout`] names all three, `ffmpeg_next` 9.0.0
37/// mints no constant for any of them, so the table alone can never
38/// produce them. It is also why a layout a *later* FFmpeg adds is
39/// reachable with no edit here, as long as the vocabulary already
40/// names it — FFmpeg speaks the name, the vocabulary reads the word,
41/// one source.
42///
43/// Returns [`ChannelLayout::default`] — the `Other("")` absent sentinel
44/// — when neither rung names the layout. The rendering itself is not
45/// smuggled into `Other`: an unrecognised layout stays *absent*, and
46/// [`ChannelLayoutDescription::text`] is where its FFmpeg rendering
47/// lives.
48pub fn channel_layout_from_ffmpeg(value: &AvChannelLayout) -> ChannelLayout {
49 mapped_constant(value).unwrap_or_else(|| channel_layout_from_describe(&describe_layout(value)))
50}
51
52/// The constant-arm table — the first and authoritative rung of
53/// [`channel_layout_from_ffmpeg`]. `None` means the layout fell off the
54/// table and the caller should try the describe rung.
55///
56/// The arm list is exactly `ffmpeg_next`'s `ChannelLayout` constant set:
57/// its `_7POINT1_TOP_BACK` is a `#define` alias of
58/// `AV_CH_LAYOUT_5POINT1POINT2_BACK` and so has no arm of its own, and
59/// its `BINAURAL` / `_5POINT1POINT2` / `_9POINT1POINT6` siblings — which
60/// [`ChannelLayout`] does name — have no constant to match against.
61fn mapped_constant(value: &AvChannelLayout) -> Option<ChannelLayout> {
62 let named = match () {
63 () if value.eq(&AvChannelLayout::MONO) => ChannelLayout::Mono,
64 () if value.eq(&AvChannelLayout::STEREO) => ChannelLayout::Stereo,
65 () if value.eq(&AvChannelLayout::STEREO_DOWNMIX) => ChannelLayout::StereoDownmix,
66 () if value.eq(&AvChannelLayout::SURROUND) => ChannelLayout::Ch3_0,
67 () if value.eq(&AvChannelLayout::QUAD) => ChannelLayout::Quad,
68 () if value.eq(&AvChannelLayout::HEXAGONAL) => ChannelLayout::Hexagonal,
69 () if value.eq(&AvChannelLayout::OCTAGONAL) => ChannelLayout::Octagonal,
70 () if value.eq(&AvChannelLayout::HEXADECAGONAL) => ChannelLayout::Hexadecagonal,
71 () if value.eq(&AvChannelLayout::CUBE) => ChannelLayout::Cube,
72 () if value.eq(&AvChannelLayout::_2POINT1) => ChannelLayout::Ch2_1,
73 () if value.eq(&AvChannelLayout::_2_1) => ChannelLayout::Ch3_0Back,
74 () if value.eq(&AvChannelLayout::_2_2) => ChannelLayout::QuadSide,
75 () if value.eq(&AvChannelLayout::_3POINT1) => ChannelLayout::Ch3_1,
76 () if value.eq(&AvChannelLayout::_3POINT1POINT2) => ChannelLayout::Ch3_1_2,
77 () if value.eq(&AvChannelLayout::_4POINT0) => ChannelLayout::Ch4_0,
78 () if value.eq(&AvChannelLayout::_4POINT1) => ChannelLayout::Ch4_1,
79 () if value.eq(&AvChannelLayout::_5POINT0) => ChannelLayout::Ch5_0,
80 () if value.eq(&AvChannelLayout::_5POINT0_BACK) => ChannelLayout::Ch5_0Back,
81 () if value.eq(&AvChannelLayout::_5POINT1) => ChannelLayout::Ch5_1,
82 () if value.eq(&AvChannelLayout::_5POINT1_BACK) => ChannelLayout::Ch5_1Back,
83 () if value.eq(&AvChannelLayout::_5POINT1POINT2_BACK) => ChannelLayout::Ch5_1_2Back,
84 () if value.eq(&AvChannelLayout::_5POINT1POINT4_BACK) => ChannelLayout::Ch5_1_4Back,
85 () if value.eq(&AvChannelLayout::_6POINT0) => ChannelLayout::Ch6_0,
86 () if value.eq(&AvChannelLayout::_6POINT0_FRONT) => ChannelLayout::Ch6_0Front,
87 () if value.eq(&AvChannelLayout::_6POINT1) => ChannelLayout::Ch6_1,
88 () if value.eq(&AvChannelLayout::_6POINT1_BACK) => ChannelLayout::Ch6_1Back,
89 () if value.eq(&AvChannelLayout::_6POINT1_FRONT) => ChannelLayout::Ch6_1Front,
90 () if value.eq(&AvChannelLayout::_7POINT0) => ChannelLayout::Ch7_0,
91 () if value.eq(&AvChannelLayout::_7POINT0_FRONT) => ChannelLayout::Ch7_0Front,
92 () if value.eq(&AvChannelLayout::_7POINT1) => ChannelLayout::Ch7_1,
93 () if value.eq(&AvChannelLayout::_7POINT1_WIDE) => ChannelLayout::Ch7_1Wide,
94 () if value.eq(&AvChannelLayout::_7POINT1_WIDE_BACK) => ChannelLayout::Ch7_1WideBack,
95 () if value.eq(&AvChannelLayout::_7POINT1POINT2) => ChannelLayout::Ch7_1_2,
96 () if value.eq(&AvChannelLayout::_7POINT1POINT4_BACK) => ChannelLayout::Ch7_1_4Back,
97 () if value.eq(&AvChannelLayout::_7POINT2POINT3) => ChannelLayout::Ch7_2_3,
98 () if value.eq(&AvChannelLayout::_9POINT1POINT4_BACK) => ChannelLayout::Ch9_1_4Back,
99 () if value.eq(&AvChannelLayout::_22POINT2) => ChannelLayout::Ch22_2,
100 () => return None,
101 };
102 Some(named)
103}
104
105/// The describe rung: read FFmpeg's own rendering of a layout
106/// (`av_channel_layout_describe`, e.g. `"binaural"`, `"5.1(side)"`)
107/// through [`ChannelLayout`]'s total `FromStr` door.
108///
109/// A **named** variant wins. Anything the vocabulary does not name —
110/// `FromStr`'s `Other` escape, which is where `"3 channels (FL+FR+TFL)"`
111/// and every custom-order rendering land — collapses to
112/// [`ChannelLayout::default`], the absent sentinel. That collapse is
113/// deliberate: `known_kind` answers *which named layout is this*, and
114/// "none of them" is `Other("")`; the rendering is already carried
115/// verbatim by [`ChannelLayoutDescription::text`], so letting it ride
116/// `Other` too would put a second, differently-shaped copy of the same
117/// string in the same struct.
118fn channel_layout_from_describe(rendered: &str) -> ChannelLayout {
119 ChannelLayout::from_str(rendered)
120 .ok()
121 .filter(|layout| !matches!(layout, ChannelLayout::Other(_)))
122 .unwrap_or_default()
123}
124
125/// Maps FFmpeg's [`AVChannelOrder`](ffi::AVChannelOrder) to the
126/// [`ChannelOrder`] tag.
127pub fn channel_order_from_ffmpeg(value: ffi::AVChannelOrder) -> ChannelOrder {
128 // Compare via integer rather than enum-matching: the caller often
129 // sources `value` from raw FFmpeg memory (`AVChannelLayout.order`),
130 // and an unknown variant would already be UB before reaching this
131 // function. Going through `as i32` here is sound because the caller
132 // is responsible for the up-conversion path; for the raw-pointer
133 // path use [`channel_order_from_raw`].
134 channel_order_from_raw(value as i32)
135}
136
137/// Variant of [`channel_order_from_ffmpeg`] that takes the raw integer
138/// directly. Use this when the caller has just read
139/// `AVChannelLayout.order` from FFmpeg memory and doesn't want to
140/// risk constructing an invalid bindgen enum value first.
141pub fn channel_order_from_raw(raw: i32) -> ChannelOrder {
142 match raw {
143 x if x == ffi::AVChannelOrder::AV_CHANNEL_ORDER_NATIVE as i32 => ChannelOrder::Native,
144 x if x == ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM as i32 => ChannelOrder::Custom,
145 x if x == ffi::AVChannelOrder::AV_CHANNEL_ORDER_AMBISONIC as i32 => ChannelOrder::Ambisonic,
146 _ => ChannelOrder::Unspecified,
147 }
148}
149
150/// Builds a fully-populated [`ChannelLayoutDescription`] from an FFmpeg
151/// [`AvChannelLayout`].
152///
153/// - Native / Ambisonic layouts populate `native_mask` from
154/// [`AvChannelLayout::bits`] (clearing it to `None` if zero).
155/// - Custom layouts populate `custom_channels` from FFmpeg's per-channel
156/// list (`AVChannelLayout.u.map`), with each label drawn from
157/// `AVChannelCustom.name`.
158/// - `text` carries the result of `av_channel_layout_describe`
159/// (FFmpeg's human-readable rendering — e.g. `"5.1(side)"`).
160/// - `known_kind` runs [`channel_layout_from_ffmpeg`]'s two rungs
161/// against that same single rendering: constant table first, then
162/// the describe rung.
163pub fn channel_layout_description_from_ffmpeg(value: &AvChannelLayout) -> ChannelLayoutDescription {
164 // SAFETY: `value` is a live reference; the inner `AVChannelLayout`
165 // stays valid for the duration of this call. We hand the raw
166 // address into the pointer-based variant which is the canonical
167 // implementation (avoids forming `&AVChannelLayout` over a
168 // potentially-invalid `order` discriminant).
169 unsafe { channel_layout_description_from_raw_ptr(&value.0 as *const ffi::AVChannelLayout) }
170}
171
172/// Pointer variant of [`channel_layout_description_from_ffmpeg`].
173/// Safe-API callers that already hold a `&AvChannelLayout` should prefer
174/// that function; the pointer form exists so the convert path
175/// (which never forms `&AVFrame`) can pass `addr_of!((*av_frame).ch_layout)`
176/// straight through without materializing a typed reference.
177///
178/// # Safety
179/// `ptr` must be a live `*const AVChannelLayout` for the duration of
180/// this call. The function reads `order` raw, then `nb_channels`,
181/// then either `u.mask` (NATIVE / AMBISONIC) or `u.map`
182/// (CUSTOM) — only after the order discriminant has been validated.
183/// It never forms a `&AVChannelLayout` reference.
184pub unsafe fn channel_layout_description_from_raw_ptr(
185 ptr: *const ffi::AVChannelLayout,
186) -> ChannelLayoutDescription {
187 use core::ptr::{addr_of, read_unaligned};
188 // Read `order` as a raw integer first — never let Rust assume
189 // the field is a valid `AVChannelOrder`.
190 // SAFETY: `ptr` is a valid `*const AVChannelLayout`; `addr_of!`
191 // computes the field address without forming a reference; reading
192 // as `i32` matches the bindgen enum's `c_int` storage.
193 let order_raw = unsafe { read_unaligned(addr_of!((*ptr).order) as *const i32) };
194 let order = channel_order_from_raw(order_raw);
195 let nb_channels = unsafe { (*ptr).nb_channels };
196
197 // Native / Ambisonic carry the bitmask in the union. Only read
198 // `u.mask` after the order is validated so we don't trip on an
199 // unknown order writing into a future variant of the union.
200 let native_mask = match order {
201 ChannelOrder::Native | ChannelOrder::Ambisonic => {
202 // SAFETY: `u.mask` is the union variant for NATIVE/AMBISONIC.
203 let mask = unsafe { (*ptr).u.mask };
204 if mask != 0 { Some(mask) } else { None }
205 }
206 _ => None,
207 };
208
209 // Build name / rendering through ffmpeg-next helpers. They take
210 // `&AvChannelLayout` (which is `repr(transparent)` over
211 // `AVChannelLayout`), but at this point we've already validated
212 // `order`, so forming the reference is sound: the only enum-typed
213 // field in `AVChannelLayout` is `order`, and it now holds a value
214 // that came back from `channel_order_from_raw` with the
215 // unknown bucket folded into a known variant — but the *underlying
216 // struct* still has the original raw bytes. We can't form `&AVChannelLayout`
217 // over an unknown order without UB, so for those helpers we
218 // explicitly only call them when order is one of the known variants.
219 let (known_kind, text) = if matches!(order, ChannelOrder::Unspecified) {
220 (ChannelLayout::default(), SmolStr::default())
221 } else {
222 // SAFETY: `order` is one of {Native, Custom, Ambisonic} — all of
223 // which are valid `AVChannelOrder` discriminants present in our
224 // bindgen output, so `&*ptr` is sound to form here.
225 let layout_ref = unsafe { &*(ptr as *const AvChannelLayout) };
226 let text = describe_layout(layout_ref);
227 // Constant-arm table first, exactly as in
228 // `channel_layout_from_ffmpeg`; the rendering is consulted only
229 // when the layout falls off it. Describing once and feeding both
230 // fields from that one string keeps `known_kind` and `text`
231 // answering from the same FFmpeg call.
232 let known_kind =
233 mapped_constant(layout_ref).unwrap_or_else(|| channel_layout_from_describe(&text));
234 (known_kind, text)
235 };
236 let custom_channels_vec = unsafe { custom_channels_raw(ptr, order) };
237
238 ChannelLayoutDescription::new(nb_channels.max(0) as u32)
239 .with_order(order)
240 .with_known_kind(known_kind)
241 .with_native_mask(native_mask)
242 .with_custom_channels(custom_channels_vec)
243 .with_text(text)
244}
245
246/// Pointer-form of `custom_channels`. `order` must be the result of
247/// reading `(*ptr).order` as `i32` and folding through
248/// [`channel_order_from_raw`]; this skips re-reading it.
249///
250/// # Safety
251/// `ptr` must be a live `*const AVChannelLayout`. Reads only fields
252/// (`u.map`, `nb_channels`, and the per-channel array) — no `&AVChannelLayout`
253/// reference is ever formed.
254unsafe fn custom_channels_raw(
255 ptr: *const ffi::AVChannelLayout,
256 order: ChannelOrder,
257) -> Vec<ChannelSpec> {
258 use core::ptr::{addr_of, read_unaligned};
259 if !matches!(order, ChannelOrder::Custom) {
260 return Vec::new();
261 }
262 let count = unsafe { (*ptr).nb_channels }.max(0) as usize;
263 if count == 0 {
264 return Vec::new();
265 }
266 // SAFETY: The `u` field is a union; reading `.map` is sound when
267 // `order == CUSTOM` per FFmpeg's documented contract. Guard
268 // explicitly for null.
269 let map_ptr = unsafe { (*ptr).u.map };
270 if map_ptr.is_null() {
271 return Vec::new();
272 }
273 // Iterate the AVChannelCustom array via raw pointers — never form
274 // `&[AVChannelCustom]` or `&AVChannelCustom`, because each entry
275 // contains `id: AVChannel`, a bindgen enum. If FFmpeg writes an
276 // unknown channel id (version skew / hostile decoder), the
277 // reference itself would be UB before the raw `id` read could
278 // sanitize it.
279 let mut out = Vec::with_capacity(count);
280 for index in 0..count {
281 // SAFETY: `map_ptr` points to `count == nb_channels` valid
282 // `AVChannelCustom` entries per FFmpeg's contract; `index < count`,
283 // so `entry_ptr` lies inside the allocation.
284 let entry_ptr: *const ffi::AVChannelCustom = unsafe { map_ptr.add(index) };
285 // SAFETY: `entry_ptr` is a valid pointer; `addr_of!((*p).field)`
286 // computes the field address without forming a reference.
287 let raw_id = unsafe { read_unaligned(addr_of!((*entry_ptr).id) as *const i32) };
288 let label = unsafe { custom_channel_label_raw(entry_ptr) };
289 out.push(ChannelSpec::new(index as u32, raw_id as u32).with_label(label));
290 }
291 out
292}
293
294/// Pointer-form of `custom_channel_label` — never forms
295/// `&AVChannelCustom`, since the struct contains an enum-typed `id`.
296///
297/// # Safety
298/// `entry_ptr` must be a live `*const AVChannelCustom`.
299unsafe fn custom_channel_label_raw(entry_ptr: *const ffi::AVChannelCustom) -> SmolStr {
300 use core::ptr::addr_of;
301 // SAFETY: `name: [c_char; 16]` is an inline byte array — no
302 // validity invariant beyond initialization (FFmpeg guarantees that).
303 // `addr_of!` computes the address; we then re-interpret as `*const u8`
304 // for UTF-8 lossy decoding.
305 let name_ptr = unsafe { addr_of!((*entry_ptr).name) } as *const u8;
306 // SAFETY: `name` is exactly 16 bytes wide.
307 let bytes = unsafe { slice::from_raw_parts(name_ptr, 16) };
308 let end = bytes
309 .iter()
310 .position(|byte| *byte == 0)
311 .unwrap_or(bytes.len());
312 if end == 0 {
313 return SmolStr::default();
314 }
315 SmolStr::new(std::string::String::from_utf8_lossy(&bytes[..end]))
316}
317
318#[allow(dead_code)]
319fn custom_channels(layout: &AvChannelLayout) -> Vec<ChannelSpec> {
320 // Same raw-integer check as in `channel_layout_description_from_ffmpeg`:
321 // never let Rust form an `AVChannelOrder` value from runtime data
322 // before we've validated its discriminant.
323 use core::ptr::{addr_of, read_unaligned};
324 // SAFETY: `layout.0` is the inner `AVChannelLayout`; reading the
325 // `order` field as `i32` matches the bindgen enum's storage.
326 let order_raw = unsafe { read_unaligned(addr_of!(layout.0.order) as *const i32) };
327 if order_raw != ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM as i32 {
328 return Vec::new();
329 }
330 let count = layout.0.nb_channels.max(0) as usize;
331 if count == 0 {
332 return Vec::new();
333 }
334 // SAFETY: The `u` field is a union; reading `.map` is sound when
335 // `order == CUSTOM` per FFmpeg's documented contract. The pointer
336 // may still be null on a malformed layout — guard explicitly.
337 let ptr = unsafe { layout.0.u.map };
338 if ptr.is_null() {
339 return Vec::new();
340 }
341 // SAFETY: AVChannelLayout's contract says `.u.map` points to
342 // `nb_channels` valid `AVChannelCustom` entries when order == CUSTOM.
343 let slice_ref = unsafe { slice::from_raw_parts(ptr, count) };
344 slice_ref
345 .iter()
346 .enumerate()
347 .map(|(index, channel)| {
348 // Read `channel.id` as raw `i32` to avoid constructing an
349 // invalid `AVChannel` enum from a value we don't recognize.
350 // SAFETY: `channel` is a valid `&AVChannelCustom`; `id` has the
351 // bindgen enum layout (c_int).
352 let raw_id = unsafe { read_unaligned(addr_of!(channel.id) as *const i32) };
353 ChannelSpec::new(index as u32, raw_id as u32).with_label(custom_channel_label(channel))
354 })
355 .collect()
356}
357
358fn custom_channel_label(channel: &ffi::AVChannelCustom) -> SmolStr {
359 // SAFETY: AVChannelCustom.name is a fixed-size [c_char; 16] inline
360 // buffer. Re-interpreting as bytes for UTF-8 lossy decoding is sound.
361 let bytes =
362 unsafe { slice::from_raw_parts(channel.name.as_ptr() as *const u8, channel.name.len()) };
363 let end = bytes
364 .iter()
365 .position(|byte| *byte == 0)
366 .unwrap_or(bytes.len());
367 if end == 0 {
368 return SmolStr::default();
369 }
370 SmolStr::new(std::string::String::from_utf8_lossy(&bytes[..end]))
371}
372
373/// Renders a layout the way FFmpeg names it (`av_channel_layout_describe`).
374fn describe_layout(layout: &AvChannelLayout) -> SmolStr {
375 // `av_channel_layout_describe` returns the number of bytes needed
376 // (excluding the NUL terminator). Start with a 128-byte buffer —
377 // comfortably bigger than every named layout — and grow once if it
378 // wasn't enough. Use `c_char` for portability (signed on
379 // x86/aarch64-Apple, unsigned on aarch64-Linux).
380 let mut buf = std::vec![0 as c_char; 128];
381 let mut needed =
382 unsafe { ffi::av_channel_layout_describe(&layout.0 as *const _, buf.as_mut_ptr(), buf.len()) };
383 if needed < 0 {
384 return SmolStr::default();
385 }
386 if needed as usize >= buf.len() {
387 buf.resize(needed as usize + 1, 0 as c_char);
388 needed = unsafe {
389 ffi::av_channel_layout_describe(&layout.0 as *const _, buf.as_mut_ptr(), buf.len())
390 };
391 if needed < 0 {
392 return SmolStr::default();
393 }
394 }
395 // SAFETY: buf is heap-allocated, NUL-terminated by FFmpeg's contract.
396 let bytes = unsafe { slice::from_raw_parts(buf.as_ptr() as *const u8, buf.len()) };
397 let end = bytes
398 .iter()
399 .position(|byte| *byte == 0)
400 .unwrap_or(needed as usize);
401 if end == 0 {
402 return SmolStr::default();
403 }
404 SmolStr::new(std::string::String::from_utf8_lossy(&bytes[..end]))
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410
411 /// Builds a NATIVE-order layout from a channel mask, the way a decoder
412 /// hands one over. This is how a layout `ffmpeg_next` mints no constant
413 /// for can be reached at all: `av_channel_layout_from_mask` fills in the
414 /// order and the channel count, so nothing about the value is
415 /// hand-forged.
416 fn native(mask: u64) -> AvChannelLayout {
417 // SAFETY: an all-zero `AVChannelLayout` is `AV_CHANNEL_ORDER_UNSPEC`
418 // with no channels — a valid value, and the same starting point
419 // `ffmpeg_next`'s own `ChannelLayout::default` uses. The constructor
420 // then overwrites every field.
421 let mut raw: ffi::AVChannelLayout = unsafe { core::mem::zeroed() };
422 // SAFETY: `raw` is a live, writable `AVChannelLayout`.
423 let rc = unsafe { ffi::av_channel_layout_from_mask(&mut raw, mask) };
424 assert_eq!(rc, 0, "av_channel_layout_from_mask({mask:#x}) failed");
425 AvChannelLayout(raw)
426 }
427
428 /// `AV_CH_LAYOUT_BINAURAL`, spelled the way the FFmpeg header spells it
429 /// (`1ULL << AV_CHAN_BINAURAL_*`). The composed `AV_CH_BINAURAL_LEFT` /
430 /// `_RIGHT` macros do not survive bindgen's macro evaluation, but the
431 /// `AVChannel` enum they shift by does, so the mask is still derived
432 /// from FFmpeg's own numbers rather than typed out.
433 fn binaural_mask() -> u64 {
434 (1u64 << ffi::AVChannel::AV_CHAN_BINAURAL_LEFT as u64)
435 | (1u64 << ffi::AVChannel::AV_CHAN_BINAURAL_RIGHT as u64)
436 }
437
438 /// The three layouts [`ChannelLayout`] names but `ffmpeg_next` 9.0.0
439 /// mints no constant for. The constant table cannot reach them by
440 /// construction; the describe rung does, because FFmpeg's own layout
441 /// map names all three and the vocabulary reads that word.
442 #[test]
443 fn orphan_layouts_are_named_through_the_describe_rung() {
444 let cases = [
445 (binaural_mask(), "binaural", ChannelLayout::Binaural),
446 (
447 ffi::AV_CH_LAYOUT_5POINT1 | ffi::AV_CH_TOP_FRONT_LEFT | ffi::AV_CH_TOP_FRONT_RIGHT,
448 "5.1.2",
449 ChannelLayout::Ch5_1_2,
450 ),
451 (
452 ffi::AV_CH_LAYOUT_9POINT1POINT4_BACK | ffi::AV_CH_TOP_SIDE_LEFT | ffi::AV_CH_TOP_SIDE_RIGHT,
453 "9.1.6",
454 ChannelLayout::Ch9_1_6,
455 ),
456 ];
457 for (mask, slug, expected) in cases {
458 let layout = native(mask);
459 assert_eq!(
460 mapped_constant(&layout),
461 None,
462 "{slug} must fall off the constant table — that is what makes it an orphan"
463 );
464 assert_eq!(
465 describe_layout(&layout).as_str(),
466 slug,
467 "FFmpeg must name {slug} for the rung to have a word to read"
468 );
469 assert_eq!(
470 channel_layout_from_ffmpeg(&layout),
471 expected,
472 "{slug} must reach its named variant through the rung"
473 );
474
475 let described = channel_layout_description_from_ffmpeg(&layout);
476 assert_eq!(
477 described.known_kind(),
478 &expected,
479 "{slug} must be named on the description path too"
480 );
481 assert_eq!(described.text(), slug, "{slug} rendering rides `text`");
482 }
483 }
484
485 /// FFmpeg 9's actual `5.1.4`: the *side*-surround mask
486 /// (`FL+FR+FC+LFE+SL+SR` plus the four heights), which its layout map
487 /// names and no constant here reaches.
488 ///
489 /// `ffmpeg_sys_next` 9.0.0 bundles a `channel_layout_fixed.h` that
490 /// `#undef`s FFmpeg's layout macros and re-declares them as C
491 /// constants, and its `AV_CH_LAYOUT_5POINT1POINT4_BACK` still carries
492 /// FFmpeg 8's *back*-surround formula. So `ffmpeg_next`'s
493 /// `_5POINT1POINT4_BACK` constant — the one the table compares against
494 /// — is a mask FFmpeg 9 no longer names, and the mask FFmpeg 9 *does*
495 /// name has no constant at all. This is the ruling's "a layout the
496 /// vocabulary already names is reachable with zero adapter edits",
497 /// arriving earlier than expected.
498 ///
499 /// Asserted through the public entry point alone, deliberately: if the
500 /// upstream shim is ever refreshed the constant table will start
501 /// answering this mask itself, and `5.1.4` must come out named either
502 /// way.
503 #[test]
504 fn ffmpeg_nines_own_5_1_4_is_named() {
505 let layout = native(
506 ffi::AV_CH_LAYOUT_5POINT1
507 | ffi::AV_CH_TOP_FRONT_LEFT
508 | ffi::AV_CH_TOP_FRONT_RIGHT
509 | ffi::AV_CH_TOP_BACK_LEFT
510 | ffi::AV_CH_TOP_BACK_RIGHT,
511 );
512 assert_eq!(describe_layout(&layout).as_str(), "5.1.4");
513 assert_eq!(
514 channel_layout_from_ffmpeg(&layout),
515 ChannelLayout::Ch5_1_4Back
516 );
517 }
518
519 /// The constant table is the first rung and answers alone.
520 ///
521 /// `Some` here *is* the bypass proof: [`channel_layout_from_ffmpeg`] is
522 /// `mapped_constant(..).unwrap_or_else(<describe rung>)`, and
523 /// `unwrap_or_else` does not evaluate its closure on `Some` — so a
524 /// mapped constant never renders, never parses, and cannot be
525 /// re-answered by a word.
526 ///
527 /// The sample is the crossed-slug family (where FFmpeg qualifies the
528 /// *side* layout in one place and the *back* one in another, so a
529 /// name-based answer is the one that could plausibly differ), plus the
530 /// `_7POINT1_TOP_BACK` alias that shares `_5POINT1POINT2_BACK`'s mask
531 /// and therefore has no arm of its own.
532 #[test]
533 fn mapped_constants_are_answered_by_the_table_alone() {
534 let table = [
535 ("MONO", AvChannelLayout::MONO, ChannelLayout::Mono),
536 ("STEREO", AvChannelLayout::STEREO, ChannelLayout::Stereo),
537 (
538 "STEREO_DOWNMIX",
539 AvChannelLayout::STEREO_DOWNMIX,
540 ChannelLayout::StereoDownmix,
541 ),
542 ("SURROUND", AvChannelLayout::SURROUND, ChannelLayout::Ch3_0),
543 ("_5POINT0", AvChannelLayout::_5POINT0, ChannelLayout::Ch5_0),
544 (
545 "_5POINT0_BACK",
546 AvChannelLayout::_5POINT0_BACK,
547 ChannelLayout::Ch5_0Back,
548 ),
549 ("_5POINT1", AvChannelLayout::_5POINT1, ChannelLayout::Ch5_1),
550 (
551 "_5POINT1_BACK",
552 AvChannelLayout::_5POINT1_BACK,
553 ChannelLayout::Ch5_1Back,
554 ),
555 (
556 "_5POINT1POINT2_BACK",
557 AvChannelLayout::_5POINT1POINT2_BACK,
558 ChannelLayout::Ch5_1_2Back,
559 ),
560 (
561 "_7POINT1_TOP_BACK",
562 AvChannelLayout::_7POINT1_TOP_BACK,
563 ChannelLayout::Ch5_1_2Back,
564 ),
565 (
566 "_7POINT1_WIDE",
567 AvChannelLayout::_7POINT1_WIDE,
568 ChannelLayout::Ch7_1Wide,
569 ),
570 (
571 "_7POINT1_WIDE_BACK",
572 AvChannelLayout::_7POINT1_WIDE_BACK,
573 ChannelLayout::Ch7_1WideBack,
574 ),
575 (
576 "_22POINT2",
577 AvChannelLayout::_22POINT2,
578 ChannelLayout::Ch22_2,
579 ),
580 ];
581 for (name, layout, expected) in table {
582 assert_eq!(
583 mapped_constant(&layout),
584 Some(expected.clone()),
585 "{name} must be answered by the constant table, not by a rendering"
586 );
587 assert_eq!(channel_layout_from_ffmpeg(&layout), expected, "{name}");
588 }
589 }
590
591 /// A layout nobody names stays *absent*. The rung upgrades the sentinel
592 /// to a named variant or leaves it alone; it never smuggles FFmpeg's
593 /// rendering into `known_kind`'s escape, because `text` already carries
594 /// that rendering verbatim.
595 #[test]
596 fn an_unnamed_layout_stays_absent_with_its_rendering_in_text() {
597 // FL+FR+TFL: a native mask FFmpeg's layout map does not carry, so
598 // `av_channel_layout_describe` falls back to listing the channels.
599 let layout = native(ffi::AV_CH_FRONT_LEFT | ffi::AV_CH_FRONT_RIGHT | ffi::AV_CH_TOP_FRONT_LEFT);
600 assert_eq!(mapped_constant(&layout), None);
601
602 let rendering = describe_layout(&layout);
603 assert!(
604 rendering.contains("TFL"),
605 "FFmpeg should list the channels it cannot name: {rendering:?}"
606 );
607 assert_eq!(
608 channel_layout_from_ffmpeg(&layout),
609 ChannelLayout::default(),
610 "an unnamed layout must land on the absent sentinel"
611 );
612
613 let described = channel_layout_description_from_ffmpeg(&layout);
614 assert_eq!(described.known_kind(), &ChannelLayout::default());
615 assert_eq!(
616 described.text(),
617 rendering.as_str(),
618 "the rendering is what `text` carries"
619 );
620 }
621
622 /// The rung itself, on describe-shaped strings — the half of the door
623 /// that needs no `AVChannelLayout` to exercise.
624 #[test]
625 fn the_describe_rung_reads_names_and_refuses_everything_else() {
626 // The three orphans, as words.
627 assert_eq!(
628 channel_layout_from_describe("binaural"),
629 ChannelLayout::Binaural
630 );
631 assert_eq!(
632 channel_layout_from_describe("5.1.2"),
633 ChannelLayout::Ch5_1_2
634 );
635 assert_eq!(
636 channel_layout_from_describe("9.1.6"),
637 ChannelLayout::Ch9_1_6
638 );
639 // The crossed slugs: unqualified `5.1` is the *back* layout and the
640 // side one is qualified, so reading the word is the only way to tell
641 // these two apart.
642 assert_eq!(
643 channel_layout_from_describe("5.1"),
644 ChannelLayout::Ch5_1Back
645 );
646 assert_eq!(
647 channel_layout_from_describe("5.1(side)"),
648 ChannelLayout::Ch5_1
649 );
650 // Case folding is the vocabulary's, not ours.
651 assert_eq!(
652 channel_layout_from_describe("BINAURAL"),
653 ChannelLayout::Binaural
654 );
655
656 // Everything else is absent — never `Other(<the rendering>)`.
657 for unnamed in [
658 "",
659 "3 channels",
660 "3 channels (FL+FR+TFL)",
661 "FL@Left+FR@Right",
662 "ambisonic 2",
663 "not-a-layout",
664 ] {
665 assert_eq!(
666 channel_layout_from_describe(unnamed),
667 ChannelLayout::default(),
668 "{unnamed:?} must stay absent"
669 );
670 }
671 }
672}