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;