Skip to main content

mediaframe/audio/channel_layout_description/
mod.rs

1//! The full structural description of an audio channel layout — order,
2//! channel count, mask, per-channel list, and the backend's rendering.
3//!
4//! **Three layers, and this is the third.**
5//!
6//! 1. [`ChannelOrder`] — how the channels are arranged (Native bitmask /
7//!    Custom per-channel list / Ambisonic / Unspecified), matching
8//!    FFmpeg's `AVChannelOrder` taxonomy.
9//! 2. [`ChannelSpec`] — for a custom-order layout, one entry per
10//!    channel: an index, a backend-specific raw id, and an optional
11//!    label.
12//! 3. [`ChannelLayoutDescription`] — the bundle: order + channel count +
13//!    the layout's name + native bitmask (when applicable) + custom
14//!    channel list (when applicable) + the backend's own free-form
15//!    rendering.
16//!
17//! **The name and the description are different things.**
18//! [`ChannelLayout`] is the *named* vocabulary — a closed-ish roster of
19//! the layouts FFmpeg's `channel_layout_map[]` spells (`"5.1"`,
20//! `"quad(side)"`, `"22.2"`), with an `Other(SmolStr)` escape for a name
21//! it does not carry. It answers *which layout is this*. This household
22//! answers *what is this layout made of*, and holds the name as one
23//! field among six ([`known_kind`](ChannelLayoutDescription::known_kind)).
24//!
25//! Neither subsumes the other. A layout with a custom channel ordering
26//! has no name at all — [`ChannelLayout::default`] is the "absent"
27//! sentinel that field then carries — while its channel list is exactly
28//! what a consumer needs. A stream tagged `"5.1"` and nothing else has a
29//! name and no mask. Keeping them apart is what lets a value be honest
30//! about which of the two it actually knows.
31
32use smol_str::SmolStr;
33use std::vec::Vec;
34
35use crate::audio::{ChannelLayout, ChannelOrder, ChannelSpec};
36
37/// Audio channel layout, described in full — order + channel count +
38/// identification.
39///
40/// The bundle FFmpeg's `AVChannelLayout` carries through to consumers,
41/// rendered as plain Rust data:
42///
43/// - [`order`](Self::order) — Native / Custom / Ambisonic / Unspecified.
44/// - [`channels`](Self::channels) — total count.
45/// - [`known_kind`](Self::known_kind) — the layout's *name*, as a
46///   [`ChannelLayout`]. [`ChannelLayout::default`] (the `Other("")`
47///   absent sentinel) when no well-known shape matches.
48/// - [`native_mask`](Self::native_mask) — `Some(bitmask)` for
49///   [`ChannelOrder::Native`] / [`ChannelOrder::Ambisonic`], `None`
50///   otherwise.
51/// - [`custom_channels`](Self::custom_channels) — populated for
52///   [`ChannelOrder::Custom`] layouts; one [`ChannelSpec`] per channel.
53/// - [`text`](Self::text) — the backend's own free-form rendering, e.g.
54///   FFmpeg's `av_channel_layout_describe` output (`"5.1(side)"`,
55///   `"3 channels (FL+FR+LFE)"`).
56///
57/// With the `serde` feature the wire form is a map of those six names,
58/// each field in its own shape: `order` as its `u32` code, `known_kind`
59/// as its canonical slug, `native_mask` as a nullable integer,
60/// `custom_channels` as an array of [`ChannelSpec`] maps.
61///
62/// **No invariant across the fields.** An incoherent combination — a
63/// `Custom` order with an empty channel list, a `Native` order with no
64/// mask — is exactly as constructible through the public setters as a
65/// coherent one, so the derive rejects nothing the builders would have
66/// accepted, and the fuzz generators deliberately reach those
67/// combinations. A consumer that needs coherence checks it.
68#[cfg_attr(
69  feature = "serde",
70  derive(serde::Serialize, serde::Deserialize),
71  serde(default)
72)]
73#[cfg_attr(
74  feature = "quickcheck",
75  derive(::quickcheck_richderive::Arbitrary),
76  quickcheck(arbitrary = "crate::quickcheck_helpers::composite::channel_layout_description")
77)]
78#[derive(Debug, Clone, PartialEq, Eq, Hash)]
79pub struct ChannelLayoutDescription {
80  order: ChannelOrder,
81  channels: u32,
82  known_kind: ChannelLayout,
83  native_mask: Option<u64>,
84  custom_channels: Vec<ChannelSpec>,
85  text: SmolStr,
86}
87
88impl Default for ChannelLayoutDescription {
89  /// Delegates to [`ChannelLayoutDescription::new`] with zero channels —
90  /// the "uninitialized" sentinel [`Self::is_empty`] recognises.
91  #[cfg_attr(not(tarpaulin), inline(always))]
92  fn default() -> Self {
93    Self::new(0)
94  }
95}
96
97impl ChannelLayoutDescription {
98  /// Constructs a minimal description with the given channel count.
99  /// Every other field starts at its own absent value (`Unspecified`
100  /// order, no name, no mask, no channel list, no rendering); fill them
101  /// in with the `with_*` builders.
102  #[cfg_attr(not(tarpaulin), inline(always))]
103  pub const fn new(channels: u32) -> Self {
104    Self {
105      order: ChannelOrder::Unspecified,
106      channels,
107      known_kind: ChannelLayout::Other(SmolStr::new_inline("")),
108      native_mask: None,
109      custom_channels: Vec::new(),
110      text: SmolStr::new_inline(""),
111    }
112  }
113
114  /// Channel ordering (Native / Custom / Ambisonic / Unspecified).
115  #[cfg_attr(not(tarpaulin), inline(always))]
116  pub const fn order(&self) -> ChannelOrder {
117    self.order
118  }
119
120  /// Total channel count.
121  #[cfg_attr(not(tarpaulin), inline(always))]
122  pub const fn channels(&self) -> u32 {
123    self.channels
124  }
125
126  /// The layout's name, or [`ChannelLayout::default`] — the `Other("")`
127  /// absent sentinel — when no well-known shape matches.
128  ///
129  /// Borrowed rather than copied: [`ChannelLayout`] carries a `SmolStr`
130  /// in its escape arm and so is not `Copy`.
131  #[cfg_attr(not(tarpaulin), inline(always))]
132  pub const fn known_kind(&self) -> &ChannelLayout {
133    &self.known_kind
134  }
135
136  /// Native-order bitmask of `AV_CH_*` channel positions, when
137  /// applicable. `None` for Custom / Unspecified orders, or when the
138  /// mask is zero.
139  #[cfg_attr(not(tarpaulin), inline(always))]
140  pub const fn native_mask(&self) -> Option<u64> {
141    self.native_mask
142  }
143
144  /// Per-channel descriptors for [`ChannelOrder::Custom`] layouts; empty
145  /// otherwise.
146  #[cfg_attr(not(tarpaulin), inline(always))]
147  pub const fn custom_channels(&self) -> &[ChannelSpec] {
148    self.custom_channels.as_slice()
149  }
150
151  /// The backend's own free-form rendering of this layout — FFmpeg's
152  /// `av_channel_layout_describe` output (`"5.1(side)"`,
153  /// `"3 channels (FL+FR+LFE)"`), empty when there is none.
154  ///
155  /// Not a slug: this is what the backend printed, verbatim, and it is
156  /// the seat that keeps a layout no vocabulary can name from being lost
157  /// entirely. [`known_kind`](Self::known_kind) is the parsed name;
158  /// this is the rendering.
159  #[cfg_attr(not(tarpaulin), inline(always))]
160  pub fn text(&self) -> &str {
161    self.text.as_str()
162  }
163
164  /// `true` when every field is at its absent value — zero channels,
165  /// `Unspecified` order, no name, no mask, no custom channels, no
166  /// rendering. Useful as an "uninitialized" sentinel.
167  #[cfg_attr(not(tarpaulin), inline(always))]
168  pub fn is_empty(&self) -> bool {
169    self.channels == 0
170      && self.order.is_unspecified()
171      && self.known_kind == ChannelLayout::default()
172      && self.native_mask.is_none()
173      && self.custom_channels.is_empty()
174      && self.text.is_empty()
175  }
176
177  /// Sets the order — consuming builder.
178  #[must_use]
179  #[cfg_attr(not(tarpaulin), inline(always))]
180  pub const fn with_order(mut self, v: ChannelOrder) -> Self {
181    self.order = v;
182    self
183  }
184
185  /// Sets the channel count — consuming builder.
186  #[must_use]
187  #[cfg_attr(not(tarpaulin), inline(always))]
188  pub const fn with_channels(mut self, v: u32) -> Self {
189    self.channels = v;
190    self
191  }
192
193  /// Sets the layout's name — consuming builder.
194  #[must_use]
195  #[cfg_attr(not(tarpaulin), inline(always))]
196  pub fn with_known_kind(mut self, v: ChannelLayout) -> Self {
197    self.known_kind = v;
198    self
199  }
200
201  /// Sets the native-order bitmask — consuming builder.
202  #[must_use]
203  #[cfg_attr(not(tarpaulin), inline(always))]
204  pub const fn with_native_mask(mut self, v: Option<u64>) -> Self {
205    self.native_mask = v;
206    self
207  }
208
209  /// Sets the custom-order channel list — consuming builder.
210  #[must_use]
211  #[cfg_attr(not(tarpaulin), inline(always))]
212  pub fn with_custom_channels(mut self, v: Vec<ChannelSpec>) -> Self {
213    self.custom_channels = v;
214    self
215  }
216
217  /// Sets the backend's rendering — consuming builder.
218  #[must_use]
219  #[cfg_attr(not(tarpaulin), inline(always))]
220  pub fn with_text(mut self, v: impl Into<SmolStr>) -> Self {
221    self.text = v.into();
222    self
223  }
224
225  /// Sets the order in place.
226  #[cfg_attr(not(tarpaulin), inline(always))]
227  pub const fn set_order(&mut self, v: ChannelOrder) -> &mut Self {
228    self.order = v;
229    self
230  }
231
232  /// Sets the channel count in place.
233  #[cfg_attr(not(tarpaulin), inline(always))]
234  pub const fn set_channels(&mut self, v: u32) -> &mut Self {
235    self.channels = v;
236    self
237  }
238
239  /// Sets the layout's name in place.
240  #[cfg_attr(not(tarpaulin), inline(always))]
241  pub fn set_known_kind(&mut self, v: ChannelLayout) -> &mut Self {
242    self.known_kind = v;
243    self
244  }
245
246  /// Sets the native-order bitmask in place.
247  #[cfg_attr(not(tarpaulin), inline(always))]
248  pub const fn set_native_mask(&mut self, v: Option<u64>) -> &mut Self {
249    self.native_mask = v;
250    self
251  }
252
253  /// Sets the custom-order channel list in place.
254  #[cfg_attr(not(tarpaulin), inline(always))]
255  pub fn set_custom_channels(&mut self, v: Vec<ChannelSpec>) -> &mut Self {
256    self.custom_channels = v;
257    self
258  }
259
260  /// Sets the backend's rendering in place.
261  #[cfg_attr(not(tarpaulin), inline(always))]
262  pub fn set_text(&mut self, v: impl Into<SmolStr>) -> &mut Self {
263    self.text = v.into();
264    self
265  }
266
267  /// Appends one channel to the custom-order list.
268  ///
269  /// Crate-private, and the `buffa` decoder is its only caller: a
270  /// repeated field arrives one element per `merge_field` call, and the
271  /// public seat is whole-`Vec` (`with_custom_channels` /
272  /// `set_custom_channels`). Reading the list out and writing it back
273  /// per element would make decoding quadratic in a length an untrusted
274  /// peer chooses, which is a denial of service rather than a
275  /// performance note.
276  ///
277  /// Not public because a caller assembling a description has the whole
278  /// list in hand and the whole-`Vec` setter is the honest shape for
279  /// that; this exists only because the wire hands them over one at a
280  /// time.
281  #[cfg(feature = "buffa")]
282  #[cfg_attr(not(tarpaulin), inline(always))]
283  pub(crate) fn push_custom_channel(&mut self, v: ChannelSpec) -> &mut Self {
284    self.custom_channels.push(v);
285    self
286  }
287}
288
289#[cfg(test)]
290mod tests;