codecs: import raw ADTS .aac files (#1291)
File ▸ Import and the Media Browser offer `.aac` (it is in AUDIO_EXTENSIONS), and Export ▸ AAC Audio writes ADTS `.aac` (#814), but no opener read them: `audio::opener` only accepted mp3/mp2/flac/ogg/aif by extension, so every `.aac` failed with "unrecognised format". `AudioFileSource` now decodes a standalone ADTS stream with our AAC decoder (ISO/IEC 13818-7 §6.2): frame after frame from the first header (after an ID3v2 tag), resyncing past damage, skipping frames with another configuration, keeping a frame that doesn't decode as silence, and upsampling HE-AAC to its output rate. Decoding stops with an error past 4 GiB of samples instead of allocating without bound. Tests encode a stereo tone with our encoder's ADTS output and import it (rate, channels, duration, level), and run truncated, bit-flipped and bad-length streams under catch_unwind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0541d4d182
commit
52b36f9764
|
|
@ -814,6 +814,21 @@ impl AudioFileSource {
|
|||
};
|
||||
return Ok(Self { info, rate, samples });
|
||||
}
|
||||
if ext == "aac" {
|
||||
let (rate, samples) = decode_adts(&bytes)?.ok_or_else(|| CodecError::Decode(format!("{name}: no ADTS AAC frames")))?;
|
||||
let frames = samples.first().map_or(0, Vec::len);
|
||||
let info = MediaInfo {
|
||||
name: name.to_string(),
|
||||
kind: MediaKind::AudioOnly,
|
||||
duration: Tick::from_units(frames as i64, rate as i64),
|
||||
video: None,
|
||||
audio_streams: vec![AudioStreamInfo { sample_rate: rate, channels: samples.len() as u32, codec: "AAC".into(), bits_per_sample: None }],
|
||||
container: "ADTS".into(),
|
||||
start_timecode: None,
|
||||
file_size: Some(bytes.len() as u64),
|
||||
};
|
||||
return Ok(Self { info, rate, samples });
|
||||
}
|
||||
let mss = MediaSourceStream::new(Box::new(std::io::Cursor::new(bytes.clone())), Default::default());
|
||||
let mut hint = Hint::new();
|
||||
hint.with_extension(&ext);
|
||||
|
|
@ -871,20 +886,94 @@ impl AudioFileSource {
|
|||
}
|
||||
}
|
||||
|
||||
/// The length of an ID3v2 tag at the start of `bytes` (0 without one): a 10-byte header, a
|
||||
/// syncsafe size, and a 10-byte footer when flagged (ID3v2.4 §3).
|
||||
fn id3v2_len(bytes: &[u8]) -> usize {
|
||||
match bytes {
|
||||
[b'I', b'D', b'3', _, _, flags, s0, s1, s2, s3, ..] => {
|
||||
let size = [*s0, *s1, *s2, *s3].iter().fold(0usize, |a, &b| (a << 7) | (b & 0x7F) as usize);
|
||||
size.saturating_add(if flags & 0x10 != 0 { 20 } else { 10 })
|
||||
}
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Most samples (all channels together) a standalone ADTS file decodes into memory: 4 GiB of f32,
|
||||
/// about three hours of 48 kHz stereo. Longer files are refused rather than allocated.
|
||||
const MAX_ADTS_SAMPLES: usize = 1 << 30;
|
||||
|
||||
/// The length of the ADTS frame starting `b` (ISO/IEC 13818-7 §6.2: 12-bit sync word, layer `00`,
|
||||
/// 13-bit `frame_length` including the header), when it starts with a header and fits in `b`.
|
||||
fn adts_frame_len(b: &[u8]) -> Option<usize> {
|
||||
let [0xFF, b1, _, b3, b4, b5, ..] = *b else { return None };
|
||||
if b1 & 0xF6 != 0xF0 {
|
||||
return None;
|
||||
}
|
||||
let len = ((b3 as usize & 3) << 11) | (b4 as usize) << 3 | (b5 as usize) >> 5;
|
||||
let hdr = if b1 & 1 != 0 { 7 } else { 9 };
|
||||
(len > hdr && len <= b.len()).then_some(len)
|
||||
}
|
||||
|
||||
/// A raw ADTS stream (`.aac`) decoded with our AAC decoder: (output sample rate, planar samples).
|
||||
/// Frames follow each other from the first header (after an ID3v2 tag); damaged data is skipped
|
||||
/// up to the next header, frames with another configuration than the first are skipped, and a
|
||||
/// frame that does not decode keeps its duration as silence. HE-AAC is upsampled to its output
|
||||
/// rate. `Ok(None)` when no frame decodes.
|
||||
fn decode_adts(bytes: &[u8]) -> Result<Option<(u32, Vec<Vec<f32>>)>> {
|
||||
let mut frames: Vec<(Vec<u8>, &[u8])> = Vec::new();
|
||||
let mut p = id3v2_len(bytes);
|
||||
while let Some(rest) = bytes.get(p..).filter(|r| r.len() >= 7) {
|
||||
match adts_frame_len(rest).and_then(|len| Some((len, adts_split(rest.get(..len)?)?))) {
|
||||
Some((len, f)) => {
|
||||
frames.push(f);
|
||||
p += len;
|
||||
}
|
||||
None => p += 1,
|
||||
}
|
||||
}
|
||||
let Some(asc) = frames.first().map(|f| f.0.clone()) else { return Ok(None) };
|
||||
let Some(rate) = aac_output_rate(&asc, frames.iter().take(8).map(|f| f.1)) else { return Ok(None) };
|
||||
let mut dec = PacketDecoder::aac(&asc, rate)?;
|
||||
let mut out: Vec<Vec<f32>> = Vec::new();
|
||||
let mut total = 0usize;
|
||||
let mut last = 0usize;
|
||||
for (cfg, payload) in frames {
|
||||
if cfg != asc {
|
||||
continue;
|
||||
}
|
||||
let decoded = dec.decode(payload, 0).ok().filter(|c| out.is_empty() || c.len() == out.len());
|
||||
match decoded {
|
||||
Some(c) if out.is_empty() => {
|
||||
last = c.first().map_or(0, Vec::len);
|
||||
out = c;
|
||||
}
|
||||
Some(c) => {
|
||||
last = c.first().map_or(0, Vec::len);
|
||||
for (o, s) in out.iter_mut().zip(c) {
|
||||
o.extend(s);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
for o in out.iter_mut() {
|
||||
o.resize(o.len() + last, 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
total = total.saturating_add(last.saturating_mul(out.len()));
|
||||
if total > MAX_ADTS_SAMPLES {
|
||||
return Err(CodecError::Unsupported("AAC file too long to import as raw ADTS (over 4 GiB decoded): remux it to .m4a".into()));
|
||||
}
|
||||
}
|
||||
Ok(out.first().is_some_and(|c| !c.is_empty()).then_some((rate, out)))
|
||||
}
|
||||
|
||||
/// A raw MPEG audio Layer I / II stream (`.mp2`) decoded with our decoder: (layer, sample rate,
|
||||
/// planar samples). Frames follow each other from the first frame header (after an ID3v2 tag);
|
||||
/// damaged data is skipped up to the next frame header of the same layer and rate, and a frame
|
||||
/// with another channel count keeps its duration as silence. `None` when the stream does not start
|
||||
/// with a Layer I / II frame followed by another frame (or the end), e.g. MP3.
|
||||
fn decode_mpeg_layer12(bytes: &[u8]) -> Option<(u8, u32, Vec<Vec<f32>>)> {
|
||||
let mut p = match bytes {
|
||||
// ID3v2: 10-byte header, a syncsafe size, and a 10-byte footer when flagged
|
||||
[b'I', b'D', b'3', _, _, flags, s0, s1, s2, s3, ..] => {
|
||||
let size = [*s0, *s1, *s2, *s3].iter().fold(0usize, |a, &b| (a << 7) | (b & 0x7F) as usize);
|
||||
size.saturating_add(if flags & 0x10 != 0 { 20 } else { 10 })
|
||||
}
|
||||
_ => 0,
|
||||
};
|
||||
let mut p = id3v2_len(bytes);
|
||||
let frame_at = |p: usize| bytes.get(p..).and_then(|b| filmcraft_mpa::parse_header(b).ok()).filter(|h| h.frame_bytes > 0);
|
||||
let first = frame_at(p)?;
|
||||
let next = p.saturating_add(first.frame_bytes);
|
||||
|
|
@ -928,7 +1017,7 @@ impl MediaSource for AudioFileSource {
|
|||
|
||||
pub fn opener(name: &str, bytes: Arc<[u8]>) -> Option<std::result::Result<SharedSource, MediaError>> {
|
||||
let ext = name.rsplit('.').next().unwrap_or("").to_ascii_lowercase();
|
||||
if !matches!(ext.as_str(), "mp3" | "mp2" | "flac" | "ogg" | "oga" | "aif" | "aiff" | "aifc") {
|
||||
if !matches!(ext.as_str(), "mp3" | "mp2" | "aac" | "flac" | "ogg" | "oga" | "aif" | "aiff" | "aifc") {
|
||||
return None;
|
||||
}
|
||||
Some(AudioFileSource::decode(name, bytes).map(|s| Arc::new(s) as SharedSource).map_err(Into::into))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
//! Standalone ADTS `.aac` files (#1291): File ▸ Import offers them, and FilmCraft's own AAC export
|
||||
//! writes them (#814), so they must open as audio. The streams are made with our AAC encoder, which
|
||||
//! prefixes each access unit with an ADTS header (ISO/IEC 13818-7 §6.2).
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use filmcraft_aac::{Encoder, EncoderConfig};
|
||||
|
||||
const RATE: u32 = 48_000;
|
||||
|
||||
/// `secs` of a 440 Hz tone at half scale in every channel, as an ADTS stream.
|
||||
fn adts_tone(channels: usize, secs: u32) -> Vec<u8> {
|
||||
let tone: Vec<f32> = (0..RATE * secs).map(|i| (std::f32::consts::TAU * 440.0 * i as f32 / RATE as f32).sin() * 0.5).collect();
|
||||
let planar: Vec<&[f32]> = (0..channels).map(|_| tone.as_slice()).collect();
|
||||
let mut enc = Encoder::new(EncoderConfig { adts: true, ..EncoderConfig::cbr(RATE, channels, 128_000) }).unwrap();
|
||||
let mut out = Vec::new();
|
||||
for au in enc.encode(&planar).into_iter().chain(enc.flush()) {
|
||||
out.extend(au);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn rms(s: &[f32]) -> f32 {
|
||||
(s.iter().map(|v| v * v).sum::<f32>() / s.len().max(1) as f32).sqrt()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adts_aac_files_import_as_audio() {
|
||||
let src = filmcraft_codecs::open_bytes("tone.aac", Arc::from(adts_tone(2, 1))).unwrap();
|
||||
let info = src.info();
|
||||
let a = info.audio().unwrap();
|
||||
assert_eq!((a.sample_rate, a.channels), (RATE, 2));
|
||||
// One second plus the encoder's priming and the last frame's padding (at most two frames).
|
||||
let secs = info.duration.seconds();
|
||||
assert!((1.0..=1.0 + 2048.0 / RATE as f64).contains(&secs), "duration {secs}");
|
||||
let buf = src.audio(RATE as i64 / 4, RATE as usize / 2, RATE).unwrap();
|
||||
for (c, ch) in buf.channels.iter().enumerate() {
|
||||
let level = rms(ch);
|
||||
assert!((level - 0.5 / 2f32.sqrt()).abs() < 0.05, "channel {c} rms {level}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn damaged_adts_files_fail_without_panicking() {
|
||||
let good = adts_tone(1, 1);
|
||||
let mut cases: Vec<Vec<u8>> = (0..good.len().min(4096)).step_by(7).map(|n| good[..n].to_vec()).collect();
|
||||
for i in (0..good.len()).step_by(97) {
|
||||
let mut b = good.clone();
|
||||
b[i] ^= 1 << (i % 8);
|
||||
cases.push(b);
|
||||
}
|
||||
// A frame length of zero, and one running past the end of the file.
|
||||
let mut zero = good.clone();
|
||||
zero[3] &= !3;
|
||||
zero[4] = 0;
|
||||
zero[5] &= 0x1F;
|
||||
cases.push(zero);
|
||||
let mut long = good[..64].to_vec();
|
||||
long[3] |= 3;
|
||||
long[4] = 0xFF;
|
||||
cases.push(long);
|
||||
for (i, b) in cases.into_iter().enumerate() {
|
||||
let r = std::panic::catch_unwind(|| filmcraft_codecs::open_bytes("damaged.aac", Arc::from(b)).map(|s| s.audio(0, 4800, RATE).is_ok()));
|
||||
assert!(r.is_ok(), "case {i} panicked");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue