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 // --- Planar GBR / GBRA (RGB stored as planes in G,B,R order) ---
544 //
545 // High-bit-depth GBR(A) layouts used by MagicYUV, JPEG 2000, OpenEXR,
546 // TIFF and similar workflows that need lossless RGB at 10/12/14 bits
547 // per channel. Planes are ordered G, B, R (and A for the `Gbrap*`
548 // variants) — matching the ffmpeg `AV_PIX_FMT_GBRP*LE` family — and
549 // each sample is stored as a 16-bit little-endian word with the
550 // top bits zero. There is no native 8-bit `Gbrp` variant in this
551 // enum yet because no in-tree codec needs it; if one is added later
552 // it must be appended at a fresh discriminant.
553 /// 10-bit planar GBR, little-endian 16-bit storage. 3 planes ordered
554 /// G, B, R; each sample uses the low 10 bits of a 16-bit word.
555 Gbrp10Le = 35,
556 /// 10-bit planar GBR + alpha, little-endian 16-bit storage. 4 planes
557 /// ordered G, B, R, A; each sample uses the low 10 bits of a 16-bit
558 /// word.
559 Gbrap10Le = 36,
560 /// 12-bit planar GBR, little-endian 16-bit storage. 3 planes ordered
561 /// G, B, R; each sample uses the low 12 bits of a 16-bit word.
562 Gbrp12Le = 37,
563 /// 12-bit planar GBR + alpha, little-endian 16-bit storage. 4 planes
564 /// ordered G, B, R, A; each sample uses the low 12 bits of a 16-bit
565 /// word.
566 Gbrap12Le = 38,
567 /// 14-bit planar GBR, little-endian 16-bit storage. 3 planes ordered
568 /// G, B, R; each sample uses the low 14 bits of a 16-bit word.
569 Gbrp14Le = 39,
570 /// 14-bit planar GBR + alpha, little-endian 16-bit storage. 4 planes
571 /// ordered G, B, R, A; each sample uses the low 14 bits of a 16-bit
572 /// word.
573 Gbrap14Le = 40,
574}
575
576impl PixelFormat {
577 /// True if this format stores its components in separate planes.
578 pub fn is_planar(&self) -> bool {
579 matches!(
580 self,
581 Self::Yuv420P
582 | Self::Yuv422P
583 | Self::Yuv444P
584 | Self::Yuv411P
585 | Self::Yuv420P10Le
586 | Self::Yuv422P10Le
587 | Self::Yuv444P10Le
588 | Self::Yuv420P12Le
589 | Self::Yuv422P12Le
590 | Self::Yuv444P12Le
591 | Self::YuvJ420P
592 | Self::YuvJ422P
593 | Self::YuvJ444P
594 | Self::Nv12
595 | Self::Nv21
596 | Self::Yuva420P
597 | Self::Gbrp10Le
598 | Self::Gbrap10Le
599 | Self::Gbrp12Le
600 | Self::Gbrap12Le
601 | Self::Gbrp14Le
602 | Self::Gbrap14Le
603 )
604 }
605
606 /// True if the format is a palette index format (`Pal8`).
607 pub fn is_palette(&self) -> bool {
608 matches!(self, Self::Pal8)
609 }
610
611 /// True if this format carries an alpha channel.
612 pub fn has_alpha(&self) -> bool {
613 matches!(
614 self,
615 Self::Rgba
616 | Self::Bgra
617 | Self::Argb
618 | Self::Abgr
619 | Self::Rgba64Le
620 | Self::Ya8
621 | Self::Yuva420P
622 | Self::Gbrap10Le
623 | Self::Gbrap12Le
624 | Self::Gbrap14Le
625 )
626 }
627
628 /// Number of planes in the stored layout. Packed and palette formats
629 /// return 1; NV12/NV21 return 2; planar YUV without alpha and the
630 /// `Gbrp*` variants return 3; YuvA and `Gbrap*` variants return 4.
631 pub fn plane_count(&self) -> usize {
632 match self {
633 Self::Nv12 | Self::Nv21 => 2,
634 Self::Yuv420P
635 | Self::Yuv422P
636 | Self::Yuv444P
637 | Self::Yuv411P
638 | Self::Yuv420P10Le
639 | Self::Yuv422P10Le
640 | Self::Yuv444P10Le
641 | Self::Yuv420P12Le
642 | Self::Yuv422P12Le
643 | Self::Yuv444P12Le
644 | Self::YuvJ420P
645 | Self::YuvJ422P
646 | Self::YuvJ444P
647 | Self::Gbrp10Le
648 | Self::Gbrp12Le
649 | Self::Gbrp14Le => 3,
650 Self::Yuva420P | Self::Gbrap10Le | Self::Gbrap12Le | Self::Gbrap14Le => 4,
651 _ => 1,
652 }
653 }
654
655 /// Rough bits-per-pixel estimate, useful for buffer sizing. Not exact
656 /// for chroma-subsampled YUV — intended for worst-case preallocation
657 /// rather than wire-accurate accounting.
658 pub fn bits_per_pixel_approx(&self) -> u32 {
659 match self {
660 Self::MonoBlack | Self::MonoWhite => 1,
661 Self::Gray8 | Self::Pal8 => 8,
662 Self::Ya8 => 16,
663 Self::Gray16Le | Self::Gray10Le | Self::Gray12Le => 16,
664 Self::Rgb24 | Self::Bgr24 => 24,
665 Self::Rgba | Self::Bgra | Self::Argb | Self::Abgr => 32,
666 Self::Rgb48Le => 48,
667 Self::Rgba64Le => 64,
668 Self::Yuyv422 | Self::Uyvy422 => 16,
669 Self::Cmyk => 32,
670 // Planar YUV: 4:2:0 ≈ 12, 4:2:2 ≈ 16, 4:4:4 ≈ 24
671 // 10/12-bit variants double the byte count but we report the
672 // packed-bits-per-pixel estimate for a uniform heuristic.
673 Self::Yuv420P | Self::YuvJ420P | Self::Nv12 | Self::Nv21 => 12,
674 // 4:1:1 has the same packed bits-per-pixel as 4:2:0 (luma at
675 // full res + 2 chroma planes each subsampled by 4).
676 Self::Yuv411P => 12,
677 Self::Yuv422P | Self::YuvJ422P => 16,
678 Self::Yuv444P | Self::YuvJ444P => 24,
679 Self::Yuv420P10Le | Self::Yuv420P12Le => 24,
680 Self::Yuv422P10Le | Self::Yuv422P12Le => 32,
681 Self::Yuv444P10Le | Self::Yuv444P12Le => 48,
682 Self::Yuva420P => 20,
683 // Planar GBR(A) at 10/12/14 bits stored in 16-bit words: we
684 // report the packed bits-per-pixel density (samples × bits)
685 // rather than the 16-bit storage cost, matching how the
686 // 10/12-bit YUV variants are reported above.
687 Self::Gbrp10Le => 30,
688 Self::Gbrap10Le => 40,
689 Self::Gbrp12Le => 36,
690 Self::Gbrap12Le => 48,
691 Self::Gbrp14Le => 42,
692 Self::Gbrap14Le => 56,
693 }
694 }
695}
696
697#[cfg(test)]
698mod tests {
699 use super::*;
700
701 /// Pin every `PixelFormat` and `SampleFormat` discriminant. This is the
702 /// stability commitment — the integer value of each variant is part of
703 /// the public ABI. Any reorder, renumber, or removal will fail this test
704 /// and the change MUST be a major version bump (or a fresh variant
705 /// appended at a new number, leaving the existing ones untouched).
706 #[test]
707 fn pixel_format_discriminants_pinned() {
708 assert_eq!(PixelFormat::Yuv420P as u16, 0);
709 assert_eq!(PixelFormat::Yuv422P as u16, 1);
710 assert_eq!(PixelFormat::Yuv444P as u16, 2);
711 assert_eq!(PixelFormat::Rgb24 as u16, 3);
712 assert_eq!(PixelFormat::Rgba as u16, 4);
713 assert_eq!(PixelFormat::Gray8 as u16, 5);
714 assert_eq!(PixelFormat::Pal8 as u16, 6);
715 assert_eq!(PixelFormat::Bgr24 as u16, 7);
716 assert_eq!(PixelFormat::Bgra as u16, 8);
717 assert_eq!(PixelFormat::Argb as u16, 9);
718 assert_eq!(PixelFormat::Abgr as u16, 10);
719 assert_eq!(PixelFormat::Rgb48Le as u16, 11);
720 assert_eq!(PixelFormat::Rgba64Le as u16, 12);
721 assert_eq!(PixelFormat::Gray16Le as u16, 13);
722 assert_eq!(PixelFormat::Gray10Le as u16, 14);
723 assert_eq!(PixelFormat::Gray12Le as u16, 15);
724 assert_eq!(PixelFormat::Yuv420P10Le as u16, 16);
725 assert_eq!(PixelFormat::Yuv422P10Le as u16, 17);
726 assert_eq!(PixelFormat::Yuv444P10Le as u16, 18);
727 assert_eq!(PixelFormat::Yuv420P12Le as u16, 19);
728 assert_eq!(PixelFormat::Yuv422P12Le as u16, 20);
729 assert_eq!(PixelFormat::Yuv444P12Le as u16, 21);
730 assert_eq!(PixelFormat::YuvJ420P as u16, 22);
731 assert_eq!(PixelFormat::YuvJ422P as u16, 23);
732 assert_eq!(PixelFormat::YuvJ444P as u16, 24);
733 assert_eq!(PixelFormat::Nv12 as u16, 25);
734 assert_eq!(PixelFormat::Nv21 as u16, 26);
735 assert_eq!(PixelFormat::Ya8 as u16, 27);
736 assert_eq!(PixelFormat::Yuva420P as u16, 28);
737 assert_eq!(PixelFormat::MonoBlack as u16, 29);
738 assert_eq!(PixelFormat::MonoWhite as u16, 30);
739 assert_eq!(PixelFormat::Yuyv422 as u16, 31);
740 assert_eq!(PixelFormat::Uyvy422 as u16, 32);
741 assert_eq!(PixelFormat::Cmyk as u16, 33);
742 assert_eq!(PixelFormat::Yuv411P as u16, 34);
743 assert_eq!(PixelFormat::Gbrp10Le as u16, 35);
744 assert_eq!(PixelFormat::Gbrap10Le as u16, 36);
745 assert_eq!(PixelFormat::Gbrp12Le as u16, 37);
746 assert_eq!(PixelFormat::Gbrap12Le as u16, 38);
747 assert_eq!(PixelFormat::Gbrp14Le as u16, 39);
748 assert_eq!(PixelFormat::Gbrap14Le as u16, 40);
749 }
750
751 #[test]
752 fn sample_format_discriminants_pinned() {
753 assert_eq!(SampleFormat::U8 as u8, 0);
754 assert_eq!(SampleFormat::S8 as u8, 1);
755 assert_eq!(SampleFormat::S16 as u8, 2);
756 assert_eq!(SampleFormat::S24 as u8, 3);
757 assert_eq!(SampleFormat::S32 as u8, 4);
758 assert_eq!(SampleFormat::F32 as u8, 5);
759 assert_eq!(SampleFormat::F64 as u8, 6);
760 assert_eq!(SampleFormat::U8P as u8, 7);
761 assert_eq!(SampleFormat::S16P as u8, 8);
762 assert_eq!(SampleFormat::S32P as u8, 9);
763 assert_eq!(SampleFormat::F32P as u8, 10);
764 assert_eq!(SampleFormat::F64P as u8, 11);
765 }
766
767 #[test]
768 fn high_bit_yuv_planar_metadata() {
769 // 10-bit reference variants are planar with three planes.
770 assert!(PixelFormat::Yuv420P10Le.is_planar());
771 assert!(PixelFormat::Yuv422P10Le.is_planar());
772 assert!(PixelFormat::Yuv444P10Le.is_planar());
773
774 // 12-bit variants must follow the same shape.
775 assert!(PixelFormat::Yuv420P12Le.is_planar());
776 assert!(PixelFormat::Yuv422P12Le.is_planar());
777 assert!(PixelFormat::Yuv444P12Le.is_planar());
778
779 assert_eq!(PixelFormat::Yuv420P12Le.plane_count(), 3);
780 assert_eq!(PixelFormat::Yuv422P12Le.plane_count(), 3);
781 assert_eq!(PixelFormat::Yuv444P12Le.plane_count(), 3);
782
783 // None of the high-bit YUV variants carry alpha or palette.
784 assert!(!PixelFormat::Yuv422P12Le.has_alpha());
785 assert!(!PixelFormat::Yuv444P12Le.has_alpha());
786 assert!(!PixelFormat::Yuv422P12Le.is_palette());
787 assert!(!PixelFormat::Yuv444P12Le.is_palette());
788 }
789
790 #[test]
791 fn channel_layout_round_trip_count_for_known_layouts() {
792 // For every `n` that `from_count` maps to a named layout, the
793 // resulting layout's `channel_count()` must equal `n` again.
794 for n in 1..=8u16 {
795 let layout = ChannelLayout::from_count(n);
796 assert_eq!(layout.channel_count(), n, "round-trip failed for n={n}");
797 // None of these defaults should fall through to DiscreteN.
798 assert!(
799 !matches!(layout, ChannelLayout::DiscreteN(_)),
800 "from_count({n}) unexpectedly produced DiscreteN"
801 );
802 }
803 }
804
805 #[test]
806 fn channel_layout_from_count_default_table() {
807 // The exact mapping documented on `from_count` — pin it so
808 // future refactors don't silently change the inferred layout.
809 assert_eq!(ChannelLayout::from_count(1), ChannelLayout::Mono);
810 assert_eq!(ChannelLayout::from_count(2), ChannelLayout::Stereo);
811 assert_eq!(ChannelLayout::from_count(3), ChannelLayout::Surround30);
812 assert_eq!(ChannelLayout::from_count(4), ChannelLayout::Quad);
813 assert_eq!(ChannelLayout::from_count(5), ChannelLayout::Surround50);
814 assert_eq!(ChannelLayout::from_count(6), ChannelLayout::Surround51);
815 assert_eq!(ChannelLayout::from_count(7), ChannelLayout::Surround61);
816 assert_eq!(ChannelLayout::from_count(8), ChannelLayout::Surround71);
817 }
818
819 #[test]
820 fn channel_layout_unknown_count_falls_through_to_discrete() {
821 assert_eq!(ChannelLayout::from_count(0), ChannelLayout::DiscreteN(0));
822 assert_eq!(ChannelLayout::from_count(13), ChannelLayout::DiscreteN(13));
823 assert_eq!(
824 ChannelLayout::from_count(64).channel_count(),
825 64,
826 "DiscreteN must report the count it was constructed with"
827 );
828 }
829
830 #[test]
831 fn channel_layout_position_lookup() {
832 assert_eq!(
833 ChannelLayout::Stereo.position(0),
834 Some(ChannelPosition::FrontLeft)
835 );
836 assert_eq!(
837 ChannelLayout::Stereo.position(1),
838 Some(ChannelPosition::FrontRight)
839 );
840 assert_eq!(ChannelLayout::Stereo.position(2), None);
841
842 // 5.1 canonical: L, R, C, LFE, Ls, Rs.
843 let s51 = ChannelLayout::Surround51;
844 assert_eq!(s51.position(0), Some(ChannelPosition::FrontLeft));
845 assert_eq!(s51.position(1), Some(ChannelPosition::FrontRight));
846 assert_eq!(s51.position(2), Some(ChannelPosition::FrontCenter));
847 assert_eq!(s51.position(3), Some(ChannelPosition::LowFrequency));
848 assert_eq!(s51.position(4), Some(ChannelPosition::SideLeft));
849 assert_eq!(s51.position(5), Some(ChannelPosition::SideRight));
850 assert_eq!(s51.position(6), None);
851
852 // DiscreteN never reveals a position.
853 assert_eq!(ChannelLayout::DiscreteN(13).position(0), None);
854 }
855
856 #[test]
857 fn channel_layout_lfe_and_surround_predicates() {
858 assert!(ChannelLayout::Surround51.has_lfe());
859 assert!(ChannelLayout::Surround71.has_lfe());
860 assert!(ChannelLayout::Stereo21.has_lfe());
861 assert!(!ChannelLayout::Quad.has_lfe());
862 assert!(!ChannelLayout::Surround50.has_lfe());
863 assert!(!ChannelLayout::Stereo.has_lfe());
864
865 assert!(!ChannelLayout::Mono.is_surround());
866 assert!(!ChannelLayout::Stereo.is_surround());
867 // Downmix carriers are still 2ch / no-LFE → not "surround" by
868 // the layout-shape definition; the surround info lives in the
869 // sample matrix itself.
870 assert!(!ChannelLayout::LoRo.is_surround());
871 assert!(!ChannelLayout::LtRt.is_surround());
872 assert!(ChannelLayout::Stereo21.is_surround());
873 assert!(ChannelLayout::Surround51.is_surround());
874 assert!(ChannelLayout::Surround71.is_surround());
875 }
876
877 #[test]
878 fn channel_layout_display_and_fromstr_round_trip() {
879 use std::str::FromStr;
880 let cases = [
881 ChannelLayout::Mono,
882 ChannelLayout::Stereo,
883 ChannelLayout::Stereo21,
884 ChannelLayout::Surround30,
885 ChannelLayout::Quad,
886 ChannelLayout::Surround40,
887 ChannelLayout::Surround41,
888 ChannelLayout::Surround50,
889 ChannelLayout::Surround51,
890 ChannelLayout::Surround60,
891 ChannelLayout::Surround61,
892 ChannelLayout::Surround70,
893 ChannelLayout::Surround71,
894 ChannelLayout::LoRo,
895 ChannelLayout::LtRt,
896 ChannelLayout::DiscreteN(13),
897 ];
898 for layout in cases {
899 let s = layout.to_string();
900 let parsed = ChannelLayout::from_str(&s).expect("display output must parse back");
901 assert_eq!(parsed, layout, "round-trip failed via {s:?}");
902 }
903 }
904
905 #[test]
906 fn channel_layout_fromstr_accepts_aliases_and_case() {
907 use std::str::FromStr;
908 assert_eq!(
909 ChannelLayout::from_str("STEREO").unwrap(),
910 ChannelLayout::Stereo
911 );
912 assert_eq!(
913 ChannelLayout::from_str("2.0").unwrap(),
914 ChannelLayout::Stereo
915 );
916 assert_eq!(
917 ChannelLayout::from_str("5.1").unwrap(),
918 ChannelLayout::Surround51
919 );
920 assert_eq!(
921 ChannelLayout::from_str("Lo/Ro").unwrap(),
922 ChannelLayout::LoRo
923 );
924 assert_eq!(
925 ChannelLayout::from_str("lt/rt").unwrap(),
926 ChannelLayout::LtRt
927 );
928 assert!(ChannelLayout::from_str("absurd_layout").is_err());
929 }
930
931 #[test]
932 fn channel_layout_positions_owned_matches_static_slice() {
933 for layout in [
934 ChannelLayout::Mono,
935 ChannelLayout::Surround51,
936 ChannelLayout::Surround71,
937 ] {
938 assert_eq!(layout.positions_owned(), layout.positions());
939 }
940 // DiscreteN returns an empty owned vec — positions are unknown.
941 assert!(ChannelLayout::DiscreteN(7).positions_owned().is_empty());
942 }
943
944 #[test]
945 fn sample_format_plane_count_interleaved_is_one() {
946 // Interleaved formats always pack into a single plane, regardless
947 // of channel count.
948 for ch in [1u16, 2, 6, 8, 64, 0] {
949 assert_eq!(SampleFormat::S16.plane_count(ch), 1);
950 assert_eq!(SampleFormat::F32.plane_count(ch), 1);
951 assert_eq!(SampleFormat::U8.plane_count(ch), 1);
952 assert_eq!(SampleFormat::S24.plane_count(ch), 1);
953 }
954 }
955
956 #[test]
957 fn sample_format_plane_count_planar_matches_channels() {
958 // Planar formats use one plane per channel.
959 assert_eq!(SampleFormat::S16P.plane_count(1), 1);
960 assert_eq!(SampleFormat::S16P.plane_count(2), 2);
961 assert_eq!(SampleFormat::F32P.plane_count(6), 6);
962 assert_eq!(SampleFormat::F64P.plane_count(8), 8);
963
964 // Edge case: zero channels in a planar format yields zero planes.
965 assert_eq!(SampleFormat::S32P.plane_count(0), 0);
966 }
967
968 #[test]
969 fn high_bit_yuv_bits_per_pixel_approx() {
970 // 4:2:2 and 4:4:4 12-bit match their 10-bit siblings on the
971 // packed-bits estimator (the approximation reports samples-per-pixel
972 // density, not the 16-bit storage width).
973 assert_eq!(PixelFormat::Yuv422P10Le.bits_per_pixel_approx(), 32);
974 assert_eq!(PixelFormat::Yuv422P12Le.bits_per_pixel_approx(), 32);
975 assert_eq!(PixelFormat::Yuv444P10Le.bits_per_pixel_approx(), 48);
976 assert_eq!(PixelFormat::Yuv444P12Le.bits_per_pixel_approx(), 48);
977 assert_eq!(PixelFormat::Yuv420P12Le.bits_per_pixel_approx(), 24);
978 }
979
980 #[test]
981 fn high_bit_gbr_planar_metadata() {
982 // All six new variants are planar with the right plane count.
983 for fmt in [
984 PixelFormat::Gbrp10Le,
985 PixelFormat::Gbrp12Le,
986 PixelFormat::Gbrp14Le,
987 ] {
988 assert!(fmt.is_planar(), "{fmt:?} must be planar");
989 assert_eq!(fmt.plane_count(), 3, "{fmt:?} must have 3 planes");
990 assert!(!fmt.has_alpha(), "{fmt:?} must not have alpha");
991 assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
992 }
993 for fmt in [
994 PixelFormat::Gbrap10Le,
995 PixelFormat::Gbrap12Le,
996 PixelFormat::Gbrap14Le,
997 ] {
998 assert!(fmt.is_planar(), "{fmt:?} must be planar");
999 assert_eq!(fmt.plane_count(), 4, "{fmt:?} must have 4 planes");
1000 assert!(fmt.has_alpha(), "{fmt:?} must carry alpha");
1001 assert!(!fmt.is_palette(), "{fmt:?} must not be palette");
1002 }
1003 }
1004
1005 #[test]
1006 fn high_bit_gbr_bits_per_pixel_approx() {
1007 // Packed bits-per-pixel = samples × bits (consistent with how
1008 // the 10/12-bit YUV variants are reported above).
1009 assert_eq!(PixelFormat::Gbrp10Le.bits_per_pixel_approx(), 30);
1010 assert_eq!(PixelFormat::Gbrap10Le.bits_per_pixel_approx(), 40);
1011 assert_eq!(PixelFormat::Gbrp12Le.bits_per_pixel_approx(), 36);
1012 assert_eq!(PixelFormat::Gbrap12Le.bits_per_pixel_approx(), 48);
1013 assert_eq!(PixelFormat::Gbrp14Le.bits_per_pixel_approx(), 42);
1014 assert_eq!(PixelFormat::Gbrap14Le.bits_per_pixel_approx(), 56);
1015 }
1016
1017 #[test]
1018 fn high_bit_gbr_constructible_and_distinct() {
1019 // Round-trip the discriminant through `as u16` and back via the
1020 // pinning test's reverse mapping — every variant must be unique.
1021 let all = [
1022 PixelFormat::Gbrp10Le,
1023 PixelFormat::Gbrap10Le,
1024 PixelFormat::Gbrp12Le,
1025 PixelFormat::Gbrap12Le,
1026 PixelFormat::Gbrp14Le,
1027 PixelFormat::Gbrap14Le,
1028 ];
1029 let mut seen = std::collections::HashSet::new();
1030 for fmt in all {
1031 assert!(seen.insert(fmt as u16), "duplicate discriminant: {fmt:?}");
1032 }
1033 }
1034}