Skip to main content

oximedia_core/
sample_format.rs

1//! Audio and video sample format definitions.
2//!
3//! This module provides [`VideoPixelFormat`], [`AudioSampleFormat`], and
4//! [`SampleFormatInfo`] for describing the pixel/sample layouts of media frames.
5
6#![allow(dead_code)]
7#![allow(clippy::cast_precision_loss)]
8
9/// Pixel format for video frames.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum VideoPixelFormat {
12    /// YUV 4:2:0 planar (3 planes: Y, Cb, Cr).
13    Yuv420p,
14    /// YUV 4:2:2 planar (3 planes: Y, Cb, Cr).
15    Yuv422p,
16    /// YUV 4:4:4 planar (3 planes: Y, Cb, Cr).
17    Yuv444p,
18    /// Packed RGBA (4 bytes per pixel).
19    Rgba,
20    /// Packed RGB 24-bit (3 bytes per pixel).
21    Rgb24,
22    /// Semi-planar YUV 4:2:0 (2 planes: Y, interleaved `CbCr`).
23    Nv12,
24    /// 10-bit semi-planar YUV 4:2:0 (P010 little-endian, 2 planes).
25    P010,
26}
27
28impl VideoPixelFormat {
29    /// Returns the number of bits used per pixel.
30    ///
31    /// For planar formats this is the average over all planes.
32    #[must_use]
33    pub fn bits_per_pixel(&self) -> u8 {
34        match self {
35            Self::Yuv420p | Self::Nv12 => 12,
36            Self::Yuv422p => 16,
37            Self::Yuv444p | Self::Rgb24 => 24,
38            Self::Rgba => 32,
39            Self::P010 => 15, // 10-bit 4:2:0 → 15 bits on average
40        }
41    }
42
43    /// Returns `true` if the format uses separate planes for each component.
44    #[must_use]
45    pub fn is_planar(&self) -> bool {
46        matches!(
47            self,
48            Self::Yuv420p | Self::Yuv422p | Self::Yuv444p | Self::Nv12 | Self::P010
49        )
50    }
51
52    /// Returns `(horizontal, vertical)` chroma subsampling factors.
53    ///
54    /// A factor of 2 means chroma is halved in that dimension.
55    /// Packed RGB formats return `(1, 1)` (no subsampling).
56    #[must_use]
57    pub fn chroma_subsampling(&self) -> (u8, u8) {
58        match self {
59            Self::Yuv420p | Self::Nv12 | Self::P010 => (2, 2),
60            Self::Yuv422p => (2, 1),
61            Self::Yuv444p | Self::Rgba | Self::Rgb24 => (1, 1),
62        }
63    }
64}
65
66/// Sample format for audio frames.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
68pub enum AudioSampleFormat {
69    /// Signed 16-bit integer, interleaved.
70    S16,
71    /// Signed 32-bit integer, interleaved.
72    S32,
73    /// 32-bit float, interleaved.
74    F32,
75    /// 64-bit float, interleaved.
76    F64,
77    /// Signed 16-bit integer, planar.
78    S16P,
79    /// Signed 32-bit integer, planar.
80    S32P,
81    /// 32-bit float, planar.
82    F32P,
83}
84
85impl AudioSampleFormat {
86    /// Returns the number of bytes per audio sample.
87    #[must_use]
88    pub fn bytes_per_sample(&self) -> u8 {
89        match self {
90            Self::S16 | Self::S16P => 2,
91            Self::S32 | Self::F32 | Self::S32P | Self::F32P => 4,
92            Self::F64 => 8,
93        }
94    }
95
96    /// Returns `true` if the format stores samples in separate per-channel planes.
97    #[must_use]
98    pub fn is_planar(&self) -> bool {
99        matches!(self, Self::S16P | Self::S32P | Self::F32P)
100    }
101
102    /// Returns `true` if the samples are floating-point values.
103    #[must_use]
104    pub fn is_float(&self) -> bool {
105        matches!(self, Self::F32 | Self::F64 | Self::F32P)
106    }
107}
108
109/// Combined format descriptor that may carry video, audio, or both.
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct SampleFormatInfo {
112    /// Optional video pixel format.
113    pub video: Option<VideoPixelFormat>,
114    /// Optional audio sample format.
115    pub audio: Option<AudioSampleFormat>,
116}
117
118impl SampleFormatInfo {
119    /// Creates a new `SampleFormatInfo`.
120    #[must_use]
121    pub fn new(video: Option<VideoPixelFormat>, audio: Option<AudioSampleFormat>) -> Self {
122        Self { video, audio }
123    }
124
125    /// Returns `true` if a video format is present.
126    #[must_use]
127    pub fn has_video(&self) -> bool {
128        self.video.is_some()
129    }
130
131    /// Returns `true` if an audio format is present.
132    #[must_use]
133    pub fn has_audio(&self) -> bool {
134        self.audio.is_some()
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    // --- VideoPixelFormat ---
143
144    #[test]
145    fn test_yuv420p_bits_per_pixel() {
146        assert_eq!(VideoPixelFormat::Yuv420p.bits_per_pixel(), 12);
147    }
148
149    #[test]
150    fn test_rgba_bits_per_pixel() {
151        assert_eq!(VideoPixelFormat::Rgba.bits_per_pixel(), 32);
152    }
153
154    #[test]
155    fn test_rgb24_bits_per_pixel() {
156        assert_eq!(VideoPixelFormat::Rgb24.bits_per_pixel(), 24);
157    }
158
159    #[test]
160    fn test_yuv422p_bits_per_pixel() {
161        assert_eq!(VideoPixelFormat::Yuv422p.bits_per_pixel(), 16);
162    }
163
164    #[test]
165    fn test_nv12_is_planar() {
166        assert!(VideoPixelFormat::Nv12.is_planar());
167    }
168
169    #[test]
170    fn test_rgba_is_not_planar() {
171        assert!(!VideoPixelFormat::Rgba.is_planar());
172    }
173
174    #[test]
175    fn test_yuv420p_chroma_subsampling() {
176        assert_eq!(VideoPixelFormat::Yuv420p.chroma_subsampling(), (2, 2));
177    }
178
179    #[test]
180    fn test_yuv422p_chroma_subsampling() {
181        assert_eq!(VideoPixelFormat::Yuv422p.chroma_subsampling(), (2, 1));
182    }
183
184    #[test]
185    fn test_yuv444p_chroma_subsampling() {
186        assert_eq!(VideoPixelFormat::Yuv444p.chroma_subsampling(), (1, 1));
187    }
188
189    #[test]
190    fn test_p010_bits_and_subsampling() {
191        assert_eq!(VideoPixelFormat::P010.bits_per_pixel(), 15);
192        assert_eq!(VideoPixelFormat::P010.chroma_subsampling(), (2, 2));
193    }
194
195    // --- AudioSampleFormat ---
196
197    #[test]
198    fn test_s16_bytes_per_sample() {
199        assert_eq!(AudioSampleFormat::S16.bytes_per_sample(), 2);
200    }
201
202    #[test]
203    fn test_f64_bytes_per_sample() {
204        assert_eq!(AudioSampleFormat::F64.bytes_per_sample(), 8);
205    }
206
207    #[test]
208    fn test_f32p_is_planar_and_float() {
209        assert!(AudioSampleFormat::F32P.is_planar());
210        assert!(AudioSampleFormat::F32P.is_float());
211    }
212
213    #[test]
214    fn test_s32_is_not_planar_not_float() {
215        assert!(!AudioSampleFormat::S32.is_planar());
216        assert!(!AudioSampleFormat::S32.is_float());
217    }
218
219    // --- SampleFormatInfo ---
220
221    #[test]
222    fn test_sample_format_info_has_video() {
223        let info = SampleFormatInfo::new(Some(VideoPixelFormat::Yuv420p), None);
224        assert!(info.has_video());
225        assert!(!info.has_audio());
226    }
227
228    #[test]
229    fn test_sample_format_info_has_audio() {
230        let info = SampleFormatInfo::new(None, Some(AudioSampleFormat::F32));
231        assert!(!info.has_video());
232        assert!(info.has_audio());
233    }
234
235    #[test]
236    fn test_sample_format_info_both() {
237        let info =
238            SampleFormatInfo::new(Some(VideoPixelFormat::Nv12), Some(AudioSampleFormat::S16P));
239        assert!(info.has_video());
240        assert!(info.has_audio());
241    }
242
243    #[test]
244    fn test_sample_format_info_neither() {
245        let info = SampleFormatInfo::new(None, None);
246        assert!(!info.has_video());
247        assert!(!info.has_audio());
248    }
249}