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
536impl PixelFormat {
537    /// True if this format stores its components in separate planes.
538    pub fn is_planar(&self) -> bool {
539        matches!(
540            self,
541            Self::Yuv420P
542                | Self::Yuv422P
543                | Self::Yuv444P
544                | Self::Yuv420P10Le
545                | Self::Yuv422P10Le
546                | Self::Yuv444P10Le
547                | Self::Yuv420P12Le
548                | Self::Yuv422P12Le
549                | Self::Yuv444P12Le
550                | Self::YuvJ420P
551                | Self::YuvJ422P
552                | Self::YuvJ444P
553                | Self::Nv12
554                | Self::Nv21
555                | Self::Yuva420P
556        )
557    }
558
559    /// True if the format is a palette index format (`Pal8`).
560    pub fn is_palette(&self) -> bool {
561        matches!(self, Self::Pal8)
562    }
563
564    /// True if this format carries an alpha channel.
565    pub fn has_alpha(&self) -> bool {
566        matches!(
567            self,
568            Self::Rgba
569                | Self::Bgra
570                | Self::Argb
571                | Self::Abgr
572                | Self::Rgba64Le
573                | Self::Ya8
574                | Self::Yuva420P
575        )
576    }
577
578    /// Number of planes in the stored layout. Packed and palette formats
579    /// return 1; NV12/NV21 return 2; planar YUV without alpha returns 3;
580    /// YuvA variants return 4.
581    pub fn plane_count(&self) -> usize {
582        match self {
583            Self::Nv12 | Self::Nv21 => 2,
584            Self::Yuv420P
585            | Self::Yuv422P
586            | Self::Yuv444P
587            | Self::Yuv420P10Le
588            | Self::Yuv422P10Le
589            | Self::Yuv444P10Le
590            | Self::Yuv420P12Le
591            | Self::Yuv422P12Le
592            | Self::Yuv444P12Le
593            | Self::YuvJ420P
594            | Self::YuvJ422P
595            | Self::YuvJ444P => 3,
596            Self::Yuva420P => 4,
597            _ => 1,
598        }
599    }
600
601    /// Rough bits-per-pixel estimate, useful for buffer sizing. Not exact
602    /// for chroma-subsampled YUV — intended for worst-case preallocation
603    /// rather than wire-accurate accounting.
604    pub fn bits_per_pixel_approx(&self) -> u32 {
605        match self {
606            Self::MonoBlack | Self::MonoWhite => 1,
607            Self::Gray8 | Self::Pal8 => 8,
608            Self::Ya8 => 16,
609            Self::Gray16Le | Self::Gray10Le | Self::Gray12Le => 16,
610            Self::Rgb24 | Self::Bgr24 => 24,
611            Self::Rgba | Self::Bgra | Self::Argb | Self::Abgr => 32,
612            Self::Rgb48Le => 48,
613            Self::Rgba64Le => 64,
614            Self::Yuyv422 | Self::Uyvy422 => 16,
615            Self::Cmyk => 32,
616            // Planar YUV: 4:2:0 ≈ 12, 4:2:2 ≈ 16, 4:4:4 ≈ 24
617            // 10/12-bit variants double the byte count but we report the
618            // packed-bits-per-pixel estimate for a uniform heuristic.
619            Self::Yuv420P | Self::YuvJ420P | Self::Nv12 | Self::Nv21 => 12,
620            Self::Yuv422P | Self::YuvJ422P => 16,
621            Self::Yuv444P | Self::YuvJ444P => 24,
622            Self::Yuv420P10Le | Self::Yuv420P12Le => 24,
623            Self::Yuv422P10Le | Self::Yuv422P12Le => 32,
624            Self::Yuv444P10Le | Self::Yuv444P12Le => 48,
625            Self::Yuva420P => 20,
626        }
627    }
628}
629
630#[cfg(test)]
631mod tests {
632    use super::*;
633
634    /// Pin every `PixelFormat` and `SampleFormat` discriminant. This is the
635    /// stability commitment — the integer value of each variant is part of
636    /// the public ABI. Any reorder, renumber, or removal will fail this test
637    /// and the change MUST be a major version bump (or a fresh variant
638    /// appended at a new number, leaving the existing ones untouched).
639    #[test]
640    fn pixel_format_discriminants_pinned() {
641        assert_eq!(PixelFormat::Yuv420P as u16, 0);
642        assert_eq!(PixelFormat::Yuv422P as u16, 1);
643        assert_eq!(PixelFormat::Yuv444P as u16, 2);
644        assert_eq!(PixelFormat::Rgb24 as u16, 3);
645        assert_eq!(PixelFormat::Rgba as u16, 4);
646        assert_eq!(PixelFormat::Gray8 as u16, 5);
647        assert_eq!(PixelFormat::Pal8 as u16, 6);
648        assert_eq!(PixelFormat::Bgr24 as u16, 7);
649        assert_eq!(PixelFormat::Bgra as u16, 8);
650        assert_eq!(PixelFormat::Argb as u16, 9);
651        assert_eq!(PixelFormat::Abgr as u16, 10);
652        assert_eq!(PixelFormat::Rgb48Le as u16, 11);
653        assert_eq!(PixelFormat::Rgba64Le as u16, 12);
654        assert_eq!(PixelFormat::Gray16Le as u16, 13);
655        assert_eq!(PixelFormat::Gray10Le as u16, 14);
656        assert_eq!(PixelFormat::Gray12Le as u16, 15);
657        assert_eq!(PixelFormat::Yuv420P10Le as u16, 16);
658        assert_eq!(PixelFormat::Yuv422P10Le as u16, 17);
659        assert_eq!(PixelFormat::Yuv444P10Le as u16, 18);
660        assert_eq!(PixelFormat::Yuv420P12Le as u16, 19);
661        assert_eq!(PixelFormat::Yuv422P12Le as u16, 20);
662        assert_eq!(PixelFormat::Yuv444P12Le as u16, 21);
663        assert_eq!(PixelFormat::YuvJ420P as u16, 22);
664        assert_eq!(PixelFormat::YuvJ422P as u16, 23);
665        assert_eq!(PixelFormat::YuvJ444P as u16, 24);
666        assert_eq!(PixelFormat::Nv12 as u16, 25);
667        assert_eq!(PixelFormat::Nv21 as u16, 26);
668        assert_eq!(PixelFormat::Ya8 as u16, 27);
669        assert_eq!(PixelFormat::Yuva420P as u16, 28);
670        assert_eq!(PixelFormat::MonoBlack as u16, 29);
671        assert_eq!(PixelFormat::MonoWhite as u16, 30);
672        assert_eq!(PixelFormat::Yuyv422 as u16, 31);
673        assert_eq!(PixelFormat::Uyvy422 as u16, 32);
674        assert_eq!(PixelFormat::Cmyk as u16, 33);
675    }
676
677    #[test]
678    fn sample_format_discriminants_pinned() {
679        assert_eq!(SampleFormat::U8 as u8, 0);
680        assert_eq!(SampleFormat::S8 as u8, 1);
681        assert_eq!(SampleFormat::S16 as u8, 2);
682        assert_eq!(SampleFormat::S24 as u8, 3);
683        assert_eq!(SampleFormat::S32 as u8, 4);
684        assert_eq!(SampleFormat::F32 as u8, 5);
685        assert_eq!(SampleFormat::F64 as u8, 6);
686        assert_eq!(SampleFormat::U8P as u8, 7);
687        assert_eq!(SampleFormat::S16P as u8, 8);
688        assert_eq!(SampleFormat::S32P as u8, 9);
689        assert_eq!(SampleFormat::F32P as u8, 10);
690        assert_eq!(SampleFormat::F64P as u8, 11);
691    }
692
693    #[test]
694    fn high_bit_yuv_planar_metadata() {
695        // 10-bit reference variants are planar with three planes.
696        assert!(PixelFormat::Yuv420P10Le.is_planar());
697        assert!(PixelFormat::Yuv422P10Le.is_planar());
698        assert!(PixelFormat::Yuv444P10Le.is_planar());
699
700        // 12-bit variants must follow the same shape.
701        assert!(PixelFormat::Yuv420P12Le.is_planar());
702        assert!(PixelFormat::Yuv422P12Le.is_planar());
703        assert!(PixelFormat::Yuv444P12Le.is_planar());
704
705        assert_eq!(PixelFormat::Yuv420P12Le.plane_count(), 3);
706        assert_eq!(PixelFormat::Yuv422P12Le.plane_count(), 3);
707        assert_eq!(PixelFormat::Yuv444P12Le.plane_count(), 3);
708
709        // None of the high-bit YUV variants carry alpha or palette.
710        assert!(!PixelFormat::Yuv422P12Le.has_alpha());
711        assert!(!PixelFormat::Yuv444P12Le.has_alpha());
712        assert!(!PixelFormat::Yuv422P12Le.is_palette());
713        assert!(!PixelFormat::Yuv444P12Le.is_palette());
714    }
715
716    #[test]
717    fn channel_layout_round_trip_count_for_known_layouts() {
718        // For every `n` that `from_count` maps to a named layout, the
719        // resulting layout's `channel_count()` must equal `n` again.
720        for n in 1..=8u16 {
721            let layout = ChannelLayout::from_count(n);
722            assert_eq!(layout.channel_count(), n, "round-trip failed for n={n}");
723            // None of these defaults should fall through to DiscreteN.
724            assert!(
725                !matches!(layout, ChannelLayout::DiscreteN(_)),
726                "from_count({n}) unexpectedly produced DiscreteN"
727            );
728        }
729    }
730
731    #[test]
732    fn channel_layout_from_count_default_table() {
733        // The exact mapping documented on `from_count` — pin it so
734        // future refactors don't silently change the inferred layout.
735        assert_eq!(ChannelLayout::from_count(1), ChannelLayout::Mono);
736        assert_eq!(ChannelLayout::from_count(2), ChannelLayout::Stereo);
737        assert_eq!(ChannelLayout::from_count(3), ChannelLayout::Surround30);
738        assert_eq!(ChannelLayout::from_count(4), ChannelLayout::Quad);
739        assert_eq!(ChannelLayout::from_count(5), ChannelLayout::Surround50);
740        assert_eq!(ChannelLayout::from_count(6), ChannelLayout::Surround51);
741        assert_eq!(ChannelLayout::from_count(7), ChannelLayout::Surround61);
742        assert_eq!(ChannelLayout::from_count(8), ChannelLayout::Surround71);
743    }
744
745    #[test]
746    fn channel_layout_unknown_count_falls_through_to_discrete() {
747        assert_eq!(ChannelLayout::from_count(0), ChannelLayout::DiscreteN(0));
748        assert_eq!(ChannelLayout::from_count(13), ChannelLayout::DiscreteN(13));
749        assert_eq!(
750            ChannelLayout::from_count(64).channel_count(),
751            64,
752            "DiscreteN must report the count it was constructed with"
753        );
754    }
755
756    #[test]
757    fn channel_layout_position_lookup() {
758        assert_eq!(
759            ChannelLayout::Stereo.position(0),
760            Some(ChannelPosition::FrontLeft)
761        );
762        assert_eq!(
763            ChannelLayout::Stereo.position(1),
764            Some(ChannelPosition::FrontRight)
765        );
766        assert_eq!(ChannelLayout::Stereo.position(2), None);
767
768        // 5.1 canonical: L, R, C, LFE, Ls, Rs.
769        let s51 = ChannelLayout::Surround51;
770        assert_eq!(s51.position(0), Some(ChannelPosition::FrontLeft));
771        assert_eq!(s51.position(1), Some(ChannelPosition::FrontRight));
772        assert_eq!(s51.position(2), Some(ChannelPosition::FrontCenter));
773        assert_eq!(s51.position(3), Some(ChannelPosition::LowFrequency));
774        assert_eq!(s51.position(4), Some(ChannelPosition::SideLeft));
775        assert_eq!(s51.position(5), Some(ChannelPosition::SideRight));
776        assert_eq!(s51.position(6), None);
777
778        // DiscreteN never reveals a position.
779        assert_eq!(ChannelLayout::DiscreteN(13).position(0), None);
780    }
781
782    #[test]
783    fn channel_layout_lfe_and_surround_predicates() {
784        assert!(ChannelLayout::Surround51.has_lfe());
785        assert!(ChannelLayout::Surround71.has_lfe());
786        assert!(ChannelLayout::Stereo21.has_lfe());
787        assert!(!ChannelLayout::Quad.has_lfe());
788        assert!(!ChannelLayout::Surround50.has_lfe());
789        assert!(!ChannelLayout::Stereo.has_lfe());
790
791        assert!(!ChannelLayout::Mono.is_surround());
792        assert!(!ChannelLayout::Stereo.is_surround());
793        // Downmix carriers are still 2ch / no-LFE → not "surround" by
794        // the layout-shape definition; the surround info lives in the
795        // sample matrix itself.
796        assert!(!ChannelLayout::LoRo.is_surround());
797        assert!(!ChannelLayout::LtRt.is_surround());
798        assert!(ChannelLayout::Stereo21.is_surround());
799        assert!(ChannelLayout::Surround51.is_surround());
800        assert!(ChannelLayout::Surround71.is_surround());
801    }
802
803    #[test]
804    fn channel_layout_display_and_fromstr_round_trip() {
805        use std::str::FromStr;
806        let cases = [
807            ChannelLayout::Mono,
808            ChannelLayout::Stereo,
809            ChannelLayout::Stereo21,
810            ChannelLayout::Surround30,
811            ChannelLayout::Quad,
812            ChannelLayout::Surround40,
813            ChannelLayout::Surround41,
814            ChannelLayout::Surround50,
815            ChannelLayout::Surround51,
816            ChannelLayout::Surround60,
817            ChannelLayout::Surround61,
818            ChannelLayout::Surround70,
819            ChannelLayout::Surround71,
820            ChannelLayout::LoRo,
821            ChannelLayout::LtRt,
822            ChannelLayout::DiscreteN(13),
823        ];
824        for layout in cases {
825            let s = layout.to_string();
826            let parsed = ChannelLayout::from_str(&s).expect("display output must parse back");
827            assert_eq!(parsed, layout, "round-trip failed via {s:?}");
828        }
829    }
830
831    #[test]
832    fn channel_layout_fromstr_accepts_aliases_and_case() {
833        use std::str::FromStr;
834        assert_eq!(
835            ChannelLayout::from_str("STEREO").unwrap(),
836            ChannelLayout::Stereo
837        );
838        assert_eq!(
839            ChannelLayout::from_str("2.0").unwrap(),
840            ChannelLayout::Stereo
841        );
842        assert_eq!(
843            ChannelLayout::from_str("5.1").unwrap(),
844            ChannelLayout::Surround51
845        );
846        assert_eq!(
847            ChannelLayout::from_str("Lo/Ro").unwrap(),
848            ChannelLayout::LoRo
849        );
850        assert_eq!(
851            ChannelLayout::from_str("lt/rt").unwrap(),
852            ChannelLayout::LtRt
853        );
854        assert!(ChannelLayout::from_str("absurd_layout").is_err());
855    }
856
857    #[test]
858    fn channel_layout_positions_owned_matches_static_slice() {
859        for layout in [
860            ChannelLayout::Mono,
861            ChannelLayout::Surround51,
862            ChannelLayout::Surround71,
863        ] {
864            assert_eq!(layout.positions_owned(), layout.positions());
865        }
866        // DiscreteN returns an empty owned vec — positions are unknown.
867        assert!(ChannelLayout::DiscreteN(7).positions_owned().is_empty());
868    }
869
870    #[test]
871    fn sample_format_plane_count_interleaved_is_one() {
872        // Interleaved formats always pack into a single plane, regardless
873        // of channel count.
874        for ch in [1u16, 2, 6, 8, 64, 0] {
875            assert_eq!(SampleFormat::S16.plane_count(ch), 1);
876            assert_eq!(SampleFormat::F32.plane_count(ch), 1);
877            assert_eq!(SampleFormat::U8.plane_count(ch), 1);
878            assert_eq!(SampleFormat::S24.plane_count(ch), 1);
879        }
880    }
881
882    #[test]
883    fn sample_format_plane_count_planar_matches_channels() {
884        // Planar formats use one plane per channel.
885        assert_eq!(SampleFormat::S16P.plane_count(1), 1);
886        assert_eq!(SampleFormat::S16P.plane_count(2), 2);
887        assert_eq!(SampleFormat::F32P.plane_count(6), 6);
888        assert_eq!(SampleFormat::F64P.plane_count(8), 8);
889
890        // Edge case: zero channels in a planar format yields zero planes.
891        assert_eq!(SampleFormat::S32P.plane_count(0), 0);
892    }
893
894    #[test]
895    fn high_bit_yuv_bits_per_pixel_approx() {
896        // 4:2:2 and 4:4:4 12-bit match their 10-bit siblings on the
897        // packed-bits estimator (the approximation reports samples-per-pixel
898        // density, not the 16-bit storage width).
899        assert_eq!(PixelFormat::Yuv422P10Le.bits_per_pixel_approx(), 32);
900        assert_eq!(PixelFormat::Yuv422P12Le.bits_per_pixel_approx(), 32);
901        assert_eq!(PixelFormat::Yuv444P10Le.bits_per_pixel_approx(), 48);
902        assert_eq!(PixelFormat::Yuv444P12Le.bits_per_pixel_approx(), 48);
903        assert_eq!(PixelFormat::Yuv420P12Le.bits_per_pixel_approx(), 24);
904    }
905}