craft-audio: an AAC program config element shaped like 3.0–5.1 gets those speakers (#964)

* craft-audio: a program config element in the shape of a channel configuration gets its speakers

An AAC stream with channel configuration 0 whose program config element lists
a centre, a front pair and then exactly the elements of configuration 3 to 6
decoded as C L R Ls Rs LFE and was reported as an unordered layout, so a
caller reading it in WAV order sent the centre to the left speaker. Such a
stream now has the configuration's layout and WAV order. Any other element
list keeps the decoded order, as before.

Ported from storytold/filmcraft#4280 (aac_stream_order in filmcraft-codecs).

* craft-audio: the ADTS program config oracle expects 5.1 in WAV order

ffmpeg 9 writes its 5.1 program config element centre first, so the stream
now has 5.1's speakers: each decoded channel must match ffmpeg's channel of
the same index. An element list in another order keeps the old check.

---------

Co-authored-by: mpratsaki <110930793+mpratsaki@users.noreply.github.com>
This commit is contained in:
Dylan M. Taylor
2026-11-11 19:25:58 -05:00
committed by GitHub
co-authored by mpratsaki
parent 53e1717523
commit 2b35d24324
3 changed files with 58 additions and 5 deletions
+44
View File
@@ -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]
+4 -1
View File
@@ -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();
+10 -4
View File
@@ -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::<Vec<_>>());
}