Skip to main content

oxideav_core/
format.rs

1//! Media-type and sample/pixel format enumerations.
2//!
3//! Audio channel ordering follows SMPTE 2036-2 / ITU-R BS.775 conventions
4//! for surround layouts; per-channel positions are named with the
5//! WAVEFORMATEXTENSIBLE / FFmpeg "front-left, front-right, …" vocabulary.
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
8pub enum MediaType {
9    Audio,
10    Video,
11    Subtitle,
12    Data,
13    Unknown,
14}
15
16/// A single speaker position within a multi-channel audio layout.
17///
18/// Names follow the WAVEFORMATEXTENSIBLE / FFmpeg / SMPTE convention.
19/// `Side*` and `Back*` are kept distinct (mirroring 7.1's
20/// L/R + Ls/Rs + Lb/Rb separation) so codecs that surface the
21/// distinction don't collapse it. `Lr`/`Rr` (rear / back-rear) are aliases
22/// for `BackLeft`/`BackRight` in this taxonomy — the rear pair sits behind
23/// the listener on the room's centreline-extension, the side pair is at
24/// roughly ±90° from front. The enum is `#[non_exhaustive]` so additional
25/// positions (height channels for Atmos / Auro-3D, etc.) can be added
26/// without breaking downstream match arms.
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
28#[non_exhaustive]
29pub enum ChannelPosition {
30    /// Front-left (L). 30° left of centre in BS.775 listening geometry.
31    FrontLeft,
32    /// Front-right (R). 30° right of centre.
33    FrontRight,
34    /// Front-centre (C). Direct centre, 0°.
35    FrontCenter,
36    /// Low-frequency effects (LFE). Sub-bass, no positional meaning.
37    LowFrequency,
38    /// Back-left (Lb / Lr). Behind the listener, ±150° in 7.1.
39    BackLeft,
40    /// Back-right (Rb / Rr). Behind the listener, mirror of `BackLeft`.
41    BackRight,
42    /// Front left-of-centre (Lc). Used in cinema 7.1 SDDS layouts.
43    FrontLeftOfCenter,
44    /// Front right-of-centre (Rc). Mirror of `FrontLeftOfCenter`.
45    FrontRightOfCenter,
46    /// Back-centre (Cs). Single rear channel for 6.1 / BS.775 4.0.
47    BackCenter,
48    /// Side-left (Ls). ±90° on the listener's left in 5.1 / 7.1.
49    SideLeft,
50    /// Side-right (Rs). Mirror of `SideLeft`.
51    SideRight,
52    /// Top front-left. Atmos / Auro-3D height layer (placeholder).
53    TopFrontLeft,
54    /// Top front-right. Atmos / Auro-3D height layer (placeholder).
55    TopFrontRight,
56    /// Top back-left. Atmos / Auro-3D ceiling layer (placeholder).
57    TopBackLeft,
58    /// Top back-right. Atmos / Auro-3D ceiling layer (placeholder).
59    TopBackRight,
60}
61
62/// Audio channel layout — names a fixed ordered tuple of speaker
63/// positions, OR carries a discrete fallback count when the layout is
64/// unknown / non-standard.
65///
66/// Channel orderings are taken from ITU-R BS.775 (5.1 / 7.1 surround
67/// reference) and SMPTE ST 2036-2 (audio channel ordering for UHDTV).
68/// For 5.1 the canonical order this crate adopts is
69/// `L, R, C, LFE, Ls, Rs` (the WAVEFORMATEXTENSIBLE / Vorbis / Opus
70/// convention). 7.1 extends that with `Lb, Rb` (back-rear pair).
71///
72/// The `Stereo` variant covers both regular two-channel stereo and the
73/// AC-3 / AC-4 matrix-encoded downmix carriers `Lo/Ro` ("two of",
74/// downmix-compatible) and `Lt/Rt` ("matrix-encoded for Pro Logic
75/// extraction"); the dedicated [`LoRo`] / [`LtRt`] variants surface the
76/// distinction explicitly when a downstream filter or muxer needs it.
77///
78/// `DiscreteN(n)` is the catch-all for "we know there are `n` channels
79/// but no recognised layout" — used when a codec produces an unusual
80/// channel count (>8) or when the container failed to surface a layout
81/// flag. It is the only variant whose `position()` returns `None`.
82///
83/// Marked `#[non_exhaustive]` so additional standard layouts (Atmos
84/// 7.1.4, Auro-3D 9.1, …) can be added without breaking match-exhaustive
85/// downstream consumers.
86#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
87#[non_exhaustive]
88pub enum ChannelLayout {
89    /// Mono (1ch): C.
90    Mono,
91    /// Stereo (2ch): L, R.
92    Stereo,
93    /// 2.1 (3ch): L, R, LFE.
94    Stereo21,
95    /// 3.0 surround (3ch): L, R, C.
96    Surround30,
97    /// Quadraphonic (4ch): L, R, Ls, Rs — no centre, side surrounds.
98    Quad,
99    /// 4.0 surround per BS.775 (4ch): L, R, C, Cs — centre + back surround.
100    Surround40,
101    /// 4.1 surround (5ch): L, R, C, Cs, LFE.
102    Surround41,
103    /// 5.0 surround (5ch): L, R, C, Ls, Rs.
104    Surround50,
105    /// 5.1 surround (6ch): L, R, C, LFE, Ls, Rs.
106    Surround51,
107    /// 6.0 surround (6ch): L, R, C, Cs, Ls, Rs.
108    Surround60,
109    /// 6.1 surround (7ch): L, R, C, LFE, Cs, Ls, Rs.
110    Surround61,
111    /// 7.0 surround (7ch): L, R, C, Ls, Rs, Lb, Rb.
112    Surround70,
113    /// 7.1 surround (8ch): L, R, C, LFE, Ls, Rs, Lb, Rb.
114    Surround71,
115    /// AC-3 / AC-4 Lo/Ro stereo downmix (2ch). Two-channel mix preserving
116    /// downmix-compatibility coefficients; not matrix-encoded.
117    LoRo,
118    /// AC-3 / AC-4 Lt/Rt stereo downmix (2ch). Two-channel matrix-encoded
119    /// downmix carrying surround information for Dolby Pro Logic decoding.
120    LtRt,
121    /// Discrete fallback: `n` channels with no recognised layout. Used for
122    /// unusual / >8ch / unknown layouts surfaced by exotic codecs or
123    /// containers that drop layout flags.
124    DiscreteN(u16),
125}
126
127impl ChannelLayout {
128    /// Number of channels in this layout.
129    pub fn channel_count(&self) -> u16 {
130        match self {
131            Self::Mono => 1,
132            Self::Stereo | Self::LoRo | Self::LtRt => 2,
133            Self::Stereo21 | Self::Surround30 => 3,
134            Self::Quad | Self::Surround40 => 4,
135            Self::Surround41 | Self::Surround50 => 5,
136            Self::Surround51 | Self::Surround60 => 6,
137            Self::Surround61 | Self::Surround70 => 7,
138            Self::Surround71 => 8,
139            Self::DiscreteN(n) => *n,
140        }
141    }
142
143    /// Speaker positions in canonical order. Returns an empty slice for
144    /// `DiscreteN` since the layout is unknown — call [`positions_owned`]
145    /// to get a `Vec` if you need to enumerate slots regardless of
146    /// known/unknown status.
147    ///
148    /// [`positions_owned`]: Self::positions_owned
149    pub fn positions(&self) -> &'static [ChannelPosition] {
150        use ChannelPosition::*;
151        match self {
152            Self::Mono => &[FrontCenter],
153            Self::Stereo | Self::LoRo | Self::LtRt => &[FrontLeft, FrontRight],
154            Self::Stereo21 => &[FrontLeft, FrontRight, LowFrequency],
155            Self::Surround30 => &[FrontLeft, FrontRight, FrontCenter],
156            Self::Quad => &[FrontLeft, FrontRight, SideLeft, SideRight],
157            Self::Surround40 => &[FrontLeft, FrontRight, FrontCenter, BackCenter],
158            Self::Surround41 => &[FrontLeft, FrontRight, FrontCenter, BackCenter, LowFrequency],
159            Self::Surround50 => &[FrontLeft, FrontRight, FrontCenter, SideLeft, SideRight],
160            Self::Surround51 => &[
161                FrontLeft,
162                FrontRight,
163                FrontCenter,
164                LowFrequency,
165                SideLeft,
166                SideRight,
167            ],
168            Self::Surround60 => &[
169                FrontLeft,
170                FrontRight,
171                FrontCenter,
172                BackCenter,
173                SideLeft,
174                SideRight,
175            ],
176            Self::Surround61 => &[
177                FrontLeft,
178                FrontRight,
179                FrontCenter,
180                LowFrequency,
181                BackCenter,
182                SideLeft,
183                SideRight,
184            ],
185            Self::Surround70 => &[
186                FrontLeft,
187                FrontRight,
188                FrontCenter,
189                SideLeft,
190                SideRight,
191                BackLeft,
192                BackRight,
193            ],
194            Self::Surround71 => &[
195                FrontLeft,
196                FrontRight,
197                FrontCenter,
198                LowFrequency,
199                SideLeft,
200                SideRight,
201                BackLeft,
202                BackRight,
203            ],
204            Self::DiscreteN(_) => &[],
205        }
206    }
207
208    /// Owned position list. For known layouts this clones [`positions`];
209    /// for `DiscreteN(n)` it returns an empty `Vec` (positions remain
210    /// unknown). Provided so callers that just want "give me positions
211    /// for any layout" don't have to special-case the discrete arm.
212    ///
213    /// [`positions`]: Self::positions
214    pub fn positions_owned(&self) -> Vec<ChannelPosition> {
215        self.positions().to_vec()
216    }
217
218    /// Speaker position at slot `idx` in canonical order, or `None` for
219    /// out-of-range slots and for `DiscreteN` (where the layout is
220    /// unknown).
221    pub fn position(&self, idx: usize) -> Option<ChannelPosition> {
222        self.positions().get(idx).copied()
223    }
224
225    /// True when this layout carries a low-frequency-effects (LFE) channel.
226    pub fn has_lfe(&self) -> bool {
227        self.positions()
228            .iter()
229            .any(|p| matches!(p, ChannelPosition::LowFrequency))
230    }
231
232    /// True when this layout carries surround information (more than two
233    /// channels OR an LFE). `Stereo` / `Mono` return false; `LoRo` /
234    /// `LtRt` are 2-channel downmixes and also return false even though
235    /// they encode surround content (that's the whole point of a
236    /// downmix).
237    pub fn is_surround(&self) -> bool {
238        self.channel_count() > 2 || self.has_lfe()
239    }
240
241    /// Back-compat bridge: infer a layout from a bare channel count.
242    ///
243    /// This mapping is what lets codecs that haven't been updated to set
244    /// a layout explicitly continue to work: they keep producing a count
245    /// and we infer the most-common layout for that count. The choices
246    /// follow industry defaults — 5.1 wins for 6ch (more common than
247    /// 6.0), 7.1 wins for 8ch, and so on.
248    ///
249    /// | count | layout       |
250    /// |-------|--------------|
251    /// | 1     | `Mono`       |
252    /// | 2     | `Stereo`     |
253    /// | 3     | `Surround30` |
254    /// | 4     | `Quad`       |
255    /// | 5     | `Surround50` |
256    /// | 6     | `Surround51` |
257    /// | 7     | `Surround61` |
258    /// | 8     | `Surround71` |
259    /// | other | `DiscreteN`  |
260    pub fn from_count(n: u16) -> ChannelLayout {
261        match n {
262            1 => Self::Mono,
263            2 => Self::Stereo,
264            3 => Self::Surround30,
265            4 => Self::Quad,
266            5 => Self::Surround50,
267            6 => Self::Surround51,
268            7 => Self::Surround61,
269            8 => Self::Surround71,
270            other => Self::DiscreteN(other),
271        }
272    }
273}
274
275impl std::fmt::Display for ChannelLayout {
276    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
277        let s = match self {
278            Self::Mono => "mono",
279            Self::Stereo => "stereo",
280            Self::Stereo21 => "2.1",
281            Self::Surround30 => "3.0",
282            Self::Quad => "quad",
283            Self::Surround40 => "4.0",
284            Self::Surround41 => "4.1",
285            Self::Surround50 => "5.0",
286            Self::Surround51 => "5.1",
287            Self::Surround60 => "6.0",
288            Self::Surround61 => "6.1",
289            Self::Surround70 => "7.0",
290            Self::Surround71 => "7.1",
291            Self::LoRo => "loro",
292            Self::LtRt => "ltrt",
293            Self::DiscreteN(n) => return write!(f, "discrete{n}"),
294        };
295        f.write_str(s)
296    }
297}
298
299/// Error returned by the [`ChannelLayout`] `FromStr` impl when the input
300/// doesn't match any recognised layout name.
301#[derive(Debug, Clone, PartialEq, Eq)]
302pub struct ParseChannelLayoutError(pub String);
303
304impl std::fmt::Display for ParseChannelLayoutError {
305    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306        write!(f, "unrecognised channel layout: {:?}", self.0)
307    }
308}
309
310impl std::error::Error for ParseChannelLayoutError {}
311
312impl std::str::FromStr for ChannelLayout {
313    type Err = ParseChannelLayoutError;
314
315    fn from_str(s: &str) -> Result<Self, Self::Err> {
316        let lower = s.trim().to_ascii_lowercase();
317        let layout = match lower.as_str() {
318            "mono" | "1.0" => Self::Mono,
319            "stereo" | "2.0" => Self::Stereo,
320            "2.1" => Self::Stereo21,
321            "3.0" | "surround3" | "surround30" => Self::Surround30,
322            "quad" => Self::Quad,
323            "4.0" | "surround4" | "surround40" => Self::Surround40,
324            "4.1" | "surround41" => Self::Surround41,
325            "5.0" | "surround5" | "surround50" => Self::Surround50,
326            "5.1" | "surround51" => Self::Surround51,
327            "6.0" | "surround6" | "surround60" => Self::Surround60,
328            "6.1" | "surround61" => Self::Surround61,
329            "7.0" | "surround7" | "surround70" => Self::Surround70,
330            "7.1" | "surround71" => Self::Surround71,
331            "loro" | "lo/ro" => Self::LoRo,
332            "ltrt" | "lt/rt" => Self::LtRt,
333            other => {
334                if let Some(rest) = other.strip_prefix("discrete") {
335                    if let Ok(n) = rest.parse::<u16>() {
336                        return Ok(Self::DiscreteN(n));
337                    }
338                }
339                return Err(ParseChannelLayoutError(s.to_owned()));
340            }
341        };
342        Ok(layout)
343    }
344}
345
346/// Audio sample format.
347///
348/// Variants carry **stable explicit discriminants** — the integer value
349/// of `SampleFormat::S16 as u8` is part of the public ABI. Add new
350/// variants only at the end with a fresh number; never reorder, renumber,
351/// or remove. `#[non_exhaustive]` lets the enum grow without breaking
352/// downstream `match` statements; pinned discriminants additionally let
353/// the format round-trip through any byte-stable serialization
354/// (config files, capability blobs, IPC) without losing meaning across
355/// crate versions.
356#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
357#[non_exhaustive]
358#[repr(u8)]
359pub enum SampleFormat {
360    /// Unsigned 8-bit, interleaved.
361    U8 = 0,
362    /// Signed 8-bit, interleaved. Native format of Amiga 8SVX and MOD samples.
363    S8 = 1,
364    /// Signed 16-bit little-endian, interleaved.
365    S16 = 2,
366    /// Signed 24-bit packed (3 bytes/sample) little-endian, interleaved.
367    S24 = 3,
368    /// Signed 32-bit little-endian, interleaved.
369    S32 = 4,
370    /// 32-bit IEEE float, interleaved.
371    F32 = 5,
372    /// 64-bit IEEE float, interleaved.
373    F64 = 6,
374    /// Planar variants — one plane per channel.
375    U8P = 7,
376    S16P = 8,
377    S32P = 9,
378    F32P = 10,
379    F64P = 11,
380}
381
382impl SampleFormat {
383    pub fn is_planar(&self) -> bool {
384        matches!(
385            self,
386            Self::U8P | Self::S16P | Self::S32P | Self::F32P | Self::F64P
387        )
388    }
389
390    /// Bytes per sample *per channel*.
391    pub fn bytes_per_sample(&self) -> usize {
392        match self {
393            Self::U8 | Self::U8P | Self::S8 => 1,
394            Self::S16 | Self::S16P => 2,
395            Self::S24 => 3,
396            Self::S32 | Self::S32P | Self::F32 | Self::F32P => 4,
397            Self::F64 | Self::F64P => 8,
398        }
399    }
400
401    pub fn is_float(&self) -> bool {
402        matches!(self, Self::F32 | Self::F64 | Self::F32P | Self::F64P)
403    }
404
405    /// Number of `Vec<u8>` planes an [`AudioFrame`](crate::AudioFrame)
406    /// of this format carries for `channels` channels: planar formats
407    /// use one plane per channel, interleaved formats use one plane
408    /// total.
409    pub fn plane_count(&self, channels: u16) -> usize {
410        if self.is_planar() {
411            channels as usize
412        } else {
413            1
414        }
415    }
416}
417
418/// Video pixel format.
419///
420/// Variants carry **stable explicit discriminants** — the integer value
421/// of `PixelFormat::Yuv420P as u16` is part of the public ABI. Add new
422/// variants only at the end with a fresh number; never reorder, renumber,
423/// or remove. `#[non_exhaustive]` lets the enum grow without breaking
424/// downstream `match` statements; pinned discriminants additionally let
425/// the format round-trip through any byte-stable serialization
426/// (config files, capability blobs, IPC, on-disk caches) without losing
427/// meaning across crate versions, and prevent inserts in the middle of
428/// the enum from shifting every later variant's number (which
429/// cargo-semver-checks rightly flags as a breaking change).
430///
431/// The first six variants (`Yuv420P` through `Gray8`) are the original
432/// formats produced by the early codec crates. Everything beyond that
433/// is additional surface handled by `oxideav-pixfmt` and the still-image
434/// codecs (PNG, GIF, still-JPEG).
435#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
436#[non_exhaustive]
437#[repr(u16)]
438pub enum PixelFormat {
439    /// 8-bit YUV 4:2:0, planar (Y, U, V).
440    Yuv420P = 0,
441    /// 8-bit YUV 4:2:2, planar.
442    Yuv422P = 1,
443    /// 8-bit YUV 4:4:4, planar.
444    Yuv444P = 2,
445    /// Packed 8-bit RGB, 3 bytes/pixel.
446    Rgb24 = 3,
447    /// Packed 8-bit RGBA, 4 bytes/pixel.
448    Rgba = 4,
449    /// Packed 8-bit grayscale.
450    Gray8 = 5,
451
452    // --- Palette ---
453    /// 8-bit palette indices — companion palette carried out of band.
454    Pal8 = 6,
455
456    // --- Packed RGB/BGR swizzles ---
457    /// Packed 8-bit BGR, 3 bytes/pixel.
458    Bgr24 = 7,
459    /// Packed 8-bit BGRA, 4 bytes/pixel.
460    Bgra = 8,
461    /// Packed 8-bit ARGB, 4 bytes/pixel (alpha first).
462    Argb = 9,
463    /// Packed 8-bit ABGR, 4 bytes/pixel.
464    Abgr = 10,
465
466    // --- Deeper packed RGB ---
467    /// Packed 16-bit-per-channel RGB, little-endian, 6 bytes/pixel.
468    Rgb48Le = 11,
469    /// Packed 16-bit-per-channel RGBA, little-endian, 8 bytes/pixel.
470    Rgba64Le = 12,
471
472    // --- Grayscale deeper / partial bit depths ---
473    /// 16-bit little-endian grayscale.
474    Gray16Le = 13,
475    /// 10-bit grayscale in a 16-bit little-endian word.
476    Gray10Le = 14,
477    /// 12-bit grayscale in a 16-bit little-endian word.
478    Gray12Le = 15,
479
480    // --- Higher-precision YUV ---
481    /// 10-bit YUV 4:2:0 planar, little-endian 16-bit storage.
482    Yuv420P10Le = 16,
483    /// 10-bit YUV 4:2:2 planar, little-endian 16-bit storage.
484    Yuv422P10Le = 17,
485    /// 10-bit YUV 4:4:4 planar, little-endian 16-bit storage.
486    Yuv444P10Le = 18,
487    /// 12-bit YUV 4:2:0 planar, little-endian 16-bit storage.
488    Yuv420P12Le = 19,
489    /// 12-bit YUV 4:2:2 planar, little-endian 16-bit storage.
490    Yuv422P12Le = 20,
491    /// 12-bit YUV 4:4:4 planar, little-endian 16-bit storage.
492    Yuv444P12Le = 21,
493
494    // --- Full-range ("J") YUV ---
495    /// JPEG/full-range YUV 4:2:0 planar.
496    YuvJ420P = 22,
497    /// JPEG/full-range YUV 4:2:2 planar.
498    YuvJ422P = 23,
499    /// JPEG/full-range YUV 4:4:4 planar.
500    YuvJ444P = 24,
501
502    // --- Semi-planar YUV ---
503    /// YUV 4:2:0, planar Y + interleaved UV (NV12).
504    Nv12 = 25,
505    /// YUV 4:2:0, planar Y + interleaved VU (NV21).
506    Nv21 = 26,
507
508    // --- Gray + alpha / YUV + alpha ---
509    /// Packed grayscale + alpha, 2 bytes/pixel (Y, A).
510    Ya8 = 27,
511    /// Yuv420P with an additional full-resolution alpha plane.
512    Yuva420P = 28,
513
514    // --- Mono (1 bit per pixel) ---
515    /// 1 bit per pixel, packed MSB-first, 0 = black.
516    MonoBlack = 29,
517    /// 1 bit per pixel, packed MSB-first, 0 = white.
518    MonoWhite = 30,
519
520    // --- Interleaved YUV 4:2:2 ---
521    /// Packed 4:2:2, byte order Y0 U0 Y1 V0.
522    Yuyv422 = 31,
523    /// Packed 4:2:2, byte order U0 Y0 V0 Y1.
524    Uyvy422 = 32,
525
526    // --- Print / prepress ---
527    /// Packed 8-bit CMYK, 4 bytes/pixel in byte order C, M, Y, K.
528    /// "Regular" convention: C=0 means no cyan ink (white), C=255 means
529    /// full cyan. Used by JPEG 4-component scans from non-Adobe encoders
530    /// and by many print-side image toolchains. Adobe Photoshop's
531    /// inverted CMYK (where 0 = full ink) is a separate variant reserved
532    /// for a future `CmykInverted`.
533    Cmyk = 33,
534
535    // --- Wide-horizontal subsampled YUV ---
536    /// 8-bit YUV 4:1:1, planar (Y, U, V). Luma at full resolution; chroma
537    /// horizontally subsampled by 4 (each chroma sample covers a 4×1
538    /// luma block), no vertical subsampling. Native sampling of
539    /// NTSC DV-25 and a legal JPEG sampling layout (luma H=4, V=1;
540    /// chroma H=V=1) emitted by some real-world JPEG corpora.
541    Yuv411P = 34,
542}
543
544impl PixelFormat {
545    /// True if this format stores its components in separate planes.
546    pub fn is_planar(&self) -> bool {
547        matches!(
548            self,
549            Self::Yuv420P
550                | Self::Yuv422P
551                | Self::Yuv444P
552                | Self::Yuv411P
553                | Self::Yuv420P10Le
554                | Self::Yuv422P10Le
555                | Self::Yuv444P10Le
556                | Self::Yuv420P12Le
557                | Self::Yuv422P12Le
558                | Self::Yuv444P12Le
559                | Self::YuvJ420P
560                | Self::YuvJ422P
561                | Self::YuvJ444P
562                | Self::Nv12
563                | Self::Nv21
564                | Self::Yuva420P
565        )
566    }
567
568    /// True if the format is a palette index format (`Pal8`).
569    pub fn is_palette(&self) -> bool {
570        matches!(self, Self::Pal8)
571    }
572
573    /// True if this format carries an alpha channel.
574    pub fn has_alpha(&self) -> bool {
575        matches!(
576            self,
577            Self::Rgba
578                | Self::Bgra
579                | Self::Argb
580                | Self::Abgr
581                | Self::Rgba64Le
582                | Self::Ya8
583                | Self::Yuva420P
584        )
585    }
586
587    /// Number of planes in the stored layout. Packed and palette formats
588    /// return 1; NV12/NV21 return 2; planar YUV without alpha returns 3;
589    /// YuvA variants return 4.
590    pub fn plane_count(&self) -> usize {
591        match self {
592            Self::Nv12 | Self::Nv21 => 2,
593            Self::Yuv420P
594            | Self::Yuv422P
595            | Self::Yuv444P
596            | Self::Yuv411P
597            | Self::Yuv420P10Le
598            | Self::Yuv422P10Le
599            | Self::Yuv444P10Le
600            | Self::Yuv420P12Le
601            | Self::Yuv422P12Le
602            | Self::Yuv444P12Le
603            | Self::YuvJ420P
604            | Self::YuvJ422P
605            | Self::YuvJ444P => 3,
606            Self::Yuva420P => 4,
607            _ => 1,
608        }
609    }
610
611    /// Rough bits-per-pixel estimate, useful for buffer sizing. Not exact
612    /// for chroma-subsampled YUV — intended for worst-case preallocation
613    /// rather than wire-accurate accounting.
614    pub fn bits_per_pixel_approx(&self) -> u32 {
615        match self {
616            Self::MonoBlack | Self::MonoWhite => 1,
617            Self::Gray8 | Self::Pal8 => 8,
618            Self::Ya8 => 16,
619            Self::Gray16Le | Self::Gray10Le | Self::Gray12Le => 16,
620            Self::Rgb24 | Self::Bgr24 => 24,
621            Self::Rgba | Self::Bgra | Self::Argb | Self::Abgr => 32,
622            Self::Rgb48Le => 48,
623            Self::Rgba64Le => 64,
624            Self::Yuyv422 | Self::Uyvy422 => 16,
625            Self::Cmyk => 32,
626            // Planar YUV: 4:2:0 ≈ 12, 4:2:2 ≈ 16, 4:4:4 ≈ 24
627            // 10/12-bit variants double the byte count but we report the
628            // packed-bits-per-pixel estimate for a uniform heuristic.
629            Self::Yuv420P | Self::YuvJ420P | Self::Nv12 | Self::Nv21 => 12,
630            // 4:1:1 has the same packed bits-per-pixel as 4:2:0 (luma at
631            // full res + 2 chroma planes each subsampled by 4).
632            Self::Yuv411P => 12,
633            Self::Yuv422P | Self::YuvJ422P => 16,
634            Self::Yuv444P | Self::YuvJ444P => 24,
635            Self::Yuv420P10Le | Self::Yuv420P12Le => 24,
636            Self::Yuv422P10Le | Self::Yuv422P12Le => 32,
637            Self::Yuv444P10Le | Self::Yuv444P12Le => 48,
638            Self::Yuva420P => 20,
639        }
640    }
641}
642
643#[cfg(test)]
644mod tests {
645    use super::*;
646
647    /// Pin every `PixelFormat` and `SampleFormat` discriminant. This is the
648    /// stability commitment — the integer value of each variant is part of
649    /// the public ABI. Any reorder, renumber, or removal will fail this test
650    /// and the change MUST be a major version bump (or a fresh variant
651    /// appended at a new number, leaving the existing ones untouched).
652    #[test]
653    fn pixel_format_discriminants_pinned() {
654        assert_eq!(PixelFormat::Yuv420P as u16, 0);
655        assert_eq!(PixelFormat::Yuv422P as u16, 1);
656        assert_eq!(PixelFormat::Yuv444P as u16, 2);
657        assert_eq!(PixelFormat::Rgb24 as u16, 3);
658        assert_eq!(PixelFormat::Rgba as u16, 4);
659        assert_eq!(PixelFormat::Gray8 as u16, 5);
660        assert_eq!(PixelFormat::Pal8 as u16, 6);
661        assert_eq!(PixelFormat::Bgr24 as u16, 7);
662        assert_eq!(PixelFormat::Bgra as u16, 8);
663        assert_eq!(PixelFormat::Argb as u16, 9);
664        assert_eq!(PixelFormat::Abgr as u16, 10);
665        assert_eq!(PixelFormat::Rgb48Le as u16, 11);
666        assert_eq!(PixelFormat::Rgba64Le as u16, 12);
667        assert_eq!(PixelFormat::Gray16Le as u16, 13);
668        assert_eq!(PixelFormat::Gray10Le as u16, 14);
669        assert_eq!(PixelFormat::Gray12Le as u16, 15);
670        assert_eq!(PixelFormat::Yuv420P10Le as u16, 16);
671        assert_eq!(PixelFormat::Yuv422P10Le as u16, 17);
672        assert_eq!(PixelFormat::Yuv444P10Le as u16, 18);
673        assert_eq!(PixelFormat::Yuv420P12Le as u16, 19);
674        assert_eq!(PixelFormat::Yuv422P12Le as u16, 20);
675        assert_eq!(PixelFormat::Yuv444P12Le as u16, 21);
676        assert_eq!(PixelFormat::YuvJ420P as u16, 22);
677        assert_eq!(PixelFormat::YuvJ422P as u16, 23);
678        assert_eq!(PixelFormat::YuvJ444P as u16, 24);
679        assert_eq!(PixelFormat::Nv12 as u16, 25);
680        assert_eq!(PixelFormat::Nv21 as u16, 26);
681        assert_eq!(PixelFormat::Ya8 as u16, 27);
682        assert_eq!(PixelFormat::Yuva420P as u16, 28);
683        assert_eq!(PixelFormat::MonoBlack as u16, 29);
684        assert_eq!(PixelFormat::MonoWhite as u16, 30);
685        assert_eq!(PixelFormat::Yuyv422 as u16, 31);
686        assert_eq!(PixelFormat::Uyvy422 as u16, 32);
687        assert_eq!(PixelFormat::Cmyk as u16, 33);
688        assert_eq!(PixelFormat::Yuv411P as u16, 34);
689    }
690
691    #[test]
692    fn sample_format_discriminants_pinned() {
693        assert_eq!(SampleFormat::U8 as u8, 0);
694        assert_eq!(SampleFormat::S8 as u8, 1);
695        assert_eq!(SampleFormat::S16 as u8, 2);
696        assert_eq!(SampleFormat::S24 as u8, 3);
697        assert_eq!(SampleFormat::S32 as u8, 4);
698        assert_eq!(SampleFormat::F32 as u8, 5);
699        assert_eq!(SampleFormat::F64 as u8, 6);
700        assert_eq!(SampleFormat::U8P as u8, 7);
701        assert_eq!(SampleFormat::S16P as u8, 8);
702        assert_eq!(SampleFormat::S32P as u8, 9);
703        assert_eq!(SampleFormat::F32P as u8, 10);
704        assert_eq!(SampleFormat::F64P as u8, 11);
705    }
706
707    #[test]
708    fn high_bit_yuv_planar_metadata() {
709        // 10-bit reference variants are planar with three planes.
710        assert!(PixelFormat::Yuv420P10Le.is_planar());
711        assert!(PixelFormat::Yuv422P10Le.is_planar());
712        assert!(PixelFormat::Yuv444P10Le.is_planar());
713
714        // 12-bit variants must follow the same shape.
715        assert!(PixelFormat::Yuv420P12Le.is_planar());
716        assert!(PixelFormat::Yuv422P12Le.is_planar());
717        assert!(PixelFormat::Yuv444P12Le.is_planar());
718
719        assert_eq!(PixelFormat::Yuv420P12Le.plane_count(), 3);
720        assert_eq!(PixelFormat::Yuv422P12Le.plane_count(), 3);
721        assert_eq!(PixelFormat::Yuv444P12Le.plane_count(), 3);
722
723        // None of the high-bit YUV variants carry alpha or palette.
724        assert!(!PixelFormat::Yuv422P12Le.has_alpha());
725        assert!(!PixelFormat::Yuv444P12Le.has_alpha());
726        assert!(!PixelFormat::Yuv422P12Le.is_palette());
727        assert!(!PixelFormat::Yuv444P12Le.is_palette());
728    }
729
730    #[test]
731    fn channel_layout_round_trip_count_for_known_layouts() {
732        // For every `n` that `from_count` maps to a named layout, the
733        // resulting layout's `channel_count()` must equal `n` again.
734        for n in 1..=8u16 {
735            let layout = ChannelLayout::from_count(n);
736            assert_eq!(layout.channel_count(), n, "round-trip failed for n={n}");
737            // None of these defaults should fall through to DiscreteN.
738            assert!(
739                !matches!(layout, ChannelLayout::DiscreteN(_)),
740                "from_count({n}) unexpectedly produced DiscreteN"
741            );
742        }
743    }
744
745    #[test]
746    fn channel_layout_from_count_default_table() {
747        // The exact mapping documented on `from_count` — pin it so
748        // future refactors don't silently change the inferred layout.
749        assert_eq!(ChannelLayout::from_count(1), ChannelLayout::Mono);
750        assert_eq!(ChannelLayout::from_count(2), ChannelLayout::Stereo);
751        assert_eq!(ChannelLayout::from_count(3), ChannelLayout::Surround30);
752        assert_eq!(ChannelLayout::from_count(4), ChannelLayout::Quad);
753        assert_eq!(ChannelLayout::from_count(5), ChannelLayout::Surround50);
754        assert_eq!(ChannelLayout::from_count(6), ChannelLayout::Surround51);
755        assert_eq!(ChannelLayout::from_count(7), ChannelLayout::Surround61);
756        assert_eq!(ChannelLayout::from_count(8), ChannelLayout::Surround71);
757    }
758
759    #[test]
760    fn channel_layout_unknown_count_falls_through_to_discrete() {
761        assert_eq!(ChannelLayout::from_count(0), ChannelLayout::DiscreteN(0));
762        assert_eq!(ChannelLayout::from_count(13), ChannelLayout::DiscreteN(13));
763        assert_eq!(
764            ChannelLayout::from_count(64).channel_count(),
765            64,
766            "DiscreteN must report the count it was constructed with"
767        );
768    }
769
770    #[test]
771    fn channel_layout_position_lookup() {
772        assert_eq!(
773            ChannelLayout::Stereo.position(0),
774            Some(ChannelPosition::FrontLeft)
775        );
776        assert_eq!(
777            ChannelLayout::Stereo.position(1),
778            Some(ChannelPosition::FrontRight)
779        );
780        assert_eq!(ChannelLayout::Stereo.position(2), None);
781
782        // 5.1 canonical: L, R, C, LFE, Ls, Rs.
783        let s51 = ChannelLayout::Surround51;
784        assert_eq!(s51.position(0), Some(ChannelPosition::FrontLeft));
785        assert_eq!(s51.position(1), Some(ChannelPosition::FrontRight));
786        assert_eq!(s51.position(2), Some(ChannelPosition::FrontCenter));
787        assert_eq!(s51.position(3), Some(ChannelPosition::LowFrequency));
788        assert_eq!(s51.position(4), Some(ChannelPosition::SideLeft));
789        assert_eq!(s51.position(5), Some(ChannelPosition::SideRight));
790        assert_eq!(s51.position(6), None);
791
792        // DiscreteN never reveals a position.
793        assert_eq!(ChannelLayout::DiscreteN(13).position(0), None);
794    }
795
796    #[test]
797    fn channel_layout_lfe_and_surround_predicates() {
798        assert!(ChannelLayout::Surround51.has_lfe());
799        assert!(ChannelLayout::Surround71.has_lfe());
800        assert!(ChannelLayout::Stereo21.has_lfe());
801        assert!(!ChannelLayout::Quad.has_lfe());
802        assert!(!ChannelLayout::Surround50.has_lfe());
803        assert!(!ChannelLayout::Stereo.has_lfe());
804
805        assert!(!ChannelLayout::Mono.is_surround());
806        assert!(!ChannelLayout::Stereo.is_surround());
807        // Downmix carriers are still 2ch / no-LFE → not "surround" by
808        // the layout-shape definition; the surround info lives in the
809        // sample matrix itself.
810        assert!(!ChannelLayout::LoRo.is_surround());
811        assert!(!ChannelLayout::LtRt.is_surround());
812        assert!(ChannelLayout::Stereo21.is_surround());
813        assert!(ChannelLayout::Surround51.is_surround());
814        assert!(ChannelLayout::Surround71.is_surround());
815    }
816
817    #[test]
818    fn channel_layout_display_and_fromstr_round_trip() {
819        use std::str::FromStr;
820        let cases = [
821            ChannelLayout::Mono,
822            ChannelLayout::Stereo,
823            ChannelLayout::Stereo21,
824            ChannelLayout::Surround30,
825            ChannelLayout::Quad,
826            ChannelLayout::Surround40,
827            ChannelLayout::Surround41,
828            ChannelLayout::Surround50,
829            ChannelLayout::Surround51,
830            ChannelLayout::Surround60,
831            ChannelLayout::Surround61,
832            ChannelLayout::Surround70,
833            ChannelLayout::Surround71,
834            ChannelLayout::LoRo,
835            ChannelLayout::LtRt,
836            ChannelLayout::DiscreteN(13),
837        ];
838        for layout in cases {
839            let s = layout.to_string();
840            let parsed = ChannelLayout::from_str(&s).expect("display output must parse back");
841            assert_eq!(parsed, layout, "round-trip failed via {s:?}");
842        }
843    }
844
845    #[test]
846    fn channel_layout_fromstr_accepts_aliases_and_case() {
847        use std::str::FromStr;
848        assert_eq!(
849            ChannelLayout::from_str("STEREO").unwrap(),
850            ChannelLayout::Stereo
851        );
852        assert_eq!(
853            ChannelLayout::from_str("2.0").unwrap(),
854            ChannelLayout::Stereo
855        );
856        assert_eq!(
857            ChannelLayout::from_str("5.1").unwrap(),
858            ChannelLayout::Surround51
859        );
860        assert_eq!(
861            ChannelLayout::from_str("Lo/Ro").unwrap(),
862            ChannelLayout::LoRo
863        );
864        assert_eq!(
865            ChannelLayout::from_str("lt/rt").unwrap(),
866            ChannelLayout::LtRt
867        );
868        assert!(ChannelLayout::from_str("absurd_layout").is_err());
869    }
870
871    #[test]
872    fn channel_layout_positions_owned_matches_static_slice() {
873        for layout in [
874            ChannelLayout::Mono,
875            ChannelLayout::Surround51,
876            ChannelLayout::Surround71,
877        ] {
878            assert_eq!(layout.positions_owned(), layout.positions());
879        }
880        // DiscreteN returns an empty owned vec — positions are unknown.
881        assert!(ChannelLayout::DiscreteN(7).positions_owned().is_empty());
882    }
883
884    #[test]
885    fn sample_format_plane_count_interleaved_is_one() {
886        // Interleaved formats always pack into a single plane, regardless
887        // of channel count.
888        for ch in [1u16, 2, 6, 8, 64, 0] {
889            assert_eq!(SampleFormat::S16.plane_count(ch), 1);
890            assert_eq!(SampleFormat::F32.plane_count(ch), 1);
891            assert_eq!(SampleFormat::U8.plane_count(ch), 1);
892            assert_eq!(SampleFormat::S24.plane_count(ch), 1);
893        }
894    }
895
896    #[test]
897    fn sample_format_plane_count_planar_matches_channels() {
898        // Planar formats use one plane per channel.
899        assert_eq!(SampleFormat::S16P.plane_count(1), 1);
900        assert_eq!(SampleFormat::S16P.plane_count(2), 2);
901        assert_eq!(SampleFormat::F32P.plane_count(6), 6);
902        assert_eq!(SampleFormat::F64P.plane_count(8), 8);
903
904        // Edge case: zero channels in a planar format yields zero planes.
905        assert_eq!(SampleFormat::S32P.plane_count(0), 0);
906    }
907
908    #[test]
909    fn high_bit_yuv_bits_per_pixel_approx() {
910        // 4:2:2 and 4:4:4 12-bit match their 10-bit siblings on the
911        // packed-bits estimator (the approximation reports samples-per-pixel
912        // density, not the 16-bit storage width).
913        assert_eq!(PixelFormat::Yuv422P10Le.bits_per_pixel_approx(), 32);
914        assert_eq!(PixelFormat::Yuv422P12Le.bits_per_pixel_approx(), 32);
915        assert_eq!(PixelFormat::Yuv444P10Le.bits_per_pixel_approx(), 48);
916        assert_eq!(PixelFormat::Yuv444P12Le.bits_per_pixel_approx(), 48);
917        assert_eq!(PixelFormat::Yuv420P12Le.bits_per_pixel_approx(), 24);
918    }
919}