diff --git a/crates/audio/src/aac/mod.rs b/crates/audio/src/aac/mod.rs index eb1c7d40..34b081ea 100644 --- a/crates/audio/src/aac/mod.rs +++ b/crates/audio/src/aac/mod.rs @@ -113,6 +113,26 @@ pub fn wav_order(channel_config: u8) -> Option<&'static [usize]> { }) } +/// The channel configuration whose speakers a stream has: its own, or for configuration 0 the +/// configuration 3–6 that its program config element spells out. A program config element lists +/// front, side, back and LFE elements in that order, so one that opens with a centre and a front +/// pair and goes on with exactly the elements of a configuration has that configuration's speakers +/// (and [`wav_order`]). Any other list has no speaker order that can be told, and gives 0: ffmpeg's +/// 5.1(side), for one, lists the pair first and already decodes in WAV order. +#[cfg(feature = "aac-decode")] +pub fn speaker_config(asc: &AudioSpecificConfig) -> u8 { + if asc.channel_config != 0 { + return asc.channel_config; + } + let centre_then_pair = asc.pce.as_ref().is_some_and(|p| p.front.iter().map(|e| e.0).eq([false, true])); + if !centre_then_pair { + return 0; + } + let elements = asc.layout(); + let same = |config: &u8| config_layout(*config).is_some_and(|l| l.iter().map(|e| e.0).eq(elements.iter().map(|e| e.0))); + (3..=6).find(same).unwrap_or(0) +} + /// The order the [`Encoder`] takes its input in, from WAV order: for each encoder input channel /// (AAC element order, see [`EncoderConfig::channels`]), the WAV-order channel to feed it. The /// inverse of [`wav_order`] for 3 to 7 channels; `None` for mono and stereo (already in order) and @@ -279,6 +299,30 @@ mod tests { mod order_tests { use super::*; + /// A program config element in the shape of a channel configuration has that configuration's + /// speakers, with the centre first. ffmpeg's 5.1(side) lists its pair first and already decodes + /// in WAV order, so it stays as it is. + #[cfg(feature = "aac-decode")] + #[test] + fn a_program_config_element_takes_the_configuration_it_matches() { + let with = |pce| AudioSpecificConfig { pce: Some(pce), ..AudioSpecificConfig::lc(3, 0) }; + let centre_first = ProgramConfig { front: vec![(false, 0), (true, 0)], back: vec![(true, 1)], lfe: vec![0], ..Default::default() }; + assert_eq!(speaker_config(&with(centre_first)), 6); + let three = ProgramConfig { front: vec![(false, 0), (true, 0)], ..Default::default() }; + assert_eq!(speaker_config(&with(three)), 3); + let ffmpeg = ProgramConfig { front: vec![(true, 0), (false, 0)], side: vec![(false, 1)], back: vec![(true, 1)], ..Default::default() }; + assert_eq!(speaker_config(&with(ffmpeg)), 0); + // the same element types with no front pair: the side pair is not left and right + let no_front_pair = + ProgramConfig { front: vec![(false, 0)], side: vec![(true, 0)], back: vec![(true, 1)], lfe: vec![0], ..Default::default() }; + assert_eq!(speaker_config(&with(no_front_pair)), 0); + // 7.1 in a program config element is not one of the unambiguous configurations + let wide = + ProgramConfig { front: vec![(false, 0), (true, 0)], side: vec![(true, 1)], back: vec![(true, 2)], lfe: vec![0], ..Default::default() }; + assert_eq!(speaker_config(&with(wide)), 0); + assert_eq!(speaker_config(&AudioSpecificConfig::lc(3, 6)), 6); + } + /// `aac_order` undoes `wav_order` for each channel count the encoder maps to a configuration /// (3–6 channels: configurations 3–6; 7: configuration 11). #[test] diff --git a/crates/audio/src/track.rs b/crates/audio/src/track.rs index 3d9ed7df..af984d71 100644 --- a/crates/audio/src/track.rs +++ b/crates/audio/src/track.rs @@ -156,7 +156,8 @@ impl TrackDecoder { let codec = if up.is_some() { CodecId::HeAac } else { CodecId::Aac }; // Only twice the core rate is upsampled to; any other signalled rate plays the core. let out = if up.is_some() { out } else { dec.sample_rate() }; - let cfg = dec.config().channel_config; + // a program config element that spells out a configuration has its speakers + let cfg = aac::speaker_config(dec.config()); let layout = aac_layout(cfg, dec.channels()); let order = layout.is_ordered().then(|| aac::wav_order(cfg)).flatten(); (Inner::Aac { dec: Box::new(dec), up, order, adts_format }, codec, out, layout, SampleFormat::Compressed) @@ -1079,6 +1080,8 @@ mod tests { let mut dec = TrackDecoder::new(&aac_params(), &[&frame]).unwrap(); assert_eq!(dec.channels(), 3); assert_eq!(dec.decode(&frame).unwrap().channels.len(), 3); + // its element lists a centre and a front pair, as configuration 3 does: L R C, in WAV order + assert_eq!(dec.layout(), ChannelLayout::SURROUND_3_0); // no program config element where one is needed let bare = aac::AdtsHeader::write(2, 3, 0, 1, 0x7FF).unwrap(); let bare = [&bare[..], &[0xE0]].concat(); diff --git a/crates/audio/tests/codecs/routing_oracle.rs b/crates/audio/tests/codecs/routing_oracle.rs index c35a8900..6472979b 100644 --- a/crates/audio/tests/codecs/routing_oracle.rs +++ b/crates/audio/tests/codecs/routing_oracle.rs @@ -669,10 +669,14 @@ fn adts_with_a_program_config_element_decodes() { assert_eq!(bytes[3] >> 6 | (bytes[2] & 1) << 2, 0, "the fixture signals its layout in a PCE"); let (info, buf) = decode(&bytes, None).unwrap(); assert_eq!((info.container, info.codec, buf.channels.len()), (c.container, c.codec, c.channels)); - assert_eq!(info.layout, c.layout); + // A program config element that lists a centre, a front pair and then the elements of 5.1 has + // 5.1's speakers, in WAV order like ffmpeg's decode; any other list keeps the decoded order, + // which ffmpeg versions write differently. + let ordered = info.layout.is_ordered(); + assert_eq!(info.layout, if ordered { ChannelLayout::SURROUND_5_1_SIDE } else { c.layout }); assert_eq!(buf.frames(), reference(&ff, &path).len() / c.channels); - // A program config's channels come in its own order, and which order ffmpeg writes differs by - // version: each of ours must match a different one of ffmpeg's. + // Each of our channels must match a different one of ffmpeg's: the same one when the layout is + // ordered. let refr = reference(&ff, &path); let mut matched = Vec::new(); for plane in &buf.channels { @@ -686,7 +690,9 @@ fn adts_with_a_program_config_element_decodes() { assert!(s >= c.min_snr, "channel {} matches ffmpeg's {best} at only {s:.1} dB", matched.len()); matched.push(best); } - matched.sort_unstable(); + if !ordered { + matched.sort_unstable(); + } assert_eq!(matched, (0..c.channels).collect::>()); }