mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 08:52:32 +00:00
* craft-audio: ALAC decoder and encoder, ported from Apple's reference Add `craft_audio::alac`, a Rust port of Apple's ALAC reference implementation (macosforge/alac@c38887c, Apache-2.0): - `alac::Decoder`: packets to planar i32, bit depths 1-32, 1-8 channels, cookies in MP4/Matroska (24-byte config), CAF and old QuickTime form. Hostile input returns errors; arithmetic wraps where C would overflow. - `alac::Encoder`: 16/20/24/32 bits, 1-8 channels, fast mode, any frame length; same packets as the reference for the same input. - `SpecificConfig` (cookie parse/write), `wav_order`/`alac_order`, `channel_mask`. - Features `alac-decode` (in `decode-native`) and `alac-encode` (in `encode`). `TrackDecoder` decodes ALAC natively, so ALAC in MP4 and Matroska no longer goes through symphonia (CAF still does). Apple's reference writes the cookie's channel layout tag in host byte order; this writes it big-endian as its cookie description specifies and reads either. Tests: unit tests for each part, an MP4 decode-and-seek test without symphonia, and ffmpeg oracles both ways (bit-exact, 1-8 channels). NOTICE and each file's header record the Apache-2.0 origin. * craft-audio: ALAC review fixes (#731) - Decode the full-width residual escape 0xffffffff as i32::MIN; the reference's `nd + 1` wraps it to 0. - Require a whole element tag after the last channel (the end tag, after any fill or data stream elements), so a packet that lost its last byte is an error instead of decoding without damage. Other elements there end the packet, as in the reference. - Refuse an ALAC track with no sample rate in its cookie or container. - Conceal a damaged packet with the length its header gives (`alac::Decoder::packet_frames`), falling back to the last packet's. - Matroska: place ALAC packet starts at multiples of the frame length instead of the rounded block timestamps, so seeks land on the exact frame. MP4 keeps its exact sample-table timing. * craft-audio: exact Matroska ALAC seeks after a short packet (#731) Packet starts were multiples of the frame length, so a short packet before the end moved every later packet and seeks after it returned the wrong frames. When a packet's timestamp is further from that multiple than its rounding allows, the starts are now counted from the earlier packets' headers (read once and kept). Other codecs with a fixed packet length fall back to the timestamp, as before. --------- Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
156 lines
6.7 KiB
Rust
156 lines
6.7 KiB
Rust
//! ALAC against ffmpeg (oracle; skipped when ffmpeg is absent): ffmpeg's ALAC files decode to the
|
|
//! exact input samples through `craft_audio::decode`, and ffmpeg decodes our encoder's packets,
|
|
//! muxed into M4A, to the exact input samples. Both in WAV channel order.
|
|
|
|
use craft_audio::alac::{Encoder, EncoderConfig, alac_order};
|
|
use craft_audio::craft_media::isobmff::{Brand, CodecConfig, FourCc, Mp4Writer, SampleEntry, TrackConfig, WriteSample, WriterOptions};
|
|
use craft_audio::{CodecId, decode};
|
|
use std::io::Cursor;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
|
|
fn dir() -> PathBuf {
|
|
crate::testkit::fixtures_dir("alac")
|
|
}
|
|
|
|
/// The ffmpeg channel layout ALAC uses for `channels`.
|
|
fn layout(channels: usize) -> &'static str {
|
|
["mono", "stereo", "3.0", "4.0", "5.0", "5.1", "6.1(back)", "7.1(wide)"][channels - 1]
|
|
}
|
|
|
|
/// `frames` frames of `channels` channels at `bits` bits, in WAV order: tones, noise bursts,
|
|
/// silence and full-scale peaks.
|
|
fn input(channels: usize, bits: u32, frames: usize) -> Vec<Vec<i32>> {
|
|
let max = ((1i64 << (bits - 1)) - 1) as f64;
|
|
let mut seed = 0x2545_F491u32;
|
|
(0..channels)
|
|
.map(|c| {
|
|
(0..frames)
|
|
.map(|i| {
|
|
seed ^= seed << 13;
|
|
seed ^= seed >> 17;
|
|
seed ^= seed << 5;
|
|
let noise = seed as f64 / f64::from(u32::MAX) * 2.0 - 1.0;
|
|
let tone = (i as f64 * (200.0 + 50.0 * c as f64) / 48_000.0 * std::f64::consts::TAU).sin();
|
|
let x = match (i / 2500) % 4 {
|
|
0 => tone * 0.7 + noise * 0.001,
|
|
1 => noise * 0.5,
|
|
2 => 0.0,
|
|
_ => tone.signum(),
|
|
};
|
|
(x * max).round().clamp(-max - 1.0, max) as i32
|
|
})
|
|
.collect()
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Interleaved s32le with each sample at the top of the word.
|
|
fn s32le(planar: &[Vec<i32>], bits: u32) -> Vec<u8> {
|
|
let mut out = Vec::new();
|
|
for i in 0..planar[0].len() {
|
|
for c in planar {
|
|
out.extend_from_slice(&(c[i] << (32 - bits)).to_le_bytes());
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
/// ffmpeg's decode of `file` as planar samples at `bits` bits.
|
|
fn ffmpeg_decode(ff: &Path, file: &Path, channels: usize, bits: u32) -> Vec<Vec<i32>> {
|
|
let out = Command::new(ff).args(["-v", "error", "-i"]).arg(file).args(["-f", "s32le", "-"]).output().unwrap();
|
|
assert!(out.status.success() && out.stderr.is_empty(), "{}", String::from_utf8_lossy(&out.stderr));
|
|
let mut planar = vec![Vec::new(); channels];
|
|
for (i, w) in out.stdout.as_chunks::<4>().0.iter().enumerate() {
|
|
planar[i % channels].push(i32::from_le_bytes([w[0], w[1], w[2], w[3]]) >> (32 - bits));
|
|
}
|
|
planar
|
|
}
|
|
|
|
/// Our encoder's packets for `wav` (WAV order) muxed into M4A.
|
|
fn our_m4a(wav: &[Vec<i32>], bits: u32, frame_length: u32) -> Vec<u8> {
|
|
let channels = wav.len();
|
|
let alac: Vec<&[i32]> = match alac_order(channels as u8) {
|
|
Some(order) => order.iter().map(|&w| wav[w].as_slice()).collect(),
|
|
None => wav.iter().map(Vec::as_slice).collect(),
|
|
};
|
|
let mut enc = Encoder::new(EncoderConfig::new(48_000, channels as u8, bits as u8).with_frame_length(frame_length)).unwrap();
|
|
let frames = wav[0].len();
|
|
let packets: Vec<(Vec<u8>, u32)> = (0..frames)
|
|
.step_by(frame_length as usize)
|
|
.map(|s| {
|
|
let e = (s + frame_length as usize).min(frames);
|
|
(enc.encode(&alac.iter().map(|c| &c[s..e]).collect::<Vec<_>>()).unwrap(), (e - s) as u32)
|
|
})
|
|
.collect();
|
|
let entry = SampleEntry::audio(FourCc(*b"alac"), CodecConfig::Alac { cookie: enc.cookie() }, channels as u32, 48_000.0, bits as u16);
|
|
let mut w = Mp4Writer::new(Cursor::new(Vec::new()), WriterOptions::new(Brand::Mp4)).unwrap();
|
|
let t = w.add_track(TrackConfig::new(entry, 48_000)).unwrap();
|
|
for (data, duration) in &packets {
|
|
w.write_sample(t, WriteSample { data, duration: *duration, composition_offset: 0, is_sync: true }).unwrap();
|
|
}
|
|
w.finish().unwrap().into_inner()
|
|
}
|
|
|
|
/// `craft_audio::decode` of an ALAC file, back to integers (exact up to 24 bits, which `f32`
|
|
/// holds).
|
|
fn our_decode(bytes: &[u8], bits: u32) -> Vec<Vec<i32>> {
|
|
let (info, buf) = decode(bytes, Some("m4a")).unwrap();
|
|
assert_eq!((info.codec, info.damaged), (CodecId::Alac, 0));
|
|
let scale = 2f64.powi(bits as i32 - 1);
|
|
buf.channels.iter().map(|c| c.iter().map(|&s| (f64::from(s) * scale) as i32).collect()).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn ffmpeg_alac_decodes_exactly() {
|
|
let Some(ff) = crate::testkit::ffmpeg_or_skip("alac oracle") else { return };
|
|
for (channels, bits) in [(1, 16), (2, 16), (2, 24), (3, 16), (4, 16), (5, 24), (6, 24), (7, 16), (8, 16)] {
|
|
let wav = input(channels, bits, 12_345);
|
|
let name = format!("ffmpeg_{channels}ch_{bits}");
|
|
let raw = dir().join(format!("{name}.s32"));
|
|
let m4a = dir().join(format!("{name}.m4a"));
|
|
std::fs::write(&raw, s32le(&wav, bits)).unwrap();
|
|
let fmt = if bits == 16 { "s16p" } else { "s32p" };
|
|
let st = Command::new(&ff)
|
|
.args(["-v", "error", "-y", "-f", "s32le", "-ar", "48000", "-ch_layout", layout(channels), "-i"])
|
|
.arg(&raw)
|
|
.args(["-c:a", "alac", "-sample_fmt", fmt])
|
|
.arg(&m4a)
|
|
.status()
|
|
.unwrap();
|
|
assert!(st.success(), "{name}: ffmpeg encode");
|
|
let ours = our_decode(&std::fs::read(&m4a).unwrap(), bits);
|
|
assert!(ours == wav, "{name}: decoded samples differ from the input");
|
|
}
|
|
}
|
|
|
|
/// 32-bit streams are left out: ffmpeg does not decode escaped (uncompressed) 32-bit stereo
|
|
/// frames ("not yet implemented"), which the noise in the input produces; Apple's decoder does.
|
|
#[test]
|
|
fn ffmpeg_decodes_our_alac_exactly() {
|
|
let Some(ff) = crate::testkit::ffmpeg_or_skip("alac oracle") else { return };
|
|
for (channels, bits, frame) in [
|
|
(1, 16, 4096),
|
|
(2, 16, 4096),
|
|
(2, 20, 4096),
|
|
(2, 24, 1000),
|
|
(3, 16, 4096),
|
|
(4, 24, 4096),
|
|
(5, 16, 4096),
|
|
(6, 24, 4096),
|
|
(7, 16, 4096),
|
|
(8, 24, 4096),
|
|
] {
|
|
let wav = input(channels, bits, 10_007);
|
|
let m4a = our_m4a(&wav, bits, frame);
|
|
let path = dir().join(format!("ours_{channels}ch_{bits}.m4a"));
|
|
std::fs::write(&path, &m4a).unwrap();
|
|
let theirs = ffmpeg_decode(&ff, &path, channels, bits);
|
|
assert!(theirs == wav, "{channels} ch, {bits} bits: ffmpeg's decode differs from the input");
|
|
// and our own reader on the same file
|
|
if bits <= 24 {
|
|
assert!(our_decode(&m4a, bits) == wav, "{channels} ch, {bits} bits: our decode differs");
|
|
}
|
|
}
|
|
}
|