Skip to main content

mediaframe/audio/format/
mod.rs

1//! Audio sample-format vocabulary (`SampleFormat`, FFmpeg
2//! `AVSampleFormat`) and audio-only container-format vocabulary
3//! (`ContainerFormat`, audio file extensions).
4
5use core::str::FromStr;
6
7use derive_more::{Display, IsVariant, TryUnwrap, Unwrap};
8use smol_str::SmolStr;
9
10/// Audio sample format — FFmpeg `AVSampleFormat`.
11///
12/// One named variant per FFmpeg n9.0 sample format (the standard 12
13/// — `u8`/`s16`/`s32`/`s64` × packed/planar plus `flt`/`dbl` ×
14/// packed/planar), with the planar variants suffixed `p` per FFmpeg
15/// convention.
16///
17/// `to_u32` / `from_u32` use the FFmpeg `AV_SAMPLE_FMT_*` enum
18/// indices (`U8 = 0`, `S16 = 1`, …, `S64P = 11`); unrecognised
19/// codes round-trip via [`Self::Unknown`]. Slugs that don't match
20/// any named variant round-trip via [`Self::Other`].
21///
22/// `#[non_exhaustive]` keeps future additions non-breaking.
23#[cfg_attr(
24  feature = "quickcheck",
25  derive(::quickcheck_richderive::Arbitrary),
26  quickcheck(arbitrary = "crate::quickcheck_helpers::strings::sample_format")
27)]
28#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, IsVariant, Unwrap, TryUnwrap)]
29#[display("{}", self.as_str())]
30#[unwrap(ref, ref_mut)]
31#[try_unwrap(ref, ref_mut)]
32#[non_exhaustive]
33pub enum SampleFormat {
34  /// `AV_SAMPLE_FMT_U8` (code `0`) — unsigned 8-bit, packed.
35  U8,
36  /// `AV_SAMPLE_FMT_S16` (code `1`) — signed 16-bit, packed.
37  S16,
38  /// `AV_SAMPLE_FMT_S32` (code `2`) — signed 32-bit, packed.
39  S32,
40  /// `AV_SAMPLE_FMT_FLT` (code `3`) — 32-bit float, packed.
41  Flt,
42  /// `AV_SAMPLE_FMT_DBL` (code `4`) — 64-bit float, packed.
43  Dbl,
44  /// `AV_SAMPLE_FMT_U8P` (code `5`) — unsigned 8-bit, planar.
45  U8p,
46  /// `AV_SAMPLE_FMT_S16P` (code `6`) — signed 16-bit, planar.
47  S16p,
48  /// `AV_SAMPLE_FMT_S32P` (code `7`) — signed 32-bit, planar.
49  S32p,
50  /// `AV_SAMPLE_FMT_FLTP` (code `8`) — 32-bit float, planar.
51  Fltp,
52  /// `AV_SAMPLE_FMT_DBLP` (code `9`) — 64-bit float, planar.
53  Dblp,
54  /// `AV_SAMPLE_FMT_S64` (code `10`) — signed 64-bit, packed.
55  S64,
56  /// `AV_SAMPLE_FMT_S64P` (code `11`) — signed 64-bit, planar.
57  S64p,
58  /// A format slug not enumerated above — carries the slug verbatim
59  /// (the [`Self::from_str`] lossless escape).
60  Other(SmolStr),
61}
62
63impl Default for SampleFormat {
64  /// `Other("")` — the wire-zero / "absent" sentinel, matching
65  /// [`ContainerFormat`]. FFmpeg's `AV_SAMPLE_FMT_NONE` is `-1`, outside
66  /// the `u32` code space, so it has no numeric spelling here; the empty
67  /// slug is the one that round-trips.
68  #[cfg_attr(not(tarpaulin), inline(always))]
69  fn default() -> Self {
70    Self::Other(SmolStr::new_inline(""))
71  }
72}
73
74impl SampleFormat {
75  /// FFmpeg-canonical slug (`"u8"`, `"s16"`, `"flt"`, `"u8p"`, …).
76  pub fn as_str(&self) -> &str {
77    match self {
78      Self::U8 => "u8",
79      Self::S16 => "s16",
80      Self::S32 => "s32",
81      Self::Flt => "flt",
82      Self::Dbl => "dbl",
83      Self::U8p => "u8p",
84      Self::S16p => "s16p",
85      Self::S32p => "s32p",
86      Self::Fltp => "fltp",
87      Self::Dblp => "dblp",
88      Self::S64 => "s64",
89      Self::S64p => "s64p",
90      Self::Other(s) => s.as_str(),
91    }
92  }
93
94  /// The FFmpeg `AV_SAMPLE_FMT_*` enum index for the named variants —
95  /// a boundary helper for FFmpeg interop, not a wire form.
96  ///
97  /// [`Self::Other`] returns [`None`]: it names a format FFmpeg has no
98  /// code for, and inventing one would lose the name. The slug from
99  /// [`Self::as_str`] is the spelling that always survives.
100  #[cfg_attr(not(tarpaulin), inline(always))]
101  pub const fn to_u32(&self) -> Option<u32> {
102    Some(match self {
103      Self::U8 => 0,
104      Self::S16 => 1,
105      Self::S32 => 2,
106      Self::Flt => 3,
107      Self::Dbl => 4,
108      Self::U8p => 5,
109      Self::S16p => 6,
110      Self::S32p => 7,
111      Self::Fltp => 8,
112      Self::Dblp => 9,
113      Self::S64 => 10,
114      Self::S64p => 11,
115      Self::Other(_) => return None,
116    })
117  }
118
119  /// Decodes an FFmpeg `AV_SAMPLE_FMT_*` code, or [`None`] if this build
120  /// names no format for it. The numeric space is FFmpeg's, so an
121  /// unrecognised code carries no name to preserve.
122  #[cfg_attr(not(tarpaulin), inline(always))]
123  pub const fn from_u32(v: u32) -> Option<Self> {
124    Some(match v {
125      0 => Self::U8,
126      1 => Self::S16,
127      2 => Self::S32,
128      3 => Self::Flt,
129      4 => Self::Dbl,
130      5 => Self::U8p,
131      6 => Self::S16p,
132      7 => Self::S32p,
133      8 => Self::Fltp,
134      9 => Self::Dblp,
135      10 => Self::S64,
136      11 => Self::S64p,
137      _ => return None,
138    })
139  }
140
141  /// The open escape for a slug this vocabulary does not name, ASCII-folded
142  /// to the crate's lowercase canon.
143  ///
144  /// The **one** construction path for [`Self::Other`]: folding here is what
145  /// keeps the whole value space lowercase-canonical, so the derived `Eq` /
146  /// `Hash` compare names rather than spellings.
147  pub fn other(slug: impl AsRef<str>) -> Self {
148    Self::Other(crate::parse::fold_owned(slug.as_ref()))
149  }
150
151  /// `true` for the planar layout variants (`*p`).
152  #[cfg_attr(not(tarpaulin), inline(always))]
153  pub const fn is_planar(&self) -> bool {
154    matches!(
155      self,
156      Self::U8p | Self::S16p | Self::S32p | Self::Fltp | Self::Dblp | Self::S64p
157    )
158  }
159}
160
161roster!(
162  SampleFormat,
163  "sample format",
164  [
165    U8, S16, S32, Flt, Dbl, U8p, S16p, S32p, Fltp, Dblp, S64, S64p
166  ],
167  escape: Other
168);
169
170impl FromStr for SampleFormat {
171  type Err = core::convert::Infallible;
172  /// Recognise a canonical FFmpeg sample-format slug; unknown
173  /// values land in [`Self::Other`] (infallible, lossless).
174  fn from_str(s: &str) -> Result<Self, Self::Err> {
175    let mut buf = [0u8; crate::parse::FOLD_CAP];
176    // An input too long to fold cannot name a variant either, so the
177    // unfolded original falls through to the miss arm.
178    let folded = crate::parse::fold(s, &mut buf).unwrap_or(s.as_bytes());
179    Ok(match folded {
180      b"u8" => Self::U8,
181      b"s16" => Self::S16,
182      b"s32" => Self::S32,
183      b"flt" => Self::Flt,
184      b"dbl" => Self::Dbl,
185      b"u8p" => Self::U8p,
186      b"s16p" => Self::S16p,
187      b"s32p" => Self::S32p,
188      b"fltp" => Self::Fltp,
189      b"dblp" => Self::Dblp,
190      b"s64" => Self::S64,
191      b"s64p" => Self::S64p,
192      _ => Self::other(s),
193    })
194  }
195}
196
197// ---------------------------------------------------------------------------
198
199/// Audio-only file / container format vocabulary.
200///
201/// Top-level multimedia containers (`mp4`/`mkv`/`mov`/`webm`/…)
202/// live on [`crate::container::Format`]; this enum
203/// enumerates the **audio-only** containers (one audio stream, no
204/// video). Closed-ish vocabulary — not FFmpeg-coded, so there is no
205/// `to_u32`/`from_u32`; the `Other(SmolStr)` arm preserves unknown
206/// slugs losslessly.
207///
208/// `as_str` returns the file-extension-style slug (`"mp3"`, `"aac"`,
209/// `"flac"`, …).
210#[cfg_attr(
211  feature = "quickcheck",
212  derive(::quickcheck_richderive::Arbitrary),
213  quickcheck(arbitrary = "crate::quickcheck_helpers::strings::audio_container_format")
214)]
215#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, IsVariant, Unwrap, TryUnwrap)]
216#[display("{}", self.as_str())]
217#[unwrap(ref, ref_mut)]
218#[try_unwrap(ref, ref_mut)]
219#[non_exhaustive]
220pub enum ContainerFormat {
221  /// MPEG-1/2 Audio Layer III (`.mp3`). The auto-derived predicate
222  /// name would be `is_mp_3` (digit-snake-case); the hand-written
223  /// [`Self::is_mp3`] uses the cleaner name.
224  #[is_variant(ignore)]
225  Mp3,
226  /// Raw AAC ADTS / ADIF stream (`.aac`).
227  Aac,
228  /// Free Lossless Audio Codec (`.flac`).
229  Flac,
230  /// Ogg Vorbis / generic Ogg container (`.ogg`).
231  Ogg,
232  /// Opus in Ogg or raw (`.opus`).
233  Opus,
234  /// RIFF WAVE (`.wav`).
235  Wav,
236  /// Audio Interchange File Format (`.aiff` / `.aif`).
237  Aiff,
238  /// Apple Lossless (ALAC) — usually carried inside `.m4a`,
239  /// occasionally `.caf`; this variant is the bare-codec spelling.
240  Alac,
241  /// Windows Media Audio (`.wma`).
242  Wma,
243  /// Monkey's Audio (`.ape`).
244  Ape,
245  /// WavPack (`.wv`).
246  Wv,
247  /// Matroska Audio (`.mka`).
248  Mka,
249  /// MPEG-4 audio-only (`.m4a`) — AAC / ALAC in an MP4 box layout.
250  /// The auto-derived predicate name would be `is_m_4_a`
251  /// (digit-snake-case); the hand-written [`Self::is_m4a`] uses the
252  /// cleaner name.
253  #[is_variant(ignore)]
254  M4a,
255  /// Apple Core Audio Format (`.caf`).
256  Caf,
257  /// A container not enumerated above — carries the
258  /// extension-style slug verbatim. Lossless escape.
259  Other(SmolStr),
260}
261
262impl Default for ContainerFormat {
263  /// `Other("")` — the wire-zero / "absent" sentinel. Audio
264  /// containers vary by source; there is no universally-defensible
265  /// default. Callers picking a meaningful fallback should be
266  /// explicit.
267  #[inline]
268  fn default() -> Self {
269    Self::Other(SmolStr::new_inline(""))
270  }
271}
272
273impl ContainerFormat {
274  /// True iff this is [`Self::Mp3`]. Hand-written to override the
275  /// auto-derived `is_mp_3` (digit-snake-case is ugly).
276  #[inline(always)]
277  pub const fn is_mp3(&self) -> bool {
278    matches!(self, Self::Mp3)
279  }
280
281  /// True iff this is [`Self::M4a`]. Hand-written to override the
282  /// auto-derived `is_m_4_a` (digit-snake-case is ugly).
283  #[inline(always)]
284  pub const fn is_m4a(&self) -> bool {
285    matches!(self, Self::M4a)
286  }
287
288  /// File-extension-style slug (`"mp3"`, `"aac"`, `"flac"`, …).
289  pub fn as_str(&self) -> &str {
290    match self {
291      Self::Mp3 => "mp3",
292      Self::Aac => "aac",
293      Self::Flac => "flac",
294      Self::Ogg => "ogg",
295      Self::Opus => "opus",
296      Self::Wav => "wav",
297      Self::Aiff => "aiff",
298      Self::Alac => "alac",
299      Self::Wma => "wma",
300      Self::Ape => "ape",
301      Self::Wv => "wv",
302      Self::Mka => "mka",
303      Self::M4a => "m4a",
304      Self::Caf => "caf",
305      Self::Other(s) => s.as_str(),
306    }
307  }
308
309  /// Primary file-on-disk extension (without the leading dot —
310  /// `"mp3"`, `"flac"`, `"m4a"`, …). For most audio containers the
311  /// extension matches the FFmpeg slug from [`Self::as_str`]; the
312  /// exception is `Alac`, which has no standalone extension (the
313  /// codec rides inside `.m4a`), so this method returns `"m4a"`.
314  ///
315  /// Returns `""` for [`Self::Other`] — the open variant carries an
316  /// FFmpeg slug, not an extension, so the mapping is unknown.
317  /// Returns `&'static str` (not `&str`) so the value is compile-time
318  /// stable and the method is `const`.
319  #[inline(always)]
320  pub const fn as_extension(&self) -> &'static str {
321    match self {
322      Self::Mp3 => "mp3",
323      Self::Aac => "aac",
324      Self::Flac => "flac",
325      Self::Ogg => "ogg",
326      Self::Opus => "opus",
327      Self::Wav => "wav",
328      Self::Aiff => "aiff",
329      Self::Alac => "m4a",
330      Self::Wma => "wma",
331      Self::Ape => "ape",
332      Self::Wv => "wv",
333      Self::Mka => "mka",
334      Self::M4a => "m4a",
335      Self::Caf => "caf",
336      Self::Other(_) => "",
337    }
338  }
339  /// The open escape for a slug this vocabulary does not name, ASCII-folded
340  /// to the crate's lowercase canon.
341  ///
342  /// The **one** construction path for [`Self::Other`]: folding here is what
343  /// keeps the whole value space lowercase-canonical, so the derived `Eq` /
344  /// `Hash` compare names rather than spellings. Constructing the variant
345  /// directly bypasses the fold and is not the supported spelling.
346  pub fn other(slug: impl AsRef<str>) -> Self {
347    Self::Other(crate::parse::fold_owned(slug.as_ref()))
348  }
349}
350
351roster!(
352  ContainerFormat,
353  "audio container format",
354  [
355    Mp3, Aac, Flac, Ogg, Opus, Wav, Aiff, Alac, Wma, Ape, Wv, Mka, M4a, Caf
356  ],
357  escape: Other
358);
359
360impl FromStr for ContainerFormat {
361  type Err = core::convert::Infallible;
362  /// Recognise a canonical extension-style slug; unknown values
363  /// land in [`Self::Other`] (infallible, lossless).
364  fn from_str(s: &str) -> Result<Self, Self::Err> {
365    let mut buf = [0u8; crate::parse::FOLD_CAP];
366    // An input too long to fold cannot name a variant either, so the
367    // unfolded original falls through to the miss arm.
368    let folded = crate::parse::fold(s, &mut buf).unwrap_or(s.as_bytes());
369    Ok(match folded {
370      b"mp3" => Self::Mp3,
371      b"aac" => Self::Aac,
372      b"flac" => Self::Flac,
373      b"ogg" => Self::Ogg,
374      b"opus" => Self::Opus,
375      b"wav" => Self::Wav,
376      b"aiff" => Self::Aiff,
377      b"alac" => Self::Alac,
378      b"wma" => Self::Wma,
379      b"ape" => Self::Ape,
380      b"wv" => Self::Wv,
381      b"mka" => Self::Mka,
382      b"m4a" => Self::M4a,
383      b"caf" => Self::Caf,
384      _ => Self::other(s),
385    })
386  }
387}
388
389#[cfg(test)]
390mod tests;