From 6f7c041a09ae6a38c1ea7be5328b4a6c73b07c5d Mon Sep 17 00:00:00 2001 From: alehel Date: Sat, 14 Nov 2026 08:31:05 +0100 Subject: [PATCH] craft-audio: tests of the coded formats hold on ffmpeg 8 and without AAC - ffmpeg 8 skips an AAC M4A's priming but plays the last packet's padding (ffmpeg 9 cuts it): the ffmpeg oracle test takes either length, and the README says which readers end the audio where. - With alac-encode and no aac-encode, `m4a` names ALAC: the extension test compared it with AAC. Refs #1230 --- crates/audio/README.md | 3 +++ crates/audio/tests/audio/decode_extensions.rs | 4 +++- crates/audio/tests/codecs/file_formats.rs | 8 +++++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/audio/README.md b/crates/audio/README.md index aae1703ec..2edc4bb13 100644 --- a/crates/audio/README.md +++ b/crates/audio/README.md @@ -468,6 +468,9 @@ match craft_audio::decode(&bytes, None) { the length: the file decodes with 1057 (MP3) or 256 (AC-3, E-AC-3) frames of delay in front and padded to whole frames at the end. MP3 gets no Xing / Info frame. `EncodeFormat::Aac` and `Alac` (M4A) decode to the length written. +- An AAC M4A's length is in the edit list and the last packet's duration. This crate, Apple's + decoders and ffmpeg 9 end the audio there; ffmpeg 8 skips the priming but plays the last + packet's padding (under 1024 frames of near silence). - `EncodeFormat::Aac` and `Alac` write the `moov` box after the audio (no fast start), and no metadata. Raw ADTS AAC is not a file format of `encode`; `aac::EncoderConfig::with_adts` gives the frames. diff --git a/crates/audio/tests/audio/decode_extensions.rs b/crates/audio/tests/audio/decode_extensions.rs index f6f021c65..27a5573de 100644 --- a/crates/audio/tests/audio/decode_extensions.rs +++ b/crates/audio/tests/audio/decode_extensions.rs @@ -66,7 +66,9 @@ fn every_encode_format_has_an_extension_it_is_written_and_read_under() { // A format whose feature is off has no name. match craft_audio::encode_format_for(ext) { Some(named) => { - assert!(named == format || (named, format) == (EncodeFormat::Aac, EncodeFormat::Alac), "{ext}"); + let shares_m4a = |f| matches!(f, EncodeFormat::Aac | EncodeFormat::Alac); + assert!(named == format || (shares_m4a(named) && shares_m4a(format)), "{ext}"); + assert!(named.is_built(), "{ext}"); assert!(craft_audio::ENCODE_EXTENSIONS.contains(&ext), "{ext}"); } None => assert!(!matches!(format, EncodeFormat::Wav | EncodeFormat::Aiff | EncodeFormat::Flac), "{ext}"), diff --git a/crates/audio/tests/codecs/file_formats.rs b/crates/audio/tests/codecs/file_formats.rs index 46ed901ff..deac5714d 100644 --- a/crates/audio/tests/codecs/file_formats.rs +++ b/crates/audio/tests/codecs/file_formats.rs @@ -418,8 +418,8 @@ fn ffmpeg_decode(ff: &std::path::Path, file: &[u8], name: &str, channels: usize) planar } -/// ffmpeg reads every format: the M4A files at the length written (it applies the edit list), -/// ALAC exactly, the raw streams behind their delay. +/// ffmpeg reads every format: the M4A files without their priming (it applies the edit list) and, +/// from ffmpeg 9, at the length written; ALAC exactly; the raw streams behind their delay. #[test] fn ffmpeg_reads_the_files() { let ff = require_ffmpeg!(); @@ -428,7 +428,9 @@ fn ffmpeg_reads_the_files() { let mut opts = EncodeOptions::new(EncodeFormat::Aac); opts.layout = layout; let out = ffmpeg_decode(&ff, &encode(&buf, &opts).unwrap(), &format!("aac_{channels}.m4a"), channels); - assert_eq!(out[0].len(), 20_000, "AAC, {channels} channels"); + // The priming is skipped by every ffmpeg (the channels below line up). The padding of the + // last packet is cut by ffmpeg 9; ffmpeg 8 plays it, to the end of the 20th packet. + assert!([20_000, 20_480].contains(&out[0].len()), "AAC, {channels} channels: {} frames", out[0].len()); if channels <= 6 { // ffmpeg also returns 5.1 in WAV order. for (c, (want, got)) in buf.channels.iter().zip(&out).enumerate() {