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}