fix(media): RF64 trailing metadata stays outside audio duration
Use ds64.dataSize for an RF64 data-size sentinel instead of the rest of the file, so chunks after the audio are no longer decoded as samples. Fixes #925
This commit is contained in:
parent
4e2d0bc2d3
commit
e283f4dee7
|
|
@ -0,0 +1,69 @@
|
|||
//! #925: a chunk after an RF64 `data` sentinel must not become audio.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use filmcraft_time::Tick;
|
||||
|
||||
fn chunk(id: &[u8; 4], body: &[u8]) -> Vec<u8> {
|
||||
let mut out = id.to_vec();
|
||||
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
|
||||
out.extend_from_slice(body);
|
||||
if body.len() % 2 == 1 {
|
||||
out.push(0);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Four 16-bit mono frames of silence at 48 kHz, as RIFF or RF64, optionally with a trailing iXML.
|
||||
fn fixture(rf64: bool, trailing: bool) -> Vec<u8> {
|
||||
let mut fmt = Vec::new();
|
||||
for v in [1u16, 1] {
|
||||
fmt.extend_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
fmt.extend_from_slice(&48_000u32.to_le_bytes());
|
||||
fmt.extend_from_slice(&96_000u32.to_le_bytes());
|
||||
fmt.extend_from_slice(&2u16.to_le_bytes());
|
||||
fmt.extend_from_slice(&16u16.to_le_bytes());
|
||||
let mut out = if rf64 { b"RF64\xff\xff\xff\xffWAVE".to_vec() } else { b"RIFF\0\0\0\0WAVE".to_vec() };
|
||||
if rf64 {
|
||||
let mut ds = vec![0u8; 28];
|
||||
ds[8..16].copy_from_slice(&8u64.to_le_bytes());
|
||||
ds[16..24].copy_from_slice(&4u64.to_le_bytes());
|
||||
out.extend(chunk(b"ds64", &ds));
|
||||
}
|
||||
out.extend(chunk(b"fmt ", &fmt));
|
||||
let offset = out.len();
|
||||
out.extend(chunk(b"data", &[0u8; 8]));
|
||||
if rf64 {
|
||||
out[offset + 4..offset + 8].copy_from_slice(&u32::MAX.to_le_bytes());
|
||||
}
|
||||
if trailing {
|
||||
out.extend(chunk(b"iXML", b"<BWFXML></BWFXML>"));
|
||||
}
|
||||
let size = out.len() - 8;
|
||||
if rf64 {
|
||||
out[20..28].copy_from_slice(&(size as u64).to_le_bytes());
|
||||
} else {
|
||||
out[4..8].copy_from_slice(&(size as u32).to_le_bytes());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rf64_metadata_stays_outside_audio() {
|
||||
for (name, rf64, trailing) in [("rf64.wav", true, false), ("riff_ixml.wav", false, true), ("rf64_ixml.wav", true, true)] {
|
||||
let bytes: Arc<[u8]> = fixture(rf64, trailing).into();
|
||||
let src = filmcraft_codecs::open_bytes(name, bytes.clone()).unwrap();
|
||||
let stream = filmcraft_media::reader::open_reader(
|
||||
name,
|
||||
Arc::new(filmcraft_media::reader::MemReader(bytes)),
|
||||
&filmcraft_codecs::reader_openers(),
|
||||
&filmcraft_codecs::openers(),
|
||||
)
|
||||
.unwrap();
|
||||
for s in [&src, &stream] {
|
||||
assert_eq!(s.info().duration, Tick::from_units(4, 48_000), "{name}");
|
||||
assert_eq!(s.audio(0, 20, 48_000).unwrap().channels.remove(0), vec![0.0; 20], "{name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -62,6 +62,7 @@ impl WavSource {
|
|||
let mut data = None;
|
||||
let mut bext_tr = None;
|
||||
let mut ixml_tr = None;
|
||||
let mut ds64_data = None;
|
||||
for _ in 0..MAX_CHUNKS {
|
||||
let Some(body) = pos.checked_add(8).filter(|&b| b <= len) else { break };
|
||||
let mut hdr = [0u8; 8];
|
||||
|
|
@ -69,8 +70,12 @@ impl WavSource {
|
|||
let id = &hdr[0..4];
|
||||
let mut clen = u64::from(le32(&hdr, 4));
|
||||
let rest = len - body;
|
||||
if id == b"data" && (clen == 0xFFFF_FFFF || clen > rest) {
|
||||
clen = rest;
|
||||
if id == b"data" {
|
||||
// RF64 sentinel: the real size is ds64.dataSize (EBU 3306 §3.4), else the rest of the file
|
||||
if clen == 0xFFFF_FFFF {
|
||||
clen = ds64_data.unwrap_or(rest);
|
||||
}
|
||||
clen = clen.min(rest);
|
||||
}
|
||||
if clen > rest {
|
||||
break;
|
||||
|
|
@ -87,6 +92,11 @@ impl WavSource {
|
|||
}
|
||||
fmt = Some((tag, ch, rate, bits));
|
||||
}
|
||||
// ds64: riffSize u64, dataSize u64, sampleCount u64, ...
|
||||
b"ds64" if clen >= 16 => {
|
||||
let b = read_range(&*reader, body + 8, 8).map_err(io)?;
|
||||
ds64_data = Some(u64::from(le32(&b, 0)) | u64::from(le32(&b, 4)) << 32);
|
||||
}
|
||||
b"data" => data = Some((body, clen)),
|
||||
// bext: Description 256, Originator 32, OriginatorReference 32, OriginationDate 10,
|
||||
// OriginationTime 8, then TimeReference (low u32, high u32)
|
||||
|
|
|
|||
Loading…
Reference in New Issue