Skip to main content

denoize/
channel_layout.rs

1//! Standard channel layouts used by the audio pipeline.
2//!
3//! The PCM buffers in [`crate::audio::Audio`] are planar, but their index order
4//! still carries meaning for surround material.  Keeping that meaning explicit
5//! prevents a 5.1 or 7.1 recording from being treated as an arbitrary list of
6//! channels when a codec has to reduce it to stereo.
7
8use std::fmt;
9
10/// WAVE_FORMAT_EXTENSIBLE channel mask (the first 18 Microsoft speaker bits).
11///
12/// The mask is kept separately from [`ChannelLayout`] because a 5.1 stream can
13/// use rear or side surrounds, and because files may carry a non-standard but
14/// still meaningful speaker arrangement.
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
16pub struct ChannelMask(u32);
17
18impl ChannelMask {
19    pub const FRONT_LEFT: u32 = 1 << 0;
20    pub const FRONT_RIGHT: u32 = 1 << 1;
21    pub const FRONT_CENTER: u32 = 1 << 2;
22    pub const LFE1: u32 = 1 << 3;
23    pub const REAR_LEFT: u32 = 1 << 4;
24    pub const REAR_RIGHT: u32 = 1 << 5;
25    pub const FRONT_LEFT_CENTER: u32 = 1 << 6;
26    pub const FRONT_RIGHT_CENTER: u32 = 1 << 7;
27    pub const REAR_CENTER: u32 = 1 << 8;
28    pub const SIDE_LEFT: u32 = 1 << 9;
29    pub const SIDE_RIGHT: u32 = 1 << 10;
30    pub const TOP_CENTER: u32 = 1 << 11;
31    pub const TOP_FRONT_LEFT: u32 = 1 << 12;
32    pub const TOP_FRONT_CENTER: u32 = 1 << 13;
33    pub const TOP_FRONT_RIGHT: u32 = 1 << 14;
34    pub const TOP_REAR_LEFT: u32 = 1 << 15;
35    pub const TOP_REAR_CENTER: u32 = 1 << 16;
36    pub const TOP_REAR_RIGHT: u32 = 1 << 17;
37
38    const fn new(bits: u32) -> Self {
39        Self(bits)
40    }
41
42    /// Parse a WAVE channel mask. Bits outside the standardized 18-bit range
43    /// are rejected; zero is valid and means "unspecified positions".
44    pub const fn from_bits(bits: u32) -> Option<Self> {
45        if bits < (1 << 18) {
46            Some(Self::new(bits))
47        } else {
48            None
49        }
50    }
51
52    pub const fn bits(self) -> u32 {
53        self.0
54    }
55
56    pub const fn channels(self) -> usize {
57        self.0.count_ones() as usize
58    }
59
60    /// Return positions in canonical WAVE channel order (least significant
61    /// mask bit first), matching planar PCM channel order.
62    pub fn positions(self) -> Vec<ChannelPosition> {
63        (0..18)
64            .filter(|index| self.0 & (1 << index) != 0)
65            .filter_map(ChannelPosition::from_index)
66            .collect()
67    }
68
69    pub fn position(self, channel: usize) -> Option<ChannelPosition> {
70        self.positions().get(channel).copied()
71    }
72
73    pub fn pan(self) -> Vec<PanInfo> {
74        self.positions()
75            .into_iter()
76            .map(ChannelPosition::pan)
77            .collect()
78    }
79}
80
81impl fmt::Display for ChannelMask {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        let labels = self
84            .positions()
85            .into_iter()
86            .map(|position| position.label())
87            .collect::<Vec<_>>()
88            .join(",");
89        write!(f, "0x{:05x} [{}]", self.bits(), labels)
90    }
91}
92
93/// A speaker position represented by one WAVE channel-mask bit.
94#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
95pub enum ChannelPosition {
96    FrontLeft,
97    FrontRight,
98    FrontCenter,
99    Lfe1,
100    RearLeft,
101    RearRight,
102    FrontLeftCenter,
103    FrontRightCenter,
104    RearCenter,
105    SideLeft,
106    SideRight,
107    TopCenter,
108    TopFrontLeft,
109    TopFrontCenter,
110    TopFrontRight,
111    TopRearLeft,
112    TopRearCenter,
113    TopRearRight,
114}
115
116impl ChannelPosition {
117    pub const fn from_index(index: u32) -> Option<Self> {
118        Some(match index {
119            0 => Self::FrontLeft,
120            1 => Self::FrontRight,
121            2 => Self::FrontCenter,
122            3 => Self::Lfe1,
123            4 => Self::RearLeft,
124            5 => Self::RearRight,
125            6 => Self::FrontLeftCenter,
126            7 => Self::FrontRightCenter,
127            8 => Self::RearCenter,
128            9 => Self::SideLeft,
129            10 => Self::SideRight,
130            11 => Self::TopCenter,
131            12 => Self::TopFrontLeft,
132            13 => Self::TopFrontCenter,
133            14 => Self::TopFrontRight,
134            15 => Self::TopRearLeft,
135            16 => Self::TopRearCenter,
136            17 => Self::TopRearRight,
137            _ => return None,
138        })
139    }
140
141    pub const fn label(self) -> &'static str {
142        match self {
143            Self::FrontLeft => "FL",
144            Self::FrontRight => "FR",
145            Self::FrontCenter => "FC",
146            Self::Lfe1 => "LFE",
147            Self::RearLeft => "RL",
148            Self::RearRight => "RR",
149            Self::FrontLeftCenter => "FLC",
150            Self::FrontRightCenter => "FRC",
151            Self::RearCenter => "RC",
152            Self::SideLeft => "SL",
153            Self::SideRight => "SR",
154            Self::TopCenter => "TC",
155            Self::TopFrontLeft => "TFL",
156            Self::TopFrontCenter => "TFC",
157            Self::TopFrontRight => "TFR",
158            Self::TopRearLeft => "TRL",
159            Self::TopRearCenter => "TRC",
160            Self::TopRearRight => "TRR",
161        }
162    }
163
164    /// Conventional loudspeaker pan coordinates. Azimuth is degrees from the
165    /// front, with left negative and right positive; elevation is degrees up.
166    pub const fn pan(self) -> PanInfo {
167        match self {
168            Self::FrontLeft => PanInfo::new(-30.0, 0.0),
169            Self::FrontRight => PanInfo::new(30.0, 0.0),
170            Self::FrontCenter => PanInfo::new(0.0, 0.0),
171            Self::Lfe1 => PanInfo::new(0.0, -10.0),
172            Self::RearLeft => PanInfo::new(-110.0, 0.0),
173            Self::RearRight => PanInfo::new(110.0, 0.0),
174            Self::FrontLeftCenter => PanInfo::new(-15.0, 0.0),
175            Self::FrontRightCenter => PanInfo::new(15.0, 0.0),
176            Self::RearCenter => PanInfo::new(180.0, 0.0),
177            Self::SideLeft => PanInfo::new(-90.0, 0.0),
178            Self::SideRight => PanInfo::new(90.0, 0.0),
179            Self::TopCenter => PanInfo::new(0.0, 90.0),
180            Self::TopFrontLeft => PanInfo::new(-30.0, 45.0),
181            Self::TopFrontCenter => PanInfo::new(0.0, 45.0),
182            Self::TopFrontRight => PanInfo::new(30.0, 45.0),
183            Self::TopRearLeft => PanInfo::new(-110.0, 45.0),
184            Self::TopRearCenter => PanInfo::new(180.0, 45.0),
185            Self::TopRearRight => PanInfo::new(110.0, 45.0),
186        }
187    }
188}
189
190/// Pan coordinates associated with one channel position.
191#[derive(Clone, Copy, Debug, PartialEq)]
192pub struct PanInfo {
193    pub azimuth_degrees: f32,
194    pub elevation_degrees: f32,
195    pub gain: f32,
196}
197
198impl PanInfo {
199    const fn new(azimuth_degrees: f32, elevation_degrees: f32) -> Self {
200        Self {
201            azimuth_degrees,
202            elevation_degrees,
203            gain: 1.0,
204        }
205    }
206}
207
208/// Conventional channel layouts recognized by denoize.
209///
210/// The channel order follows the order used by WAV/FLAC and the MPEG channel
211/// configuration tables.  A layout inferred only from a channel count is a
212/// convention, not a claim that a file's optional channel mask was present;
213/// channel masks are handled separately when available.
214#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
215pub enum ChannelLayout {
216    /// One front/center (mono) channel.
217    #[default]
218    Mono,
219    /// Front left, front right.
220    Stereo,
221    /// Front left, front right, low-frequency effects.
222    TwoPointOne,
223    /// Front left, front right, back left, back right.
224    Quad,
225    /// Front left, front right, front center, back left, back right.
226    FivePointZero,
227    /// Front left, front right, front center, LFE, back left, back right.
228    FivePointOne,
229    /// Front left, front right, front center, LFE, back center, side left,
230    /// side right.
231    SixPointOne,
232    /// Front left, front right, front center, LFE, back left, back right, side
233    /// left, side right.
234    SevenPointOne,
235    /// A channel count for which no safe conventional layout is known.
236    Unknown(usize),
237}
238
239impl ChannelLayout {
240    /// Infer the conventional layout for a channel count.
241    pub const fn from_channel_count(channels: usize) -> Self {
242        match channels {
243            1 => Self::Mono,
244            2 => Self::Stereo,
245            3 => Self::TwoPointOne,
246            4 => Self::Quad,
247            5 => Self::FivePointZero,
248            6 => Self::FivePointOne,
249            7 => Self::SixPointOne,
250            8 => Self::SevenPointOne,
251            other => Self::Unknown(other),
252        }
253    }
254
255    /// Number of channels in this layout.
256    pub const fn channels(self) -> usize {
257        match self {
258            Self::Mono => 1,
259            Self::Stereo => 2,
260            Self::TwoPointOne => 3,
261            Self::Quad => 4,
262            Self::FivePointZero => 5,
263            Self::FivePointOne => 6,
264            Self::SixPointOne => 7,
265            Self::SevenPointOne => 8,
266            Self::Unknown(channels) => channels,
267        }
268    }
269
270    /// Return the standard WAVE channel mask for this layout.
271    pub const fn mask(self) -> Option<ChannelMask> {
272        match self {
273            Self::Mono => Some(ChannelMask::new(ChannelMask::FRONT_CENTER)),
274            Self::Stereo => Some(ChannelMask::new(
275                ChannelMask::FRONT_LEFT | ChannelMask::FRONT_RIGHT,
276            )),
277            Self::TwoPointOne => Some(ChannelMask::new(
278                ChannelMask::FRONT_LEFT | ChannelMask::FRONT_RIGHT | ChannelMask::LFE1,
279            )),
280            Self::Quad => Some(ChannelMask::new(
281                ChannelMask::FRONT_LEFT
282                    | ChannelMask::FRONT_RIGHT
283                    | ChannelMask::REAR_LEFT
284                    | ChannelMask::REAR_RIGHT,
285            )),
286            Self::FivePointZero => Some(ChannelMask::new(
287                ChannelMask::FRONT_LEFT
288                    | ChannelMask::FRONT_RIGHT
289                    | ChannelMask::FRONT_CENTER
290                    | ChannelMask::REAR_LEFT
291                    | ChannelMask::REAR_RIGHT,
292            )),
293            Self::FivePointOne => Some(ChannelMask::new(
294                ChannelMask::FRONT_LEFT
295                    | ChannelMask::FRONT_RIGHT
296                    | ChannelMask::FRONT_CENTER
297                    | ChannelMask::LFE1
298                    | ChannelMask::REAR_LEFT
299                    | ChannelMask::REAR_RIGHT,
300            )),
301            Self::SixPointOne => Some(ChannelMask::new(
302                ChannelMask::FRONT_LEFT
303                    | ChannelMask::FRONT_RIGHT
304                    | ChannelMask::FRONT_CENTER
305                    | ChannelMask::LFE1
306                    | ChannelMask::REAR_CENTER
307                    | ChannelMask::SIDE_LEFT
308                    | ChannelMask::SIDE_RIGHT,
309            )),
310            Self::SevenPointOne => Some(ChannelMask::new(
311                ChannelMask::FRONT_LEFT
312                    | ChannelMask::FRONT_RIGHT
313                    | ChannelMask::FRONT_CENTER
314                    | ChannelMask::LFE1
315                    | ChannelMask::REAR_LEFT
316                    | ChannelMask::REAR_RIGHT
317                    | ChannelMask::SIDE_LEFT
318                    | ChannelMask::SIDE_RIGHT,
319            )),
320            Self::Unknown(_) => None,
321        }
322    }
323
324    /// Match an explicit WAVE mask to a conventional layout when possible.
325    pub fn from_channel_mask(mask: ChannelMask) -> Self {
326        let bits = mask.bits();
327        for layout in [
328            Self::Mono,
329            Self::Stereo,
330            Self::TwoPointOne,
331            Self::Quad,
332            Self::FivePointZero,
333            Self::FivePointOne,
334            Self::SixPointOne,
335            Self::SevenPointOne,
336        ] {
337            if layout.mask().is_some_and(|known| known.bits() == bits) {
338                return layout;
339            }
340        }
341        Self::Unknown(mask.channels())
342    }
343
344    /// Stable, human-readable name suitable for CLI reports and diagnostics.
345    pub const fn name(self) -> &'static str {
346        match self {
347            Self::Mono => "mono",
348            Self::Stereo => "stereo",
349            Self::TwoPointOne => "2.1",
350            Self::Quad => "quad",
351            Self::FivePointZero => "5.0",
352            Self::FivePointOne => "5.1",
353            Self::SixPointOne => "6.1",
354            Self::SevenPointOne => "7.1",
355            Self::Unknown(_) => "unknown",
356        }
357    }
358
359    /// Whether this layout is a known multi-channel surround layout.
360    pub const fn is_surround(self) -> bool {
361        matches!(
362            self,
363            Self::TwoPointOne
364                | Self::Quad
365                | Self::FivePointZero
366                | Self::FivePointOne
367                | Self::SixPointOne
368                | Self::SevenPointOne
369        )
370    }
371}
372
373impl fmt::Display for ChannelLayout {
374    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
375        match self {
376            Self::Unknown(channels) => write!(f, "unknown ({channels}ch)"),
377            known => f.write_str(known.name()),
378        }
379    }
380}
381
382#[cfg(test)]
383mod tests {
384    use super::*;
385
386    #[test]
387    fn maps_standard_channel_counts() {
388        assert_eq!(ChannelLayout::from_channel_count(1), ChannelLayout::Mono);
389        assert_eq!(ChannelLayout::from_channel_count(2), ChannelLayout::Stereo);
390        assert_eq!(
391            ChannelLayout::from_channel_count(6),
392            ChannelLayout::FivePointOne
393        );
394        assert_eq!(
395            ChannelLayout::from_channel_count(8),
396            ChannelLayout::SevenPointOne
397        );
398    }
399
400    #[test]
401    fn unknown_layout_keeps_its_channel_count() {
402        let layout = ChannelLayout::from_channel_count(12);
403        assert_eq!(layout, ChannelLayout::Unknown(12));
404        assert_eq!(layout.channels(), 12);
405        assert_eq!(layout.to_string(), "unknown (12ch)");
406    }
407
408    #[test]
409    fn masks_and_pan_positions_follow_canonical_channel_order() {
410        let mask = ChannelLayout::FivePointOne.mask().unwrap();
411        assert_eq!(mask.bits(), 0x3f);
412        assert_eq!(
413            mask.positions()[..3],
414            [
415                ChannelPosition::FrontLeft,
416                ChannelPosition::FrontRight,
417                ChannelPosition::FrontCenter,
418            ]
419        );
420        assert_eq!(
421            ChannelLayout::from_channel_mask(mask),
422            ChannelLayout::FivePointOne
423        );
424        assert_eq!(mask.pan()[0].azimuth_degrees, -30.0);
425        assert_eq!(mask.pan()[5].azimuth_degrees, 110.0);
426    }
427
428    #[test]
429    fn side_surround_mask_is_not_mistaken_for_rear_surround() {
430        let bits = ChannelMask::FRONT_LEFT
431            | ChannelMask::FRONT_RIGHT
432            | ChannelMask::FRONT_CENTER
433            | ChannelMask::LFE1
434            | ChannelMask::SIDE_LEFT
435            | ChannelMask::SIDE_RIGHT;
436        let mask = ChannelMask::from_bits(bits).unwrap();
437        assert_eq!(
438            ChannelLayout::from_channel_mask(mask),
439            ChannelLayout::Unknown(6)
440        );
441        assert_eq!(mask.positions()[4], ChannelPosition::SideLeft);
442    }
443}