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}