Merge pull request #357 from HEOJUNFO/fix-317-aiff-float

Export: AIFF 32-bit float writes AIFF-C fl32 instead of 24-bit (#317)
This commit is contained in:
Brandon Thomas committed 2026-10-09 09:31:38 -04:00
commit f87f3b3e1a
3 files changed
+43 -11

No files matched your search

+1 -1
View File
@@ -10,7 +10,7 @@
//! | WebM | WebM (Matroska) | VP9 profile 0 (`effectcraft-vp9enc`: key + inter frames, a key frame every 2 s, loop filter), 8-bit 4:2:0; or AV1 (`effectcraft-av1enc`) | VP9 alpha (BlockAdditional) | Opus (`effectcraft-opusenc`: SILK / hybrid / CELT by bitrate and application), 48 kHz |
//! | HEVC | MP4 (`hvc1`) | `effectcraft-hevcenc` Main / Main 10 4:2:0, I + P slices, bitrate or constant quality, level, key-frame interval | no | AAC-LC (FilmCraft) |
//! | AV1 | MP4 (`av01`) | `effectcraft-av1enc` Main 8/10-bit 4:2:0, key + inter frames | no | AAC-LC (FilmCraft) |
//! | WAV / AIFF | RIFF WAVE / AIFF | — | — | PCM 16/24-bit (WAV also 32-bit float), mono/stereo (audio only) |
//! | WAV / AIFF | RIFF WAVE / AIFF (AIFF-C for float) | — | — | PCM 16/24-bit or 32-bit float, mono/stereo (audio only) |
//!
//! Every frame goes through [`pipeline`]: the Render Settings overrides (Effects, Solo Switches,
//! Guide Layers, Frame Blending, Motion Blur, Color Depth), field rendering (two fields half a
+24 -9
View File
@@ -9,8 +9,8 @@
//! frames every two seconds, inter frames between them: SimpleBlocks carry the key flag, inter
//! BlockGroups a ReferenceBlock; the alpha stream's key frames line up with the colour stream's),
//! Opus from `effectcraft-opusenc` (CELT, 48 kHz stereo, 20 ms packets,
//! `CodecDelay` = pre-skip). WAV (RIFF, 16-bit PCM) and AIFF (big-endian 16-bit, 80-bit
//! extended sample rate) follow their published file layouts.
//! `CodecDelay` = pre-skip). WAV (RIFF, PCM or IEEE float) and AIFF (big-endian PCM, 80-bit
//! extended sample rate; AIFF-C `fl32` for float) follow their published file layouts.
use std::io::{Seek, SeekFrom, Write};
@@ -322,14 +322,11 @@ fn ieee_extended(v: f64) -> [u8; 10] {
}
/// WAV or AIFF: the comp's audio over the render span (Audio Output: sample rate, mono/stereo,
/// 16/24-bit integer or 32-bit float; AIFF has no float in plain AIFF, so it writes 24-bit).
/// 16/24-bit integer or 32-bit float; plain AIFF has no float, so 32-bit float is AIFF-C `fl32`).
pub(crate) fn audio_file(job: &Cx, comp: &Comp, aiff: bool, st: &mut State) -> Result<Report> {
let sr = job.output.audio_sample_rate.clamp(8_000, 192_000);
let channels = if job.output.audio_channels == 1 { 1u16 } else { 2 };
let fmt = match job.output.audio_format {
AudioFormat::F32 if aiff => AudioFormat::S24,
f => f,
};
let fmt = job.output.audio_format;
let bytes_per_sample: u16 = match fmt {
AudioFormat::S16 => 2,
AudioFormat::S24 => 3,
@@ -363,15 +360,33 @@ pub(crate) fn audio_file(job: &Cx, comp: &Comp, aiff: bool, st: &mut State) -> R
let frames = (pcm.len() / block_align as usize) as u32;
let mut out = vec![];
if aiff {
// AIFF-C (Apple, 1991) for float: a format version chunk, and the COMM chunk names the
// compression type and its Pascal-string name, padded to an even length.
let float = fmt == AudioFormat::F32;
let mut comm = vec![];
comm.extend_from_slice(&channels.to_be_bytes());
comm.extend_from_slice(&frames.to_be_bytes());
comm.extend_from_slice(&(bytes_per_sample * 8).to_be_bytes());
comm.extend_from_slice(&ieee_extended(sr as f64));
let mut fver = vec![];
if float {
const NAME: &[u8] = b"32-bit floating point";
comm.extend_from_slice(b"fl32");
comm.push(NAME.len() as u8);
comm.extend_from_slice(NAME);
if (1 + NAME.len()) % 2 == 1 {
comm.push(0);
}
fver.extend_from_slice(b"FVER");
fver.extend_from_slice(&4u32.to_be_bytes());
// AIFC Version 1 (May 23, 1990, 2:40 pm).
fver.extend_from_slice(&0xA280_5140u32.to_be_bytes());
}
let ssnd_len = 8 + pcm.len();
out.extend_from_slice(b"FORM");
out.extend_from_slice(&((4 + 8 + comm.len() + 8 + ssnd_len + ssnd_len % 2) as u32).to_be_bytes());
out.extend_from_slice(b"AIFF");
out.extend_from_slice(&((4 + fver.len() + 8 + comm.len() + 8 + ssnd_len + ssnd_len % 2) as u32).to_be_bytes());
out.extend_from_slice(if float { b"AIFC" } else { b"AIFF" });
out.extend_from_slice(&fver);
out.extend_from_slice(b"COMM");
out.extend_from_slice(&(comm.len() as u32).to_be_bytes());
out.extend_from_slice(&comm);
+18 -1
View File
@@ -319,11 +319,28 @@ fn wav_and_aiff_channels_and_formats() {
let d = files.lock().unwrap()[0].1.clone();
let u16_at = |o: usize| u16::from_le_bytes([d[o], d[o + 1]]);
assert_eq!((u16_at(20), u16_at(22), u16_at(32), u16_at(34)), (3, 2, 8, 32), "IEEE float stereo");
// AIFF has no float: 24-bit.
// Plain AIFF has no float: 32-bit float is AIFF-C with `fl32` samples (#317).
let mut aiff = OutputModule::for_format(OutputFormat::Aiff);
aiff.audio_format = AudioFormat::F32;
let files = run(&p, cid, &s, &aiff, "/w/c.aif");
let d = files.lock().unwrap()[0].1.clone();
let u32_at = |o: usize| u32::from_be_bytes([d[o], d[o + 1], d[o + 2], d[o + 3]]);
assert_eq!((&d[..4], u32_at(4) as usize + 8, &d[8..12]), (&b"FORM"[..], d.len(), &b"AIFC"[..]));
assert_eq!((&d[12..16], u32_at(16), u32_at(20)), (&b"FVER"[..], 4, 0xA280_5140));
assert_eq!(&d[24..28], b"COMM");
let comm = 32;
assert_eq!(u32_at(28) as usize, 18 + 4 + 22, "COMM: AIFF fields, compression type, name");
assert_eq!(u16::from_be_bytes([d[comm], d[comm + 1]]), 2, "stereo");
assert_eq!(u32_at(comm + 2), 24_000, "half a second at 48 kHz");
assert_eq!(u16::from_be_bytes([d[comm + 6], d[comm + 7]]), 32, "sample size");
assert_eq!(&d[comm + 18..comm + 22], b"fl32");
assert_eq!(&d[comm + 22..comm + 44], b"\x1532-bit floating point");
let ssnd = comm + 44;
assert_eq!((&d[ssnd..ssnd + 4], u32_at(ssnd + 4) as usize), (&b"SSND"[..], 8 + 24_000 * 2 * 4));
// 16- and 24-bit stay plain AIFF.
aiff.audio_format = AudioFormat::S24;
let files = run(&p, cid, &s, &aiff, "/w/d.aif");
let d = files.lock().unwrap()[0].1.clone();
assert_eq!(&d[8..12], b"AIFF");
assert_eq!(u16::from_be_bytes([d[26], d[27]]), 24, "sample size");
}