From 7e40e129894760c395bb9e4fdbbe75d7c1e97e5e Mon Sep 17 00:00:00 2001 From: Aleksander Helgaker Date: Wed, 11 Nov 2026 14:18:09 +0100 Subject: [PATCH] craft-audio: ALAC decoder and encoder (#731) * 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 --- NOTICE | 7 + crates/audio/Cargo.toml | 15 +- crates/audio/README.md | 37 +- crates/audio/src/alac/ag.rs | 296 +++++++++++++ crates/audio/src/alac/bits.rs | 232 +++++++++++ crates/audio/src/alac/decoder.rs | 388 +++++++++++++++++ crates/audio/src/alac/dp.rs | 196 +++++++++ crates/audio/src/alac/encoder.rs | 580 ++++++++++++++++++++++++++ crates/audio/src/alac/matrix.rs | 120 ++++++ crates/audio/src/alac/mod.rs | 348 ++++++++++++++++ crates/audio/src/layout.rs | 9 +- crates/audio/src/lib.rs | 32 +- crates/audio/src/read/native.rs | 144 ++++++- crates/audio/src/track.rs | 153 ++++++- crates/audio/tests/audio/malformed.rs | 3 +- crates/audio/tests/codecs/alac.rs | 155 +++++++ crates/audio/tests/codecs/main.rs | 1 + 17 files changed, 2663 insertions(+), 53 deletions(-) create mode 100644 crates/audio/src/alac/ag.rs create mode 100644 crates/audio/src/alac/bits.rs create mode 100644 crates/audio/src/alac/decoder.rs create mode 100644 crates/audio/src/alac/dp.rs create mode 100644 crates/audio/src/alac/encoder.rs create mode 100644 crates/audio/src/alac/matrix.rs create mode 100644 crates/audio/src/alac/mod.rs create mode 100644 crates/audio/tests/codecs/alac.rs diff --git a/NOTICE b/NOTICE index bd88d43e7..1386b621f 100644 --- a/NOTICE +++ b/NOTICE @@ -252,3 +252,10 @@ Its white-level plateau estimate (saturation_plateau) is LightCraft's vendor::wh same revision. The CRW compression was derived clean-room from CC0 sample files and the maintainer's own photographs by black-box comparison with a reference decoder's command-line output; no raw-decoder source was consulted (crates/crw/clean-room/sources.md). + +crates/audio/src/alac/ (the ALAC decoder and encoder) is ported from Apple's ALAC reference +implementation, https://github.com/macosforge/alac at c38887c5c5e64a4b31108733bd79ca9b2496d987 +(codec/), Copyright (c) 2011 Apple Inc., licensed under the Apache License, Version 2.0 +(LICENSE-APACHE). The files are modified: translated to Rust, with bounds checks on hostile input, +wrapping integer arithmetic, the unrolled predictor and per-width matrix routines folded into one, +and the average bit rate filled in. Those files are Apache-2.0 only. diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index 0c17708a9..e380d9e96 100644 --- a/crates/audio/Cargo.toml +++ b/crates/audio/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "craft-audio" -description = "Audio file I/O and codecs: WAV/AIFF read and write, FLAC encoder, AAC decoder and encoder, Opus, AC-3/E-AC-3 and MP1/MP2 decoders." +description = "Audio file I/O and codecs: WAV/AIFF read and write, FLAC encoder, AAC and ALAC decoders and encoders, Opus, AC-3/E-AC-3 and MP1/MP2 decoders." version.workspace = true edition.workspace = true license.workspace = true @@ -14,16 +14,16 @@ all-features = true workspace = true [features] -# Probe and decode files: WAV and AIFF natively; AAC, Opus, AC-3/E-AC-3, MP1/MP2 and PCM through -# craft-media's demuxers and this crate's decoders; MP3, FLAC, ALAC, Vorbis, CAF, ADPCM, A-law +# Probe and decode files: WAV and AIFF natively; AAC, ALAC, Opus, AC-3/E-AC-3, MP1/MP2 and PCM +# through craft-media's demuxers and this crate's decoders; MP3, FLAC, Vorbis, CAF, ADPCM, A-law # and µ-law through symphonia. decode = ["decode-native", "symphonia"] # `decode` without symphonia: only the formats with a native decoder (others are Unsupported). -decode-native = ["aac-decode", "opus-decode", "ac3-decode", "mpa-decode", "craft-media/isobmff", "craft-media/matroska", "craft-media/ogg", "dep:log"] +decode-native = ["aac-decode", "alac-decode", "opus-decode", "ac3-decode", "mpa-decode", "craft-media/isobmff", "craft-media/matroska", "craft-media/ogg", "dep:log"] # The symphonia fallback for the formats with no native decoder yet. symphonia = ["decode-native", "dep:symphonia"] -# Every encoder: the lossless file writers and the AAC encoder. -encode = ["lossless-encode", "aac-encode"] +# Every encoder: the lossless file writers, the AAC encoder and the ALAC packet encoder. +encode = ["lossless-encode", "aac-encode", "alac-encode"] # Encode WAV/BWF/RF64, AIFF/AIFF-C and FLAC files. lossless-encode = ["pcm-encode", "flac-encode"] # Encode WAV/BWF/RF64 and AIFF/AIFF-C files. @@ -36,6 +36,9 @@ aac-encode = ["craft-media/bitstream"] opus-decode = [] ac3-decode = [] mpa-decode = ["craft-media/bitstream"] +# ALAC packet decoder and encoder (`alac`), ported from Apple's reference implementation. +alac-decode = [] +alac-encode = [] # Multi-resolution waveform overviews (`peaks`). peaks = [] # Encode AAC channels in parallel (with `aac-encode`). diff --git a/crates/audio/README.md b/crates/audio/README.md index d3db9754e..ee550a9d8 100644 --- a/crates/audio/README.md +++ b/crates/audio/README.md @@ -1,7 +1,7 @@ # craft-audio Audio file I/O and codecs for the Craft apps. Native implementations: WAV and AIFF read/write, -FLAC encoder, AAC decoder/encoder, Opus, AC-3/E-AC-3 and MP1/MP2 decoders. +FLAC encoder, AAC and ALAC decoders/encoders, Opus, AC-3/E-AC-3 and MP1/MP2 decoders. Formats without a native decoder are decoded by [symphonia](#symphonia), which is to be replaced by native decoders. @@ -11,7 +11,9 @@ by native decoders. per channel), in WAV channel order (L R C LFE, back, side) whenever the channel layout is known. - Malformed input returns an error, never a panic (see [symphonia](#symphonia) for the one exception). No `unsafe`. Readers and decoders are tested with corrupted and truncated input. -- Implemented from the published specifications; tested against ffmpeg and conformance vectors. +- Implemented from the published specifications, except ALAC, which is ported from Apple's + Apache-2.0 reference implementation (see [Origin](#origin)); tested against ffmpeg and + conformance vectors. Builds for `wasm32-unknown-unknown`. - Containers (MP4/MOV, Matroska/WebM, Ogg), bit I/O and the `Container`/`CodecId`/`CodecParams` types come from [`craft-media`](../media/README.md). This crate never depends on video codecs. @@ -24,13 +26,15 @@ No default features. The shared types (`AudioBuffer`, `AudioInfo`, `ChannelLayou | Feature | Provides | Pulls in | |---|---|---| | `decode` | `decode`, `probe`, `Decoder`, `tracks`, `TrackDecoder`; every format in [Format support](#format-support) | `decode-native`, `symphonia` | -| `decode-native` | the same, native formats only (others are `Unsupported`) | the four decoders below, craft-media `isobmff`/`matroska`/`ogg`, `log` | +| `decode-native` | the same, native formats only (others are `Unsupported`) | the five decoders below, craft-media `isobmff`/`matroska`/`ogg`, `log` | | `symphonia` | the fallback for formats with no native decoder; turning it on is the same as `decode` | `decode-native`, symphonia | -| `encode` | every encoder: the WAV, AIFF and FLAC file writers, and the AAC packet encoder | `lossless-encode`, `aac-encode` | +| `encode` | every encoder: the WAV, AIFF and FLAC file writers, and the AAC and ALAC packet encoders | `lossless-encode`, `aac-encode`, `alac-encode` | | `lossless-encode` | WAV, AIFF and FLAC writing | `pcm-encode`, `flac-encode` | | `pcm-encode` | `encode`, `FileEncoder` for WAV and AIFF; `wav_float_header` | none | | `flac-encode` | `encode`, `FileEncoder` for FLAC | craft-media `bitstream` | | `aac-decode`, `aac-encode` | `aac::Decoder`, `aac::Encoder` | craft-media `bitstream` | +| `alac-decode` | `alac::Decoder` | none | +| `alac-encode` | `alac::Encoder` | none | | `opus-decode` | `opus::Decoder`/`StreamDecoder` | none | | `ac3-decode` | `ac3::Decoder` | none | | `mpa-decode` | `mpa::Decoder` | craft-media `bitstream` | @@ -38,8 +42,8 @@ No default features. The shared types (`AudioBuffer`, `AudioInfo`, `ChannelLayou | `rayon` | parallel AAC encoding (with `aac-encode`) | `rayon` | | `serde` | `Serialize`/`Deserialize` on the shared types | `serde` | -Any one decoder feature (`aac-decode`, `opus-decode`, `ac3-decode`, `mpa-decode`) also gives -`TrackDecoder`, for that codec and PCM. +Any one decoder feature (`aac-decode`, `alac-decode`, `opus-decode`, `ac3-decode`, `mpa-decode`) +also gives `TrackDecoder`, for that codec and PCM. An app that decodes 32-bit FLAC with `decode` needs a profile override in its workspace; see [symphonia](#symphonia). @@ -294,15 +298,16 @@ match craft_audio::decode(&bytes, None) { | FLAC | symphonia | native | | AAC-LC | native | native | | HE-AAC v1/v2 | native, core only (see [Limits](#limits)) | — | +| ALAC | native | native, packets (`alac::Encoder`; no file writer) | | MP1, MP2 (MPEG audio Layer I/II) | native | — | | MP3 (Layer III) | symphonia | — | | Opus | native | — | | AC-3, E-AC-3 | native | — | -| ALAC, Vorbis, ADPCM, A-law, µ-law | symphonia | — | +| Vorbis, ADPCM, A-law, µ-law | symphonia | — | | Container | Demuxer | Timing | |---|---|---| -| MP4, M4A, MOV | `craft_media::isobmff` for native codecs; symphonia for the others (ALAC, MP3, FLAC) | edit lists (AAC priming, Opus pre-skip) | +| MP4, M4A, MOV | `craft_media::isobmff` for native codecs; symphonia for the others (MP3, FLAC) | edit lists (AAC priming, Opus pre-skip) | | Matroska, WebM | `craft_media::matroska` for native codecs; symphonia for the others (Vorbis, FLAC, MP3) | `CodecDelay`, `DiscardPadding` | | Ogg | `craft_media::ogg` (Opus); symphonia (Vorbis, FLAC) | pre-skip, end trimming | | Raw ADTS AAC, AC-3, E-AC-3, MP1/MP2 | native frame sync | — | @@ -352,8 +357,16 @@ match craft_audio::decode(&bytes, None) { - `ChannelLayout::unordered` is written to FLAC as mask `0x0000`, as RFC 9639 §8.6.2 allows, but ffmpeg and `flac -d` ignore that mask and use their default layout for the channel count. - Not decoded at all: Speex. -- `TrackDecoder` returns `Unsupported` for MP3, FLAC, ALAC and Vorbis; `decode` and `Decoder` read - those through symphonia. +- `TrackDecoder` returns `Unsupported` for MP3, FLAC and Vorbis; `decode` and `Decoder` read + those through symphonia. ALAC in CAF still goes through symphonia (craft-media has no CAF + demuxer). +- ALAC: `EncodeFormat` has no ALAC entry; `alac::Encoder` writes packets and the configuration, + and the caller muxes them (MP4 `alac` box, Matroska `A_ALAC`). craft-media's MP4 muxer writes no + `chan` box, so an MP4 of more than two channels carries no explicit layout; decoders then use the + standard ALAC layout for the count. +- ALAC 32-bit streams: ffmpeg does not decode their uncompressed (escaped) stereo frames, which + Apple's encoder and this one write for audio that does not compress; Apple's decoder and this + one do. - MP4 edit lists: a single media edit (encoder priming and the end) is applied, alone or after one empty edit; the empty edit's delay is not added as silence. Other edit lists are not applied, and the media plays as it is stored. @@ -390,6 +403,7 @@ Each codec module's rustdoc lists the coding tools it implements. | Opus (`opus`) | RFC 6716, 8251, 7845 | decoder: SILK, hybrid, CELT, all frame sizes, PLC and FEC, multistream (`Decoder`) and single stream (`StreamDecoder`), 8–48 kHz output | all 36 RFC 8251 conformance cases | | AC-3/E-AC-3 (`ac3`) | ATSC A/52:2012 | all AC-3 channel modes and frame sizes; E-AC-3 independent substream 0 with spectral extension; output in WAV order, no downmix | ffmpeg: 65–94 dB SNR on tones, 27–48 dB on white noise (see [Limits](#limits)) | | MP1/MP2 (`mpa`) | ISO/IEC 11172-3, 13818-3 | Layer I and II, MPEG-1/2/2.5, 8–48 kHz; also the Layer II rate/mode pairs the spec disallows (ffmpeg's default mono 384 kbit/s); no free format | ffmpeg-encoded streams, 73–84 dB SNR | +| ALAC (`alac`) | Apple's reference implementation | decode and encode, 1–8 channels, 16/20/24/32 bits (decode: any depth 1–32), fast mode, magic cookie in MP4/Matroska and CAF form | Apple's reference: same packets from the encoder, same samples from both decoders on 766 configurations; ffmpeg: bit-exact both ways | ## symphonia @@ -456,6 +470,7 @@ the full tests with ffmpeg and the vectors required. |---|---| | WAV, AIFF, `decode`/`probe`, peaks | SoundCraft `soundcraft-audio-io` [7dc56a4](https://github.com/storytold/soundcraft/commit/7dc56a4dbeebf57eacece09536726c57844fc59d) | | FLAC encoder, AAC, Opus decoder, AC-3, MP1/MP2, Ogg | FilmCraft [8fda2f2](https://github.com/storytold/filmcraft/commit/8fda2f232198c4c79ff9cfe5b6a347bb1375fa9e) | +| ALAC | Apple's reference implementation [macosforge/alac@c38887c](https://github.com/macosforge/alac/tree/c38887c5c5e64a4b31108733bd79ca9b2496d987/codec), Copyright © 2011 Apple Inc., Apache-2.0; ported to Rust and modified (see the `alac` module docs and [NOTICE](../../NOTICE)) | Where apps had overlapping implementations, the one that measured better was kept, with the tests of both: @@ -502,7 +517,7 @@ The codec modules' public items keep their names, so apps can re-export them short slices instead of filling them with silence. - `DecodeOptions` is not `Copy`. - The FLAC encoder's vendor string is `craft-audio `. -- `decode` tries native decoders first. AAC, Opus, AC-3 and MP2 no longer go through symphonia, except AAC in +- `decode` tries native decoders first. AAC, ALAC, Opus, AC-3 and MP2 no longer go through symphonia, except AAC in CAF. MP4 and Matroska files decode to presentation length. A packet that fails is concealed, not dropped. A failed source read is `AudioError::Io`. diff --git a/crates/audio/src/alac/ag.rs b/crates/audio/src/alac/ag.rs new file mode 100644 index 000000000..e81344193 --- /dev/null +++ b/crates/audio/src/alac/ag.rs @@ -0,0 +1,296 @@ +// Ported from Apple's ALAC reference implementation (aglib.h, ag_dec.c and ag_enc.c, macosforge/alac@c38887c, +// https://github.com/macosforge/alac). Copyright (c) 2011 Apple Inc. +// Licensed under the Apache License, Version 2.0 (LICENSE-APACHE at the repository root). +// Modified: translated to Rust and changed as the `alac` module docs describe. +// SPDX-License-Identifier: Apache-2.0 + +//! Adaptive Golomb-Rice coding of prediction residuals, from Apple's `aglib.h`, `ag_dec.c` and +//! `ag_enc.c`. +//! +//! Arithmetic is `u32`/`i32` with wrapping, as the C code has on the platforms it targets, so +//! hostile parameters give garbage samples rather than a panic. Peeks past the packet read zero +//! bits; [`decompress`] fails when the codes it consumed end past the packet. + +use super::bits::{BitReader, BitSink}; +use super::{Error, Result}; + +pub(crate) const QBSHIFT: u32 = 9; +pub(crate) const QB: u32 = 1 << QBSHIFT; +pub(crate) const PB0: u32 = 40; +pub(crate) const MB0: u32 = 10; +pub(crate) const KB0: u32 = 14; +pub(crate) const MAX_RUN_DEFAULT: u32 = 255; +const MMULSHIFT: u32 = 2; +const MDENSHIFT: u32 = QBSHIFT - MMULSHIFT - 1; +const MOFF: u32 = 1 << (MDENSHIFT - 2); +const BITOFF: u32 = 24; +/// Longest prefix of ones before an escaped value. +const MAX_PREFIX_16: u32 = 9; +const MAX_PREFIX_32: u32 = 9; +/// Width of an escaped zero-run length. +const MAX_DATATYPE_BITS_16: u32 = 16; +const N_MAX_MEAN_CLAMP: u32 = 0xffff; +const N_MEAN_CLAMP_VAL: u32 = 0xffff; + +/// `AGParamRec`: the coder's tuning (`fw`/`sw`, the image-row widths, are always the sample +/// count for audio and not kept). +#[derive(Clone, Copy, Debug)] +pub(crate) struct AgParams { + mb0: u32, + pb: u32, + kb: u32, + wb: u32, +} + +impl AgParams { + /// `set_ag_params` (`maxrun` is unused by the coder). + pub(crate) fn new(mb: u32, pb: u32, kb: u32) -> Self { + let wb = 1u32.checked_shl(kb).unwrap_or(0).wrapping_sub(1); + Self { mb0: mb, pb, kb, wb } + } + + /// `set_standard_ag_params`. + pub(crate) fn standard() -> Self { + Self::new(MB0, PB0, KB0) + } +} + +/// Leading zero bits of `m` (`lead`). +fn lead(m: u32) -> u32 { + m.leading_zeros() +} + +/// `lg3a`: floor(log2(x + 3)). +fn lg3a(x: u32) -> u32 { + 31u32.wrapping_sub(lead(x.wrapping_add(3))) +} + +/// `(1 << k) - 1`, 0 for `k` of 32 or more. +fn mask(k: u32) -> u32 { + 1u32.checked_shl(k).unwrap_or(0).wrapping_sub(1) +} + +/// The Rice parameter of the zero-run length that follows when the mean has dropped this low. +fn zero_run_k(mb: u32) -> u32 { + lead(mb).wrapping_sub(BITOFF).wrapping_add(mb.wrapping_add(MOFF) >> MDENSHIFT) +} + +/// The next mean, after a value of `n` (`zmode` is 1 right after a zero run). +fn next_mean(pb: u32, mb: u32, n: u32, zmode: u32) -> u32 { + let mb = pb.wrapping_mul(n.wrapping_add(zmode)).wrapping_add(mb).wrapping_sub(pb.wrapping_mul(mb) >> QBSHIFT); + if n > N_MAX_MEAN_CLAMP { N_MEAN_CLAMP_VAL } else { mb } +} + +/// `dyn_get`: a zero-run length, coded with parameter `k` (escape: 16 raw bits). +fn get_run(r: &BitReader, pos: &mut usize, m: u32, k: u32) -> u32 { + let pre = lead(!r.peek_at(*pos, 32)); + if pre >= MAX_PREFIX_16 { + let v = r.peek_at(*pos + MAX_PREFIX_16 as usize, MAX_DATATYPE_BITS_16); + *pos += (MAX_PREFIX_16 + MAX_DATATYPE_BITS_16) as usize; + v + } else { + let v = r.peek_at(*pos + pre as usize + 1, k); + *pos += (pre + 1 + k) as usize; + let mut result = pre.wrapping_mul(m).wrapping_add(v).wrapping_sub(1); + if v < 2 { + result = result.wrapping_sub(v.wrapping_sub(1)); + *pos -= 1; + } + result + } +} + +/// `dyn_get_32bit`: one residual magnitude, coded with parameter `k` (escape: `maxbits` raw bits). +fn get_value(r: &BitReader, pos: &mut usize, m: u32, k: u32, maxbits: u32) -> u32 { + let pre = lead(!r.peek_at(*pos, 32)); + if pre >= MAX_PREFIX_32 { + let v = r.peek_at(*pos + MAX_PREFIX_32 as usize, maxbits); + *pos += (MAX_PREFIX_32 + maxbits) as usize; + return v; + } + *pos += pre as usize + 1; + if k == 1 { + return pre; + } + let v = r.peek_at(*pos, k); + // k - 1 bits when the suffix is 0 or 1, else k (k is 0 only in a hostile stream, which + // moves back by one; the caller's loop is bounded by the sample count). + *pos = (*pos + k as usize).saturating_sub(1); + let mut result = pre.wrapping_mul(m); + if v >= 2 { + result = result.wrapping_add(v - 1); + *pos += 1; + } + result +} + +/// `dyn_decomp`: `out.len()` residuals of up to `max_size` bits from `r`, which is advanced past +/// them. +pub(crate) fn decompress(params: AgParams, r: &mut BitReader, out: &mut [i32], max_size: u32) -> Result<()> { + let num = out.len(); + let max_pos = r.len_bits(); + let mut pos = r.position(); + let mut mb = params.mb0; + let mut zmode = 0u32; + let mut c = 0usize; + while c < num { + if pos >= max_pos { + return Err(Error::Eof); + } + let k = lg3a(mb >> QBSHIFT).min(params.kb); + let m = mask(k); + let n = get_value(r, &mut pos, m, k, max_size); + // the least significant bit is the sign + let nd = n.wrapping_add(zmode); + let multiplier = (nd & 1).wrapping_neg() | 1; + // ceil(nd / 2) without the reference's `nd + 1`, which wraps 0xffffffff (i32::MIN) to 0 + out[c] = ((nd >> 1) + (nd & 1)).wrapping_mul(multiplier) as i32; + c += 1; + mb = next_mean(params.pb, mb, n, zmode); + zmode = 0; + if (mb << MMULSHIFT) < QB && c < num { + zmode = 1; + let k = zero_run_k(mb); + let mz = mask(k) & params.wb; + let run = get_run(r, &mut pos, mz, k); + let run_end = c.checked_add(run as usize).filter(|&e| e <= num).ok_or(Error::Bitstream("zero run past the end of the frame"))?; + out[c..run_end].fill(0); + c = run_end; + if run >= 65535 { + zmode = 0; + } + mb = 0; + } + } + r.set_position(pos) +} + +/// `dyn_code`: a zero-run length as (value, bits). +fn code_run(m: u32, k: u32, n: u32) -> (u32, u32) { + let escape = || (((1u32 << MAX_PREFIX_16) - 1) << MAX_DATATYPE_BITS_16).wrapping_add(n); + let div = n / m.max(1); + if div >= MAX_PREFIX_16 { + return (escape(), MAX_PREFIX_16 + MAX_DATATYPE_BITS_16); + } + let rem = n % m.max(1); + let de = u32::from(rem == 0); + let bits = div + k + 1 - de; + if bits > MAX_PREFIX_16 + MAX_DATATYPE_BITS_16 { + return (escape(), MAX_PREFIX_16 + MAX_DATATYPE_BITS_16); + } + let value = (mask(div) << (bits - div)).wrapping_add(rem).wrapping_add(1).wrapping_sub(de); + (value, bits) +} + +/// `dyn_code_32bit`: a residual magnitude as (value, bits), plus the raw `maxbits`-bit escape +/// that follows a prefix of nine ones. +fn code_value(maxbits: u32, m: u32, k: u32, n: u32) -> (u32, u32, Option<(u32, u32)>) { + let div = n / m.max(1); + if div < MAX_PREFIX_32 { + let rem = n - m * div; + let de = u32::from(rem == 0); + let bits = div + k + 1 - de; + if bits <= 25 { + let value = (mask(div) << (bits - div)).wrapping_add(rem).wrapping_add(1).wrapping_sub(de); + return (value, bits, None); + } + } + (mask(MAX_PREFIX_32), MAX_PREFIX_32, Some((n, maxbits))) +} + +/// `dyn_comp`: code the residuals `pc`, each of up to `bit_size` bits, into `out`. +pub(crate) fn compress(params: AgParams, pc: &[i32], out: &mut impl BitSink, bit_size: u32) -> Result<()> { + if !(1..=32).contains(&bit_size) { + return Err(Error::InvalidInput("residual width outside 1 to 32 bits")); + } + let num = pc.len(); + let mut mb = params.mb0; + let mut zmode = 0u32; + let mut c = 0usize; + while c < num { + let k = lg3a(mb >> QBSHIFT).min(params.kb); + let m = mask(k); + let del = pc[c]; + let n = (del.unsigned_abs() << 1).wrapping_sub(u32::from(del < 0)).wrapping_sub(zmode); + let (value, bits, escape) = code_value(bit_size, m, k, n); + out.put(value, bits); + if let Some((raw, raw_bits)) = escape { + out.put(raw, raw_bits); + } + c += 1; + mb = next_mean(params.pb, mb, n, zmode); + zmode = 0; + if (mb << MMULSHIFT) < QB && c < num { + zmode = 1; + let mut nz = 0u32; + while c < num && pc[c] == 0 { + nz += 1; + c += 1; + if nz >= 65535 { + zmode = 0; + break; + } + } + let k = zero_run_k(mb); + let mz = mask(k) & params.wb; + let (value, bits) = code_run(mz, k, nz); + out.put(value, bits); + mb = 0; + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::super::bits::{BitReader, BitSink, BitWriter}; + use super::*; + + fn roundtrip(values: &[i32], bits: u32) { + let mut w = BitWriter::new(); + compress(AgParams::standard(), values, &mut w, bits).unwrap(); + let len = w.bit_len(); + let bytes = w.into_bytes(); + let mut r = BitReader::new(&bytes); + let mut out = vec![0; values.len()]; + decompress(AgParams::standard(), &mut r, &mut out, bits).unwrap(); + assert_eq!(out, values); + assert_eq!(r.position(), len); + } + + #[test] + fn residuals_roundtrip() { + roundtrip(&[0, 1, -1, 2, -2, 100, -100, 30000, -32768, 32767], 16); + roundtrip(&[0; 300], 16); + let mut v = vec![0; 70_000]; + v[0] = 5; + v[69_999] = -3; + roundtrip(&v, 16); + let wide: Vec = (0..500).map(|i| ((i * 7919) % 65_536 - 32_768) << 8).collect(); + roundtrip(&wide, 25); + roundtrip(&[i32::MAX >> 1, i32::MIN >> 1, 0, 0, 0, 1], 32); + } + + /// The largest escaped magnitude, 0xffffffff, is i32::MIN (the reference wraps it to 0). + #[test] + fn the_full_width_escape_decodes_to_the_minimum() { + let mut w = BitWriter::new(); + w.put(0x1FF, 9); // nine ones: escape + w.put(u32::MAX, 32); + let bytes = w.into_bytes(); + let mut out = [7]; + decompress(AgParams::standard(), &mut BitReader::new(&bytes), &mut out, 32).unwrap(); + assert_eq!(out, [i32::MIN]); + } + + #[test] + fn a_truncated_stream_is_an_error() { + let mut w = BitWriter::new(); + let values: Vec = (0..64).map(|i| i * 1000 - 32_000).collect(); + compress(AgParams::standard(), &values, &mut w, 16).unwrap(); + let bytes = w.into_bytes(); + let mut r = BitReader::new(&bytes[..bytes.len() / 2]); + let mut out = vec![0; values.len()]; + assert!(decompress(AgParams::standard(), &mut r, &mut out, 16).is_err()); + } +} diff --git a/crates/audio/src/alac/bits.rs b/crates/audio/src/alac/bits.rs new file mode 100644 index 000000000..1ff8d10f0 --- /dev/null +++ b/crates/audio/src/alac/bits.rs @@ -0,0 +1,232 @@ +// Ported from Apple's ALAC reference implementation (ALACBitUtilities.c, macosforge/alac@c38887c, +// https://github.com/macosforge/alac). Copyright (c) 2011 Apple Inc. +// Licensed under the Apache License, Version 2.0 (LICENSE-APACHE at the repository root). +// Modified: translated to Rust and changed as the `alac` module docs describe. +// SPDX-License-Identifier: Apache-2.0 + +//! MSB-first bit reading and writing, from Apple's `ALACBitUtilities.c`. +//! +//! The reader never reads past its slice: [`BitReader::peek`] pads with zero bits, and every +//! read that would end past the data is [`Error::Eof`]. The writer can rewind, as the reference +//! encoder does when a compressed element turns out larger than its escape (uncompressed) form. + +use super::{Error, Result}; + +/// Reader over one packet. +#[derive(Clone, Copy)] +pub(crate) struct BitReader<'a> { + data: &'a [u8], + /// Position in bits from the start of `data`. + pos: usize, +} + +impl<'a> BitReader<'a> { + pub(crate) fn new(data: &'a [u8]) -> Self { + Self { data, pos: 0 } + } + + /// Bit position from the start. + pub(crate) fn position(&self) -> usize { + self.pos + } + + /// Bits in the packet. + pub(crate) fn len_bits(&self) -> usize { + self.data.len().saturating_mul(8) + } + + /// The next `n` (at most 32) bits at bit `pos`, right-aligned, with zeros past the end. + pub(crate) fn peek_at(&self, pos: usize, n: u32) -> u32 { + if n == 0 { + return 0; + } + let byte = pos / 8; + let mut window: u64 = 0; + for i in 0..8 { + window = (window << 8) | u64::from(self.data.get(byte.wrapping_add(i)).copied().unwrap_or(0)); + } + // `pos % 8 + n` is at most 39, so the wanted bits lie in the 64-bit window. + let shift = 64 - (pos % 8) as u32 - n.min(32); + ((window >> shift) & ((1u64 << n.min(32)) - 1)) as u32 + } + + /// The next `n` bits without consuming them. + pub(crate) fn peek(&self, n: u32) -> u32 { + self.peek_at(self.pos, n) + } + + /// Read `n` (at most 32) bits. + pub(crate) fn read(&mut self, n: u32) -> Result { + let end = self.pos.checked_add(n as usize).ok_or(Error::Eof)?; + if end > self.len_bits() { + return Err(Error::Eof); + } + let v = self.peek(n); + self.pos = end; + Ok(v) + } + + /// Read `n` bits as a two's-complement value of that width. + pub(crate) fn read_signed(&mut self, n: u32) -> Result { + let v = self.read(n)?; + Ok(sign_extend(v, n)) + } + + /// Skip `n` bits; skipping past the end is [`Error::Eof`]. + pub(crate) fn skip(&mut self, n: usize) -> Result<()> { + let end = self.pos.checked_add(n).ok_or(Error::Eof)?; + if end > self.len_bits() { + return Err(Error::Eof); + } + self.pos = end; + Ok(()) + } + + /// Move to `pos`, which the caller has checked lies within the packet. + pub(crate) fn set_position(&mut self, pos: usize) -> Result<()> { + if pos > self.len_bits() { + return Err(Error::Eof); + } + self.pos = pos; + Ok(()) + } + + /// Skip to the next byte boundary (`BitBufferByteAlign` without zeros). + pub(crate) fn byte_align(&mut self) { + // The packet is whole bytes, so the boundary is never past its end. + self.pos = self.pos.div_ceil(8).saturating_mul(8); + } +} + +/// The low `n` bits of `v` as a signed value (`n` of 0 gives 0, 32 or more the value itself). +pub(crate) fn sign_extend(v: u32, n: u32) -> i32 { + match n { + 0 => 0, + 1..=31 => { + let s = 32 - n; + ((v << s) as i32) >> s + } + _ => v as i32, + } +} + +/// Where bits go: a [`BitWriter`], or a [`BitCounter`] for the encoder's trial runs. +pub(crate) trait BitSink { + /// Write the low `n` (at most 32) bits of `value`. + fn put(&mut self, value: u32, n: u32); + /// Bits written so far. + fn bit_len(&self) -> usize; +} + +/// Growable MSB-first writer. +#[derive(Default)] +pub(crate) struct BitWriter { + buf: Vec, + bits: usize, +} + +impl BitWriter { + pub(crate) fn new() -> Self { + Self::default() + } + + /// Drop everything after bit `pos` (no-op when `pos` is past the end). + pub(crate) fn truncate(&mut self, pos: usize) { + if pos >= self.bits { + return; + } + self.bits = pos; + self.buf.truncate(pos.div_ceil(8)); + if let Some(last) = self.buf.last_mut() + && !pos.is_multiple_of(8) + { + *last &= 0xFFu8 << (8 - pos % 8); + } + } + + /// Pad with zero bits to a byte boundary. + pub(crate) fn byte_align(&mut self) { + let pad = (8 - self.bits % 8) % 8; + self.put(0, pad as u32); + } + + pub(crate) fn into_bytes(mut self) -> Vec { + self.byte_align(); + self.buf + } +} + +impl BitSink for BitWriter { + fn put(&mut self, value: u32, n: u32) { + let n = n.min(32); + for i in (0..n).rev() { + let bit = ((value >> i) & 1) as u8; + if self.bits.is_multiple_of(8) { + self.buf.push(0); + } + if let Some(last) = self.buf.last_mut() { + *last |= bit << (7 - self.bits % 8); + } + self.bits += 1; + } + } + + fn bit_len(&self) -> usize { + self.bits + } +} + +/// Counts bits without storing them. +#[derive(Default)] +pub(crate) struct BitCounter(pub(crate) usize); + +impl BitSink for BitCounter { + fn put(&mut self, _value: u32, n: u32) { + self.0 += n.min(32) as usize; + } + + fn bit_len(&self) -> usize { + self.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn writer_and_reader_agree() { + let mut w = BitWriter::new(); + w.put(0b101, 3); + w.put(0xABCD, 16); + w.put(0xDEAD_BEEF, 32); + w.put(1, 1); + assert_eq!(w.bit_len(), 52); + let bytes = w.into_bytes(); + let mut r = BitReader::new(&bytes); + assert_eq!(r.read(3), Ok(0b101)); + assert_eq!(r.read(16), Ok(0xABCD)); + assert_eq!(r.read(32), Ok(0xDEAD_BEEF)); + assert_eq!(r.read(1), Ok(1)); + assert_eq!(r.read(4), Ok(0)); + assert_eq!(r.read(1), Err(Error::Eof)); + assert_eq!(r.peek(32), 0); + } + + #[test] + fn truncate_clears_the_dropped_bits() { + let mut w = BitWriter::new(); + w.put(0xFFFF, 16); + w.truncate(5); + w.put(0, 3); + assert_eq!(w.into_bytes(), vec![0xF8]); + } + + #[test] + fn sign_extension() { + assert_eq!(sign_extend(0xF, 4), -1); + assert_eq!(sign_extend(0x7, 4), 7); + assert_eq!(sign_extend(0x8000_0000, 32), i32::MIN); + assert_eq!(sign_extend(5, 0), 0); + } +} diff --git a/crates/audio/src/alac/decoder.rs b/crates/audio/src/alac/decoder.rs new file mode 100644 index 000000000..f728936b3 --- /dev/null +++ b/crates/audio/src/alac/decoder.rs @@ -0,0 +1,388 @@ +// Ported from Apple's ALAC reference implementation (ALACDecoder.cpp, macosforge/alac@c38887c, +// https://github.com/macosforge/alac). Copyright (c) 2011 Apple Inc. +// Licensed under the Apache License, Version 2.0 (LICENSE-APACHE at the repository root). +// Modified: translated to Rust and changed as the `alac` module docs describe. +// SPDX-License-Identifier: Apache-2.0 + +//! The packet decoder, from Apple's `ALACDecoder.cpp`. + +use super::ag::{self, AgParams}; +use super::bits::BitReader; +use super::dp::{unpc_block, unpc_first_order_in_place}; +use super::matrix::{Mix, unmix, unshift}; +use super::{Error, ID_CCE, ID_CPE, ID_DSE, ID_END, ID_FIL, ID_LFE, ID_PCE, ID_SCE, MAX_CHANNELS, MAX_FRAME_LENGTH, Result, SpecificConfig}; + +/// Decodes ALAC packets to planar `i32` (see the [module docs](super) for the sample format). +/// +/// Packets decode independently of each other: there is no state to reset after a seek. +#[derive(Debug, Clone)] +pub struct Decoder { + config: SpecificConfig, +} + +/// One channel's prediction parameters from an element header. +struct ChannelParams { + mode: u32, + den_shift: u32, + pb_factor: u32, + coefs: [i16; 32], + num_coefs: usize, +} + +impl Decoder { + /// A decoder for the stream `cookie` (the magic cookie, [`SpecificConfig::parse`]) describes. + pub fn new(cookie: &[u8]) -> Result { + Self::from_config(SpecificConfig::parse(cookie)?) + } + + /// A decoder for `config`. + pub fn from_config(config: SpecificConfig) -> Result { + if config.compatible_version > 0 { + return Err(Error::Unsupported("a compatible version above 0")); + } + if !(1..=32).contains(&config.bit_depth) { + return Err(Error::InvalidConfig("bit depth outside 1 to 32")); + } + if config.channels == 0 || config.channels > MAX_CHANNELS { + return Err(Error::InvalidConfig("channel count outside 1 to 8")); + } + if config.frame_length == 0 || config.frame_length > MAX_FRAME_LENGTH { + return Err(Error::InvalidConfig("frame length outside 1 to 2^20")); + } + Ok(Self { config }) + } + + /// The stream configuration. + pub fn config(&self) -> &SpecificConfig { + &self.config + } + + /// Channel count. + pub fn channels(&self) -> usize { + usize::from(self.config.channels) + } + + /// Sample rate from the cookie. + pub fn sample_rate(&self) -> u32 { + self.config.sample_rate + } + + /// Bits per sample. + pub fn bit_depth(&self) -> u32 { + u32::from(self.config.bit_depth) + } + + /// The frames `packet` holds, from its first element's header alone (for concealing a packet + /// that fails to decode): the partial-frame count when the header has one, else the frame + /// length. `None` when the packet does not start with an audio element or its header is cut + /// short or out of range. + pub fn packet_frames(&self, packet: &[u8]) -> Option { + header_frames(packet, u64::from(self.config.frame_length)).map(|n| n as usize) + } + + /// Decode one packet into one `Vec` per channel, in ALAC channel order. Channels the packet + /// has no element for are silent, as in the reference decoder. + /// + /// After the last channel the packet must hold a whole element tag (the end tag, after any + /// fill and data stream elements): a packet cut short after its samples is an error. Other + /// elements there end the packet, as in the reference decoder. + pub fn decode(&mut self, packet: &[u8]) -> Result>> { + let channels = self.channels(); + let mut out: Vec> = vec![Vec::new(); channels]; + let mut r = BitReader::new(packet); + let mut num_samples = self.config.frame_length as usize; + // the sample count of the elements decoded so far + let mut frames: Option = None; + let mut index = 0; + // whether the packet ended (or stopped as the reference stops) before the last channel + let mut stopped = false; + while index < channels { + if r.position() / 8 >= packet.len() { + return Err(Error::Eof); + } + match r.read(3)? { + tag @ (ID_SCE | ID_LFE | ID_CPE) => { + let pair = tag == ID_CPE; + if pair && index + 2 > channels { + // a pair past the channel count: the reference stops here + stopped = true; + break; + } + let (a, b) = self.element(&mut r, pair, &mut num_samples)?; + if frames.is_some_and(|f| f != a.len()) { + return Err(Error::Bitstream("the elements of a frame differ in length")); + } + frames = Some(a.len()); + out[index] = a; + index += 1; + if let Some(b) = b { + out[index] = b; + index += 1; + } + } + ID_CCE | ID_PCE => return Err(Error::Unsupported("coupling channel or program config element")), + ID_DSE => data_stream_element(&mut r)?, + ID_FIL => fill_element(&mut r)?, + ID_END => { + r.byte_align(); + stopped = true; + break; + } + _ => return Err(Error::Bitstream("element tag")), + } + } + if !stopped { + trailer(&mut r)?; + } + let frames = frames.unwrap_or(0); + for ch in out.iter_mut().filter(|c| c.len() != frames) { + *ch = vec![0; frames]; + } + Ok(out) + } + + /// One SCE/LFE (`pair` false) or CPE element after its tag: the channel's (or both channels') + /// samples. A partial-frame header changes `num_samples` for this and later elements. + fn element(&self, r: &mut BitReader, pair: bool, num_samples: &mut usize) -> Result<(Vec, Option>)> { + let cfg = &self.config; + let bit_depth = u32::from(cfg.bit_depth); + let _instance = r.read(4)?; + if r.read(12)? != 0 { + return Err(Error::Bitstream("unused element header bits are set")); + } + let header = r.read(4)?; + let partial = header >> 3 != 0; + let mut bytes_shifted = (header >> 1) & 3; + if bytes_shifted == 3 { + return Err(Error::Bitstream("three shifted-off bytes")); + } + let escape = header & 1 != 0; + if partial { + let n = (r.read(16)? << 16) | r.read(16)?; + if n > cfg.frame_length { + return Err(Error::Bitstream("a partial frame longer than the frame length")); + } + *num_samples = n as usize; + } + let n = *num_samples; + let nch = if pair { 2 } else { 1 }; + // the prediction (or escaped sample) width + let chan_bits = + |bits: i64| u32::try_from(bits).ok().filter(|b| (1..=32).contains(b)).ok_or(Error::Bitstream("sample width outside 1 to 32 bits")); + let mut mixed = [vec![0i32; n], if pair { vec![0i32; n] } else { Vec::new() }]; + let mut shift_buf: Vec = Vec::new(); + let mut mix = Mix { bits: 0, res: 0 }; + if !escape { + mix = Mix { bits: r.read(8)?, res: i32::from(r.read(8)? as u8 as i8) }; + let mut params = Vec::with_capacity(nch); + for _ in 0..nch { + let a = r.read(8)?; + let b = r.read(8)?; + let mut p = ChannelParams { mode: a >> 4, den_shift: a & 0xF, pb_factor: b >> 5, coefs: [0; 32], num_coefs: (b & 0x1F) as usize }; + for c in &mut p.coefs[..p.num_coefs] { + *c = r.read(16)? as u16 as i16; + } + params.push(p); + } + let bits = chan_bits(i64::from(bit_depth) - 8 * i64::from(bytes_shifted) + i64::from(pair))?; + // the shifted-off low bytes come before the residuals; read them after + let shift_at = r.position(); + if bytes_shifted != 0 { + r.skip((bytes_shifted as usize * 8).saturating_mul(nch).saturating_mul(n))?; + } + let mut pred = vec![0i32; n]; + for (p, out) in params.iter_mut().zip(mixed.iter_mut()) { + let pb = u32::from(cfg.pb) * p.pb_factor / 4; + ag::decompress(AgParams::new(u32::from(cfg.mb), pb, u32::from(cfg.kb)), r, &mut pred, bits)?; + if p.mode != 0 { + unpc_first_order_in_place(&mut pred, n, bits); + } + unpc_block(&pred, out, n, &mut p.coefs, p.num_coefs, bits, p.den_shift); + } + if bytes_shifted != 0 { + let mut sr = *r; + sr.set_position(shift_at)?; + shift_buf = Vec::with_capacity(n * nch); + for _ in 0..n * nch { + shift_buf.push(sr.read(bytes_shifted * 8)? as u16); + } + } + } else { + // uncompressed: a pair's samples are always full width + let bits = chan_bits(if pair { i64::from(bit_depth) } else { i64::from(bit_depth) - 8 * i64::from(bytes_shifted) })?; + for i in 0..n { + for ch in mixed.iter_mut().take(nch) { + ch[i] = r.read_signed(bits)?; + } + } + bytes_shifted = 0; + } + let [u, v] = mixed; + if pair { + let (mut l, mut rr) = (vec![0i32; n], vec![0i32; n]); + unmix((&u, &v), (&mut l, &mut rr), mix, &shift_buf, bytes_shifted, bit_depth); + Ok((l, Some(rr))) + } else { + let mut out = vec![0i32; n]; + unshift(&u, &shift_buf, &mut out, n, bytes_shifted, bit_depth); + Ok((out, None)) + } + } +} + +/// What follows the last channel: fill and data stream elements, then the end tag. The +/// reference decoder stops after the last channel; this needs the next tag to be whole, so a +/// packet missing its last byte is found, and like the reference ignores any other element. +fn trailer(r: &mut BitReader) -> Result<()> { + loop { + match r.read(3)? { + ID_DSE => data_stream_element(r)?, + ID_FIL => fill_element(r)?, + _ => return Ok(()), + } + } +} + +/// Skip a fill element (`FillElement`): a 4-bit count, 15 meaning 14 plus an 8-bit count. +fn fill_element(r: &mut BitReader) -> Result<()> { + let mut count = r.read(4)? as usize; + if count == 15 { + count = (count + r.read(8)? as usize).saturating_sub(1); + } + r.skip(count * 8) +} + +/// Skip a data stream element (`DataStreamElement`). +fn data_stream_element(r: &mut BitReader) -> Result<()> { + let _instance = r.read(4)?; + let align = r.read(1)? != 0; + let mut count = r.read(8)? as usize; + if count == 255 { + count += r.read(8)? as usize; + } + if align { + r.byte_align(); + } + r.skip(count * 8) +} + +/// [`Decoder::packet_frames`] for a stream of `frame_length` frames a packet. +pub(crate) fn header_frames(packet: &[u8], frame_length: u64) -> Option { + let mut r = BitReader::new(packet); + if !matches!(r.read(3).ok()?, ID_SCE | ID_LFE | ID_CPE) { + return None; + } + let _instance_and_unused = r.read(16).ok()?; + let flags = r.read(4).ok()?; + let frames = if flags & 0b1000 != 0 { u64::from(r.read(32).ok()?) } else { frame_length }; + (frames <= frame_length).then_some(frames) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn stereo16() -> Vec { + SpecificConfig::new(44_100, 2, 16, 4096).to_cookie() + } + + #[test] + fn cookies_out_of_range_are_refused() { + let ok = SpecificConfig::new(44_100, 2, 16, 4096); + for bad in [ + SpecificConfig { compatible_version: 1, ..ok }, + SpecificConfig { bit_depth: 0, ..ok }, + SpecificConfig { bit_depth: 33, ..ok }, + SpecificConfig { channels: 0, ..ok }, + SpecificConfig { channels: 9, ..ok }, + SpecificConfig { frame_length: 0, ..ok }, + SpecificConfig { frame_length: MAX_FRAME_LENGTH + 1, ..ok }, + ] { + assert!(Decoder::from_config(bad).is_err(), "{bad:?}"); + } + } + + #[test] + fn an_escaped_stereo_frame() { + // CPE, instance 0, 12 zero bits, flags: partial + escape, 2 frames, then L R L R. + let mut w = super::super::bits::BitWriter::new(); + use super::super::bits::BitSink; + w.put(ID_CPE, 3); + w.put(0, 4); + w.put(0, 12); + w.put(0b1001, 4); + w.put(2, 32); + for s in [1i32, -1, 32767, -32768] { + w.put(s as u32, 16); + } + w.put(ID_END, 3); + let mut dec = Decoder::new(&stereo16()).unwrap(); + assert_eq!(dec.decode(&w.into_bytes()).unwrap(), vec![vec![1, 32767], vec![-1, -32768]]); + } + + /// An escaped mono packet that lost its last byte keeps every sample but not its end tag. + #[test] + fn a_packet_without_its_end_tag_is_an_error() { + use super::super::bits::{BitSink, BitWriter}; + let mut w = BitWriter::new(); + for (value, bits) in [(ID_SCE, 3), (0, 4), (0, 12), (0b1001, 4), (3, 32)] { + w.put(value, bits); + } + for s in [5u32, 6, 7] { + w.put(s, 16); + } + w.put(ID_END, 3); + let packet = w.into_bytes(); + let mut dec = Decoder::new(&SpecificConfig::new(44_100, 1, 16, 4096).to_cookie()).unwrap(); + assert_eq!(dec.decode(&packet).unwrap(), vec![vec![5, 6, 7]]); + assert_eq!(dec.decode(&packet[..packet.len() - 1]), Err(Error::Eof)); + // a fill element before the end tag is skipped + let mut w = BitWriter::new(); + for (value, bits) in [(ID_SCE, 3), (0, 4), (0, 12), (0b1001, 4), (1, 32), (9, 16), (ID_FIL, 3), (1, 4), (0xA5, 8), (ID_END, 3)] { + w.put(value, bits); + } + assert_eq!(dec.decode(&w.into_bytes()).unwrap(), vec![vec![9]]); + } + + #[test] + fn the_frames_of_a_packet_come_from_its_header() { + use super::super::bits::{BitSink, BitWriter}; + let dec = Decoder::new(&SpecificConfig::new(44_100, 2, 16, 4096).to_cookie()).unwrap(); + let header = |flags: u32, frames: Option| { + let mut w = BitWriter::new(); + for (value, bits) in [(ID_CPE, 3), (0, 4), (0, 12), (flags, 4)] { + w.put(value, bits); + } + if let Some(n) = frames { + w.put(n, 32); + } + w.into_bytes() + }; + assert_eq!(dec.packet_frames(&header(0b1001, Some(300))), Some(300)); + assert_eq!(dec.packet_frames(&header(0b0001, None)), Some(4096)); + assert_eq!(dec.packet_frames(&header(0b1001, Some(5000))), None); + assert_eq!(dec.packet_frames(&header(0b1001, None)), None); + assert_eq!(dec.packet_frames(&[0xE0]), None); // ID_END first + } + + #[test] + fn hostile_packets_fail_or_decode_without_panicking() { + let mut dec = Decoder::new(&SpecificConfig::new(48_000, 8, 24, 4096).to_cookie()).unwrap(); + let mut seed = 0x1234_5678u32; + for len in 0..400 { + let packet: Vec = (0..len) + .map(|_| { + seed ^= seed << 13; + seed ^= seed >> 17; + seed ^= seed << 5; + seed as u8 + }) + .collect(); + if let Ok(chans) = dec.decode(&packet) { + assert_eq!(chans.len(), 8); + assert!(chans.iter().all(|c| c.len() == chans[0].len() && c.len() <= 4096)); + } + } + assert_eq!(dec.decode(&[]), Err(Error::Eof)); + } +} diff --git a/crates/audio/src/alac/dp.rs b/crates/audio/src/alac/dp.rs new file mode 100644 index 000000000..3e37e3021 --- /dev/null +++ b/crates/audio/src/alac/dp.rs @@ -0,0 +1,196 @@ +// Ported from Apple's ALAC reference implementation (dplib.h, dp_dec.c and dp_enc.c, macosforge/alac@c38887c, +// https://github.com/macosforge/alac). Copyright (c) 2011 Apple Inc. +// Licensed under the Apache License, Version 2.0 (LICENSE-APACHE at the repository root). +// Modified: translated to Rust and changed as the `alac` module docs describe. +// SPDX-License-Identifier: Apache-2.0 + +//! The adaptive FIR predictor, from Apple's `dplib.h`, `dp_dec.c` and `dp_enc.c`. +//! +//! The reference has unrolled copies for 4 and 8 taps; they compute the same values as its +//! general loop, which is the one ported. Arithmetic wraps as the C code does on its targets. +//! As in the reference, the warm-up (the first `numactive` samples, first differences) runs to +//! `numactive` even in a shorter block, so the encoder's trial runs leave the buffers as the +//! reference leaves them; here it stops at the end of the buffers. + +pub(crate) const DENSHIFT_DEFAULT: u32 = 9; +const AINIT: i32 = 38; +const BINIT: i32 = -29; +const CINIT: i32 = -2; +/// Coefficients of the first-difference shortcut (`numactive == 31`). +pub(crate) const FIRST_ORDER: usize = 31; + +/// `init_coefs`. +pub(crate) fn init_coefs(coefs: &mut [i16], denshift: u32) { + let den = 1i32 << denshift; + coefs.fill(0); + for (c, init) in coefs.iter_mut().zip([AINIT, BINIT, CINIT]) { + *c = ((init * den) >> 4) as i16; + } +} + +/// `sign_of_int`: -1, 0 or 1. +fn sign(i: i32) -> i32 { + i.signum() +} + +/// `(v << shift) >> shift`: `v` wrapped to `chanbits` bits, sign-extended. +fn wrap(v: i32, chanshift: u32) -> i32 { + (v << chanshift) >> chanshift +} + +/// 1 << (denshift - 1), 0 when `denshift` is 0 (a shift the reference leaves undefined). +fn half(denshift: u32) -> i32 { + if denshift == 0 { 0 } else { 1i32 << (denshift - 1).min(30) } +} + +/// The sign-LMS coefficient update after a residual of `del` (`top`, `prev`: the base sample and +/// the samples before the current one, nearest first). +fn adapt(coefs: &mut [i16], del: i32, top: i32, prev: impl Fn(usize) -> i32, denshift: u32) { + let numactive = coefs.len(); + let mut del0 = del; + let shift = denshift.min(31); + if del > 0 { + for k in (0..numactive).rev() { + let dd = top.wrapping_sub(prev(k)); + let sgn = sign(dd); + coefs[k] = coefs[k].wrapping_sub(sgn as i16); + del0 = del0.wrapping_sub(((numactive - k) as i32).wrapping_mul(sgn.wrapping_mul(dd) >> shift)); + if del0 <= 0 { + break; + } + } + } else if del < 0 { + for k in (0..numactive).rev() { + let dd = top.wrapping_sub(prev(k)); + let sgn = sign(dd); + coefs[k] = coefs[k].wrapping_add(sgn as i16); + del0 = del0.wrapping_sub(((numactive - k) as i32).wrapping_mul(sgn.wrapping_neg().wrapping_mul(dd) >> shift)); + if del0 >= 0 { + break; + } + } + } +} + +/// `pc_block`: residuals `pc` of `input` (both at least `num` long) with `numactive` +/// coefficients ([`FIRST_ORDER`]: first differences, 0: a copy). +pub(crate) fn pc_block(input: &[i32], pc: &mut [i32], num: usize, coefs: &mut [i16], numactive: usize, chanbits: u32, denshift: u32) { + let cap = input.len().min(pc.len()); + if cap == 0 { + return; + } + let num = num.min(cap); + let chanshift = 32u32.saturating_sub(chanbits).min(31); + pc[0] = input[0]; + if numactive == 0 { + if num > 1 { + pc[1..num].copy_from_slice(&input[1..num]); + } + return; + } + let warm = if numactive == FIRST_ORDER { num } else { (numactive + 1).min(cap) }; + for j in 1..warm { + pc[j] = wrap(input[j].wrapping_sub(input[j - 1]), chanshift); + } + if numactive == FIRST_ORDER { + return; + } + let active = numactive.min(coefs.len()); + let coefs = &mut coefs[..active]; + let numactive = coefs.len(); + let lim = numactive + 1; + let denhalf = half(denshift); + let shift = denshift.min(31); + for j in lim..num { + let top = input[j - lim]; + let prev = |k: usize| input[j - 1 - k]; + let mut sum: i32 = 0; + for (k, &c) in coefs.iter().enumerate() { + sum = sum.wrapping_sub(i32::from(c).wrapping_mul(top.wrapping_sub(prev(k)))); + } + let del = wrap(input[j].wrapping_sub(top).wrapping_sub(sum.wrapping_add(denhalf) >> shift), chanshift); + pc[j] = del; + adapt(coefs, del, top, prev, denshift); + } +} + +/// `unpc_block`: samples `out` from residuals `pc` (both at least `num` long), the inverse of +/// [`pc_block`] with the same coefficients. +pub(crate) fn unpc_block(pc: &[i32], out: &mut [i32], num: usize, coefs: &mut [i16], numactive: usize, chanbits: u32, denshift: u32) { + let cap = pc.len().min(out.len()); + if cap == 0 { + return; + } + let num = num.min(cap); + let chanshift = 32u32.saturating_sub(chanbits).min(31); + out[0] = pc[0]; + if numactive == 0 { + if num > 1 { + out[1..num].copy_from_slice(&pc[1..num]); + } + return; + } + let warm = if numactive == FIRST_ORDER { num } else { (numactive + 1).min(cap) }; + for j in 1..warm { + out[j] = wrap(pc[j].wrapping_add(out[j - 1]), chanshift); + } + if numactive == FIRST_ORDER { + return; + } + let active = numactive.min(coefs.len()); + let coefs = &mut coefs[..active]; + let numactive = coefs.len(); + let lim = numactive + 1; + let denhalf = half(denshift); + let shift = denshift.min(31); + for j in lim..num { + let top = out[j - lim]; + let mut sum: i32 = 0; + for (k, &c) in coefs.iter().enumerate() { + sum = sum.wrapping_add(i32::from(c).wrapping_mul(out[j - 1 - k].wrapping_sub(top))); + } + let del = pc[j]; + out[j] = wrap(del.wrapping_add(top).wrapping_add(sum.wrapping_add(denhalf) >> shift), chanshift); + let (done, _) = out.split_at(j); + adapt(coefs, del, top, |k| done[j - 1 - k], denshift); + } +} + +/// [`unpc_block`] in place for the first-difference shortcut (mode 1's first stage). +pub(crate) fn unpc_first_order_in_place(buf: &mut [i32], num: usize, chanbits: u32) { + let num = num.min(buf.len()); + let chanshift = 32u32.saturating_sub(chanbits).min(31); + for j in 1..num { + buf[j] = wrap(buf[j].wrapping_add(buf[j - 1]), chanshift); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_predictor_inverts() { + let input: Vec = (0..2000).map(|i: i32| ((i as f64 * 0.05).sin() * 20000.0) as i32 + (i * 31 % 17) - 8).collect(); + for numactive in [0, 1, 4, 8, 12, FIRST_ORDER] { + for num in [0, 1, 3, 9, 2000] { + let mut ce = [0i16; 32]; + init_coefs(&mut ce, DENSHIFT_DEFAULT); + let mut cd = ce; + let mut pc = vec![0; 2000]; + let mut out = vec![0; 2000]; + pc_block(&input, &mut pc, num, &mut ce, numactive, 16, DENSHIFT_DEFAULT); + unpc_block(&pc, &mut out, num, &mut cd, numactive, 16, DENSHIFT_DEFAULT); + assert_eq!(out[..num], input[..num], "numactive {numactive}, num {num}"); + assert_eq!(ce, cd); + } + } + } + + #[test] + fn coefficient_start_values() { + let mut c = [7i16; 16]; + init_coefs(&mut c, DENSHIFT_DEFAULT); + assert_eq!(&c[..4], &[1216, -928, -64, 0]); + } +} diff --git a/crates/audio/src/alac/encoder.rs b/crates/audio/src/alac/encoder.rs new file mode 100644 index 000000000..a589a9060 --- /dev/null +++ b/crates/audio/src/alac/encoder.rs @@ -0,0 +1,580 @@ +// Ported from Apple's ALAC reference implementation (ALACEncoder.cpp, macosforge/alac@c38887c, +// https://github.com/macosforge/alac). Copyright (c) 2011 Apple Inc. +// Licensed under the Apache License, Version 2.0 (LICENSE-APACHE at the repository root). +// Modified: translated to Rust and changed as the `alac` module docs describe. +// SPDX-License-Identifier: Apache-2.0 + +//! The packet encoder, from Apple's `ALACEncoder.cpp`. +//! +//! The search (stereo matrixing, 4 or 8 predictor coefficients) and the state kept across +//! packets (coefficients, the last matrixing, and the buffers the trial runs read stale parts of) +//! follow the reference, so for the same input this writes the same packets as it. + +use super::ag::{self, AgParams}; +use super::bits::{BitCounter, BitSink, BitWriter}; +use super::dp::{DENSHIFT_DEFAULT, init_coefs, pc_block}; +use super::matrix::{Mix, mix, shift_mono}; +use super::{Error, ID_CPE, ID_END, ID_LFE, MAX_CHANNELS, MAX_FRAME_LENGTH, Result, SpecificConfig, element_map}; + +const MAX_COEFS: usize = 16; +const MAX_SEARCHES: usize = 16; +const DEFAULT_MIX_BITS: u32 = 2; +const MAX_RES: i32 = 4; +const DEFAULT_NUM_UV: usize = 8; +const MIN_UV: usize = 4; +const MAX_UV: usize = 8; +/// The `pbFactor` every element is coded with (pb = 40 · 4 / 4). +const PB_FACTOR: u32 = 4; + +/// Encoder settings. +/// +/// `#[non_exhaustive]`: build it with [`EncoderConfig::new`] and the `with_*` methods. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EncoderConfig { + /// Sample rate in Hz, recorded in the cookie. + pub sample_rate: u32, + /// 1 to 8 channels. + pub channels: u8, + /// 16, 20, 24 or 32. + pub bit_depth: u8, + /// Frames per packet (default [`DEFAULT_FRAME_LENGTH`](super::DEFAULT_FRAME_LENGTH)); only the + /// last packet may be shorter. + pub frame_length: u32, + /// The reference's fast mode: a stereo stream skips the matrixing and coefficient search + /// (streams of other channel counts are encoded as without it, as in the reference). + pub fast: bool, +} + +impl EncoderConfig { + /// `channels` channels of `bit_depth`-bit samples at `sample_rate`, 4096 frames per packet. + pub fn new(sample_rate: u32, channels: u8, bit_depth: u8) -> Self { + Self { sample_rate, channels, bit_depth, frame_length: super::DEFAULT_FRAME_LENGTH, fast: false } + } + + /// Sets `frame_length`. + pub fn with_frame_length(mut self, frame_length: u32) -> Self { + self.frame_length = frame_length; + self + } + + /// Sets `fast`. + pub fn with_fast(mut self, fast: bool) -> Self { + self.fast = fast; + self + } +} + +/// Per-channel predictor coefficients, one set per search width. +type SearchCoefs = [[i16; MAX_COEFS]; MAX_SEARCHES]; + +/// Encodes planar `i32` (see the [module docs](super)) into ALAC packets. +pub struct Encoder { + cfg: EncoderConfig, + bit_depth: u32, + last_mix_res: [i32; MAX_CHANNELS as usize], + mix_u: Vec, + mix_v: Vec, + pred_u: Vec, + pred_v: Vec, + shift_uv: Vec, + coefs_u: Vec, + coefs_v: Vec, + total_bytes: u64, + total_frames: u64, + max_frame_bytes: u32, +} + +impl std::fmt::Debug for Encoder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Encoder").field("cfg", &self.cfg).finish_non_exhaustive() + } +} + +/// The bytes of each sample the reference passes through uncompressed below the predicted part. +fn bytes_shifted(bit_depth: u32) -> u32 { + match bit_depth { + 32 => 2, + 24.. => 1, + _ => 0, + } +} + +impl Encoder { + /// An encoder for `cfg`. + pub fn new(cfg: EncoderConfig) -> Result { + if ![16, 20, 24, 32].contains(&cfg.bit_depth) { + return Err(Error::InvalidInput("bit depth other than 16, 20, 24 or 32")); + } + if cfg.channels == 0 || cfg.channels > MAX_CHANNELS { + return Err(Error::InvalidInput("channel count outside 1 to 8")); + } + if cfg.frame_length == 0 || cfg.frame_length > MAX_FRAME_LENGTH { + return Err(Error::InvalidInput("frame length outside 1 to 2^20")); + } + let n = cfg.frame_length as usize; + let mut coefs: SearchCoefs = [[0; MAX_COEFS]; MAX_SEARCHES]; + for c in &mut coefs { + init_coefs(c, DENSHIFT_DEFAULT); + } + let channels = usize::from(cfg.channels); + Ok(Self { + cfg, + bit_depth: u32::from(cfg.bit_depth), + last_mix_res: [0; MAX_CHANNELS as usize], + mix_u: vec![0; n], + mix_v: vec![0; n], + pred_u: vec![0; n], + pred_v: vec![0; n], + shift_uv: vec![0; 2 * n], + coefs_u: vec![coefs; channels], + coefs_v: vec![coefs; channels], + total_bytes: 0, + total_frames: 0, + max_frame_bytes: 0, + }) + } + + /// The settings. + pub fn encoder_config(&self) -> &EncoderConfig { + &self.cfg + } + + /// The stream configuration, with the largest packet and the average bit rate of the packets + /// encoded so far. + pub fn config(&self) -> SpecificConfig { + let mut c = SpecificConfig::new(self.cfg.sample_rate, self.cfg.channels, self.cfg.bit_depth, self.cfg.frame_length); + c.max_frame_bytes = self.max_frame_bytes; + if self.total_frames > 0 { + let rate = u128::from(self.total_bytes) * 8 * u128::from(self.cfg.sample_rate) / u128::from(self.total_frames); + c.avg_bit_rate = u32::try_from(rate).unwrap_or(u32::MAX); + } + c + } + + /// The configuration as MP4's `alac` box and Matroska's CodecPrivate hold it: + /// [`Self::config`] as [`SpecificConfig::to_bytes`] writes it (for CAF, use + /// [`SpecificConfig::to_cookie`]). Write it after the last packet for the statistics to cover + /// the whole stream. + pub fn cookie(&self) -> Vec { + self.config().to_bytes() + } + + /// Encode one packet from one slice per channel, in ALAC channel order (for WAV-order audio, + /// pass WAV channel [`alac_order`](super::alac_order)`(n)[j]` as channel `j`), each holding the same + /// number of samples: 1 to the frame length (fewer only for the last packet). Samples must fit + /// the bit depth. + pub fn encode(&mut self, channels: &[&[i32]]) -> Result> { + if channels.len() != usize::from(self.cfg.channels) { + return Err(Error::InvalidInput("channel count differs from the configuration")); + } + let n = channels.first().map_or(0, |c| c.len()); + if n == 0 || n > self.cfg.frame_length as usize { + return Err(Error::InvalidInput("packet length outside 1 to the frame length")); + } + if channels.iter().any(|c| c.len() != n) { + return Err(Error::InvalidInput("channels differ in length")); + } + if self.bit_depth < 32 { + let max = (1i32 << (self.bit_depth - 1)) - 1; + if channels.iter().flat_map(|c| c.iter()).any(|&s| s > max || s < -max - 1) { + return Err(Error::InvalidInput("a sample outside the bit depth")); + } + } + let mut w = BitWriter::new(); + let mut index = 0; + let (mut sce_tag, mut cpe_tag, mut lfe_tag) = (0u32, 0u32, 0u32); + for &element in element_map(channels.len()) { + w.put(element, 3); + match element { + ID_CPE => { + w.put(cpe_tag, 4); + cpe_tag += 1; + let (l, r) = (channels[index], channels[index + 1]); + // as in the reference, fast mode applies to stereo streams only + if self.cfg.fast && channels.len() == 2 { + self.encode_stereo_fast(&mut w, index, l, r)?; + } else { + self.encode_stereo(&mut w, index, l, r)?; + } + index += 2; + } + _ => { + let tag = if element == ID_LFE { &mut lfe_tag } else { &mut sce_tag }; + w.put(*tag, 4); + *tag += 1; + self.encode_mono(&mut w, index, channels[index])?; + index += 1; + } + } + } + w.put(ID_END, 3); + let bytes = w.into_bytes(); + self.total_bytes += bytes.len() as u64; + self.total_frames += n as u64; + self.max_frame_bytes = self.max_frame_bytes.max(u32::try_from(bytes.len()).unwrap_or(u32::MAX)); + Ok(bytes) + } + + fn partial(&self, n: usize) -> bool { + n != self.cfg.frame_length as usize + } + + /// The element header: 12 zero bits, the flags, the sample count of a partial frame. + fn header(&self, w: &mut BitWriter, n: usize, flags: u32) { + let partial = self.partial(n); + w.put(0, 12); + w.put((u32::from(partial) << 3) | flags, 4); + if partial { + w.put(n as u32, 32); + } + } + + /// `EncodeStereo`: a channel pair with the matrixing and coefficient search. + fn encode_stereo(&mut self, w: &mut BitWriter, ch: usize, l: &[i32], r: &[i32]) -> Result<()> { + let start = w.bit_len(); + let n = l.len(); + let bs = bytes_shifted(self.bit_depth); + let chan_bits = self.bit_depth - bs * 8 + 1; + let partial = self.partial(n); + let params = AgParams::new(ag::MB0, PB_FACTOR * ag::PB0 / 4, ag::KB0); + + // matrixing search over an eighth of the samples + let mut dilate = 8; + let mut min_bits1 = 1u64 << 31; + let mut best_res = self.last_mix_res[ch]; + for mix_res in 0..=MAX_RES { + let nd = n / dilate; + let m = Mix { bits: DEFAULT_MIX_BITS, res: mix_res }; + mix((&l[..nd], &r[..nd]), (&mut self.mix_u, &mut self.mix_v), m, &mut self.shift_uv, bs); + let cu = &mut self.coefs_u[ch][DEFAULT_NUM_UV - 1]; + pc_block(&self.mix_u, &mut self.pred_u, nd, cu, DEFAULT_NUM_UV, chan_bits, DENSHIFT_DEFAULT); + let cv = &mut self.coefs_v[ch][DEFAULT_NUM_UV - 1]; + pc_block(&self.mix_v, &mut self.pred_v, nd, cv, DEFAULT_NUM_UV, chan_bits, DENSHIFT_DEFAULT); + let bits = count(params, &self.pred_u[..nd], chan_bits)? + count(params, &self.pred_v[..nd], chan_bits)?; + if bits < min_bits1 { + min_bits1 = bits; + best_res = mix_res; + } + } + self.last_mix_res[ch] = best_res; + let m = Mix { bits: DEFAULT_MIX_BITS, res: best_res }; + mix((l, r), (&mut self.mix_u, &mut self.mix_v), m, &mut self.shift_uv, bs); + + // predictor search: 4 or 8 coefficients, converged on a 32nd of the samples + let (mut num_u, mut num_v) = (MIN_UV, MIN_UV); + let (mut min_bits1, mut min_bits2) = (1u64 << 31, 1u64 << 31); + for num_uv in (MIN_UV..=MAX_UV).step_by(4) { + dilate = 32; + for _ in 0..8 { + let cu = &mut self.coefs_u[ch][num_uv - 1]; + pc_block(&self.mix_u, &mut self.pred_u, n / dilate, cu, num_uv, chan_bits, DENSHIFT_DEFAULT); + let cv = &mut self.coefs_v[ch][num_uv - 1]; + pc_block(&self.mix_v, &mut self.pred_v, n / dilate, cv, num_uv, chan_bits, DENSHIFT_DEFAULT); + } + dilate = 8; + let header = 16 * num_uv as u64; + let bits1 = count(params, &self.pred_u[..n / dilate], chan_bits)? * dilate as u64 + header; + if bits1 < min_bits1 { + min_bits1 = bits1; + num_u = num_uv; + } + let bits2 = count(params, &self.pred_v[..n / dilate], chan_bits)? * dilate as u64 + header; + if bits2 < min_bits2 { + min_bits2 = bits2; + num_v = num_uv; + } + } + + // escape when the estimate is no smaller than the samples themselves + let partial_bits = if partial { 32 } else { 0 }; + let mut min_bits = min_bits1 + min_bits2 + 64 + partial_bits; + if bs != 0 { + min_bits += n as u64 * u64::from(bs * 8) * 2; + } + let escape_bits = n as u64 * u64::from(self.bit_depth) * 2 + partial_bits + 16; + let mut escape = min_bits >= escape_bits; + if !escape { + self.header(w, n, bs << 1); + w.put(DEFAULT_MIX_BITS, 8); + w.put(best_res as u32, 8); + for (coefs, num) in [(&self.coefs_u[ch][num_u - 1], num_u), (&self.coefs_v[ch][num_v - 1], num_v)] { + w.put(DENSHIFT_DEFAULT, 8); // mode 0 + w.put((PB_FACTOR << 5) | num as u32, 8); + for &c in &coefs[..num] { + w.put(u32::from(c as u16), 16); + } + } + if bs != 0 { + let shift = bs * 8; + for pair in self.shift_uv[..2 * n].as_chunks::<2>().0 { + w.put((u32::from(pair[0]) << shift) | u32::from(pair[1]), shift * 2); + } + } + let cu = &mut self.coefs_u[ch][num_u - 1]; + pc_block(&self.mix_u, &mut self.pred_u, n, cu, num_u, chan_bits, DENSHIFT_DEFAULT); + ag::compress(params, &self.pred_u[..n], w, chan_bits)?; + let cv = &mut self.coefs_v[ch][num_v - 1]; + pc_block(&self.mix_v, &mut self.pred_v, n, cv, num_v, chan_bits, DENSHIFT_DEFAULT); + ag::compress(params, &self.pred_v[..n], w, chan_bits)?; + if (w.bit_len() - start) as u64 >= escape_bits { + w.truncate(start); + escape = true; + } + } + if escape { + self.encode_stereo_escape(w, l, r); + } + Ok(()) + } + + /// `EncodeStereoFast`: a channel pair without matrixing or search. + fn encode_stereo_fast(&mut self, w: &mut BitWriter, ch: usize, l: &[i32], r: &[i32]) -> Result<()> { + let start = w.bit_len(); + let n = l.len(); + let bs = bytes_shifted(self.bit_depth); + let chan_bits = self.bit_depth - bs * 8 + 1; + let partial = self.partial(n); + let params = AgParams::new(ag::MB0, PB_FACTOR * ag::PB0 / 4, ag::KB0); + let num = DEFAULT_NUM_UV; + let m = Mix { bits: DEFAULT_MIX_BITS, res: 0 }; + mix((l, r), (&mut self.mix_u, &mut self.mix_v), m, &mut self.shift_uv, bs); + // written speculatively, rewound for an escape + self.header(w, n, bs << 1); + w.put(DEFAULT_MIX_BITS, 8); + w.put(0, 8); + for coefs in [&self.coefs_u[ch][num - 1], &self.coefs_v[ch][num - 1]] { + w.put(DENSHIFT_DEFAULT, 8); + w.put((PB_FACTOR << 5) | num as u32, 8); + for &c in &coefs[..num] { + w.put(u32::from(c as u16), 16); + } + } + if bs != 0 { + let shift = bs * 8; + for pair in self.shift_uv[..2 * n].as_chunks::<2>().0 { + w.put((u32::from(pair[0]) << shift) | u32::from(pair[1]), shift * 2); + } + } + let cu = &mut self.coefs_u[ch][num - 1]; + pc_block(&self.mix_u, &mut self.pred_u, n, cu, num, chan_bits, DENSHIFT_DEFAULT); + let before = w.bit_len(); + ag::compress(params, &self.pred_u[..n], w, chan_bits)?; + let bits1 = (w.bit_len() - before) as u64; + let cv = &mut self.coefs_v[ch][num - 1]; + pc_block(&self.mix_v, &mut self.pred_v, n, cv, num, chan_bits, DENSHIFT_DEFAULT); + let before = w.bit_len(); + ag::compress(params, &self.pred_v[..n], w, chan_bits)?; + let bits2 = (w.bit_len() - before) as u64; + + let partial_bits = if partial { 32 } else { 0 }; + let mut min_bits = bits1 + bits2 + 2 * 16 * num as u64 + 64 + partial_bits; + if bs != 0 { + min_bits += n as u64 * u64::from(bs * 8) * 2; + } + let escape_bits = n as u64 * u64::from(self.bit_depth) * 2 + partial_bits + 16; + if min_bits >= escape_bits || (w.bit_len() - start) as u64 >= escape_bits { + w.truncate(start); + self.encode_stereo_escape(w, l, r); + } + Ok(()) + } + + /// `EncodeStereoEscape`: a channel pair uncompressed, L R interleaved. + fn encode_stereo_escape(&mut self, w: &mut BitWriter, l: &[i32], r: &[i32]) { + let n = l.len(); + self.header(w, n, 1); + for (&a, &b) in l.iter().zip(r) { + w.put(a as u32, self.bit_depth); + w.put(b as u32, self.bit_depth); + } + // the reference de-interleaves 20- and 24-bit input through the mix buffers + if matches!(self.bit_depth, 20 | 24) { + self.mix_u[..n].copy_from_slice(l); + self.mix_v[..n].copy_from_slice(r); + } + } + + /// `EncodeMono`: one channel (SCE or LFE). + fn encode_mono(&mut self, w: &mut BitWriter, ch: usize, input: &[i32]) -> Result<()> { + let start = w.bit_len(); + let n = input.len(); + let bs = bytes_shifted(self.bit_depth); + let shift = bs * 8; + let chan_bits = self.bit_depth - shift; + let partial = self.partial(n); + let params = AgParams::new(ag::MB0, PB_FACTOR * ag::PB0 / 4, ag::KB0); + shift_mono(input, &mut self.mix_u, &mut self.shift_uv, n, bs); + + let mut min_bits = 1u64 << 31; + let mut best_u = MIN_UV; + for num_u in (MIN_UV..=MAX_UV).step_by(4) { + let coefs = &mut self.coefs_u[ch][num_u - 1]; + for _ in 0..7 { + pc_block(&self.mix_u, &mut self.pred_u, n / 32, coefs, num_u, chan_bits, DENSHIFT_DEFAULT); + } + pc_block(&self.mix_u, &mut self.pred_u, n / 8, coefs, num_u, chan_bits, DENSHIFT_DEFAULT); + let bits = 8 * count(params, &self.pred_u[..n / 8], chan_bits)? + 16 * num_u as u64; + if bits < min_bits { + best_u = num_u; + min_bits = bits; + } + } + let partial_bits = if partial { 32 } else { 0 }; + min_bits += 32 + partial_bits; + if bs != 0 { + min_bits += n as u64 * u64::from(shift); + } + let escape_bits = n as u64 * u64::from(self.bit_depth) + partial_bits + 16; + let mut escape = min_bits >= escape_bits; + if !escape { + self.header(w, n, bs << 1); + w.put(0, 16); // mixBits = mixRes = 0 + w.put(DENSHIFT_DEFAULT, 8); // mode 0 + w.put((PB_FACTOR << 5) | best_u as u32, 8); + for &c in &self.coefs_u[ch][best_u - 1][..best_u] { + w.put(u32::from(c as u16), 16); + } + if bs != 0 { + for &s in &self.shift_uv[..n] { + w.put(u32::from(s), shift); + } + } + let coefs = &mut self.coefs_u[ch][best_u - 1]; + pc_block(&self.mix_u, &mut self.pred_u, n, coefs, best_u, chan_bits, DENSHIFT_DEFAULT); + ag::compress(AgParams::standard(), &self.pred_u[..n], w, chan_bits)?; + if (w.bit_len() - start) as u64 >= escape_bits { + w.truncate(start); + escape = true; + } + } + if escape { + self.header(w, n, 1); + for &s in input { + w.put(s as u32, self.bit_depth); + } + // the reference converts 20- and 24-bit input through the mix buffer + if matches!(self.bit_depth, 20 | 24) { + self.mix_u[..n].copy_from_slice(input); + } + } + Ok(()) + } +} + +/// Bits the residuals `pc` code to. +fn count(params: AgParams, pc: &[i32], bits: u32) -> Result { + let mut c = BitCounter::default(); + ag::compress(params, pc, &mut c, bits)?; + Ok(c.bit_len() as u64) +} + +// The round trips need the decoder. +#[cfg(test)] +#[cfg(feature = "alac-decode")] +mod tests { + use super::super::Decoder; + use super::*; + + fn signal(n: usize, ch: usize, bits: u32) -> Vec> { + let amp = ((1i64 << (bits - 1)) - 1) as f64 * 0.8; + (0..ch) + .map(|c| { + (0..n) + .map(|i| { + let t = i as f64 / 44_100.0; + let x = + (t * (220.0 + 110.0 * c as f64) * std::f64::consts::TAU).sin() * 0.7 + ((i * 7919 + c * 31) % 101) as f64 / 400.0 - 0.125; + (x * amp) as i32 + }) + .collect() + }) + .collect() + } + + fn roundtrip(cfg: EncoderConfig, frames: usize) -> usize { + let input = signal(frames, usize::from(cfg.channels), u32::from(cfg.bit_depth)); + let mut enc = Encoder::new(cfg).unwrap(); + let mut packets = Vec::new(); + let fl = cfg.frame_length as usize; + for start in (0..frames).step_by(fl) { + let end = (start + fl).min(frames); + let chunk: Vec<&[i32]> = input.iter().map(|c| &c[start..end]).collect(); + packets.push(enc.encode(&chunk).unwrap()); + } + let mut dec = Decoder::new(&enc.cookie()).unwrap(); + let mut out = vec![Vec::new(); input.len()]; + for p in &packets { + for (o, c) in out.iter_mut().zip(dec.decode(p).unwrap()) { + o.extend(c); + } + } + assert_eq!(out, input, "{cfg:?}"); + assert_eq!(enc.config().max_frame_bytes as usize, packets.iter().map(Vec::len).max().unwrap()); + packets.iter().map(Vec::len).sum() + } + + #[test] + fn every_depth_and_channel_count_roundtrips() { + for bits in [16, 20, 24, 32] { + for ch in 1..=8 { + for fast in [false, true] { + let cfg = EncoderConfig::new(44_100, ch, bits).with_fast(fast).with_frame_length(1024); + roundtrip(cfg, 3000); + } + } + } + } + + #[test] + fn short_and_tiny_packets_roundtrip() { + for n in [1, 2, 5, 9, 31, 33, 100] { + roundtrip(EncoderConfig::new(48_000, 2, 16).with_frame_length(4096), n); + roundtrip(EncoderConfig::new(48_000, 1, 24).with_frame_length(4096), n); + } + roundtrip(EncoderConfig::new(48_000, 2, 24).with_frame_length(3), 50); + } + + #[test] + fn compresses_tonal_audio_and_escapes_noise() { + // two tones, one per channel, at half scale: well under half the PCM size + let tones: Vec> = [440.0, 660.0] + .iter() + .map(|f| (0..44_100).map(|i| ((i as f64 * f / 44_100.0 * std::f64::consts::TAU).sin() * 16_000.0) as i32).collect()) + .collect(); + let mut enc = Encoder::new(EncoderConfig::new(44_100, 2, 16)).unwrap(); + let mut bytes = 0; + for start in (0..44_100).step_by(4096) { + let end = (start + 4096).min(44_100); + bytes += enc.encode(&[&tones[0][start..end], &tones[1][start..end]]).unwrap().len(); + } + assert!(bytes < 44_100 * 4 / 2, "{bytes} bytes"); + // white noise at full scale does not compress: escaped frames, a few bytes over raw + let mut seed = 1u32; + let noise: Vec = (0..8192) + .map(|_| { + seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); + (seed >> 16) as i16 as i32 + }) + .collect(); + let mut enc = Encoder::new(EncoderConfig::new(44_100, 1, 16)).unwrap(); + let p = enc.encode(&[&noise[..4096]]).unwrap(); + assert!(p.len() <= 4096 * 2 + 4, "{}", p.len()); + let mut dec = Decoder::new(&enc.cookie()).unwrap(); + assert_eq!(dec.decode(&p).unwrap(), vec![noise[..4096].to_vec()]); + } + + #[test] + fn bad_input_is_refused() { + assert!(Encoder::new(EncoderConfig::new(44_100, 2, 8)).is_err()); + assert!(Encoder::new(EncoderConfig::new(44_100, 9, 16)).is_err()); + assert!(Encoder::new(EncoderConfig::new(44_100, 2, 16).with_frame_length(0)).is_err()); + let mut enc = Encoder::new(EncoderConfig::new(44_100, 2, 16).with_frame_length(16)).unwrap(); + assert!(enc.encode(&[&[0; 4]]).is_err()); + assert!(enc.encode(&[&[0; 4], &[0; 5]]).is_err()); + assert!(enc.encode(&[&[0; 17], &[0; 17]]).is_err()); + assert!(enc.encode(&[&[], &[]]).is_err()); + assert!(enc.encode(&[&[40_000], &[0]]).is_err()); + assert!(enc.encode(&[&[-32_768], &[32_767]]).is_ok()); + } +} diff --git a/crates/audio/src/alac/matrix.rs b/crates/audio/src/alac/matrix.rs new file mode 100644 index 000000000..95dfc13c3 --- /dev/null +++ b/crates/audio/src/alac/matrix.rs @@ -0,0 +1,120 @@ +// Ported from Apple's ALAC reference implementation (matrixlib.h, matrix_dec.c and matrix_enc.c, macosforge/alac@c38887c, +// https://github.com/macosforge/alac). Copyright (c) 2011 Apple Inc. +// Licensed under the Apache License, Version 2.0 (LICENSE-APACHE at the repository root). +// Modified: translated to Rust and changed as the `alac` module docs describe. +// SPDX-License-Identifier: Apache-2.0 + +//! Stereo matrixing and the shifted-off low bytes, from Apple's `matrixlib.h`, `matrix_dec.c` and +//! `matrix_enc.c`. +//! +//! The reference has one routine per sample width (16, 20, 24, 32 bits) over interleaved buffers; +//! on right-aligned planar `i32` samples they reduce to the same arithmetic, ported once here. +//! +//! A generalised mid/side transform, lossless for any `mixres` and `mixbits` (m = 2^mixbits): +//! u = floor((r·L + (m − r)·R) / m), v = L − R; inverse L = u + v − floor(r·v / m), R = L − v. + +/// `x >> n` for any `n` (31 and above give the sign). +fn shr(x: i32, n: u32) -> i32 { + x >> n.min(31) +} + +/// Split off the low `shift` bits of `x` into the shift buffer value and return the rest. +fn split(x: i32, shift: u32) -> (i32, u16) { + if shift == 0 { (x, 0) } else { (x >> shift, (x as u32 & ((1u32 << shift) - 1)) as u16) } +} + +/// A channel pair's matrixing: `mixbits` (m = 2^mixbits) and `mixres` (r; 0 for none). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct Mix { + pub(crate) bits: u32, + pub(crate) res: i32, +} + +/// `mix16/20/24/32`: `u`, `v` from the samples of `l` and `r` (as many as the shortest of the +/// four holds), the low `bytes_shifted` bytes of each going to `shift_uv` (interleaved L, R). +pub(crate) fn mix((l, r): (&[i32], &[i32]), (u, v): (&mut [i32], &mut [i32]), m: Mix, shift_uv: &mut [u16], bytes_shifted: u32) { + let (mixbits, mixres) = (m.bits, m.res); + let shift = bytes_shifted * 8; + let m2 = (1i32 << mixbits.min(30)).wrapping_sub(mixres); + for j in 0..l.len().min(r.len()).min(u.len()).min(v.len()) { + let (lj, ls) = split(l[j], shift); + let (rj, rs) = split(r[j], shift); + if shift != 0 + && let Some(s) = shift_uv.get_mut(2 * j..2 * j + 2) + { + s.copy_from_slice(&[ls, rs]); + } + if mixres != 0 { + u[j] = shr(mixres.wrapping_mul(lj).wrapping_add(m2.wrapping_mul(rj)), mixbits); + v[j] = lj.wrapping_sub(rj); + } else { + u[j] = lj; + v[j] = rj; + } + } +} + +/// `unmix16/20/24/32`: the samples of `l` and `r` (as many as the shortest of the four holds) from +/// `u`, `v` and the shifted-off bytes, wrapped to `bit_depth` bits as the reference's stores to +/// 16/24/32-bit words wrap them. +pub(crate) fn unmix((u, v): (&[i32], &[i32]), (l, r): (&mut [i32], &mut [i32]), m: Mix, shift_uv: &[u16], bytes_shifted: u32, bit_depth: u32) { + let (mixbits, mixres) = (m.bits, m.res); + let shift = bytes_shifted * 8; + for j in 0..u.len().min(v.len()).min(l.len()).min(r.len()) { + let (mut lj, mut rj) = if mixres != 0 { + let lj = u[j].wrapping_add(v[j]).wrapping_sub(shr(mixres.wrapping_mul(v[j]), mixbits)); + (lj, lj.wrapping_sub(v[j])) + } else { + (u[j], v[j]) + }; + if shift != 0 { + let ls = shift_uv.get(2 * j).copied().unwrap_or(0); + let rs = shift_uv.get(2 * j + 1).copied().unwrap_or(0); + lj = (lj << shift) | i32::from(ls); + rj = (rj << shift) | i32::from(rs); + } + l[j] = super::bits::sign_extend(lj as u32, bit_depth); + r[j] = super::bits::sign_extend(rj as u32, bit_depth); + } +} + +/// `copyPredictorTo24Shift` and friends for one channel: the samples from the predictor output +/// and the shifted-off bytes, wrapped to `bit_depth`. +pub(crate) fn unshift(pred: &[i32], shift_buf: &[u16], out: &mut [i32], num: usize, bytes_shifted: u32, bit_depth: u32) { + let shift = bytes_shifted * 8; + for j in 0..num.min(pred.len()).min(out.len()) { + let x = if shift != 0 { (pred[j] << shift) | i32::from(shift_buf.get(j).copied().unwrap_or(0)) } else { pred[j] }; + out[j] = super::bits::sign_extend(x as u32, bit_depth); + } +} + +/// The encoder's mono split (`EncodeMono`): predictor input and shifted-off bytes of `input`. +pub(crate) fn shift_mono(input: &[i32], pred_in: &mut [i32], shift_buf: &mut [u16], num: usize, bytes_shifted: u32) { + let shift = bytes_shifted * 8; + for j in 0..num.min(input.len()).min(pred_in.len()) { + let (x, s) = split(input[j], shift); + pred_in[j] = x; + if let Some(b) = shift_buf.get_mut(j) { + *b = s; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn matrixing_inverts() { + let l: Vec = (0..100).map(|i| (i * 977 % 16_000_000) - 8_000_000).collect(); + let r: Vec = (0..100).map(|i| (i * 7919 % 16_000_000) - 8_000_000).collect(); + for (mixres, bytes) in [(0, 0), (1, 0), (2, 1), (4, 1), (3, 0)] { + let (mut u, mut v, mut s) = (vec![0; 100], vec![0; 100], vec![0u16; 200]); + let m = Mix { bits: 2, res: mixres }; + mix((&l, &r), (&mut u, &mut v), m, &mut s, bytes); + let (mut l2, mut r2) = (vec![0; 100], vec![0; 100]); + unmix((&u, &v), (&mut l2, &mut r2), m, &s, bytes, 24); + assert_eq!((l2, r2), (l.clone(), r.clone()), "mixres {mixres}"); + } + } +} diff --git a/crates/audio/src/alac/mod.rs b/crates/audio/src/alac/mod.rs new file mode 100644 index 000000000..a797d1ab6 --- /dev/null +++ b/crates/audio/src/alac/mod.rs @@ -0,0 +1,348 @@ +// Ported from Apple's ALAC reference implementation (ALACAudioTypes.h, ALACDecoder.cpp, ALACEncoder.cpp and ALACMagicCookieDescription.txt, macosforge/alac@c38887c, +// https://github.com/macosforge/alac). Copyright (c) 2011 Apple Inc. +// Licensed under the Apache License, Version 2.0 (LICENSE-APACHE at the repository root). +// Modified: translated to Rust and changed as the `alac` module docs describe. +// SPDX-License-Identifier: Apache-2.0 + +//! Apple Lossless (ALAC): [`Decoder`] (feature `alac-decode`) and [`Encoder`] (feature +//! `alac-encode`), packet by packet, with the stream configuration ([`SpecificConfig`]). +//! +//! Where the configuration goes: MP4's `alac` box (after its FullBox header) and Matroska's +//! `A_ALAC` CodecPrivate hold the 24-byte `ALACSpecificConfig` ([`SpecificConfig::to_bytes`]); +//! an MP4 of more than two channels carries the layout in a separate `chan` box, as Apple's +//! `afconvert` writes it. CAF's `kuki` chunk holds the magic cookie as Apple's encoder vends it, +//! the config followed by a `chan` atom for more than two channels ([`SpecificConfig::to_cookie`]). +//! The decoder takes either form. +//! +//! Samples are planar `i32`, right-aligned at the stream's bit depth (a 24-bit sample lies in +//! −2^23 ..= 2^23 − 1), channels in ALAC order (see [`channel_mask`]; [`wav_order`] maps them +//! to WAV order). +//! +//! Ported from Apple's reference implementation (, commit +//! c38887c, `codec/`), Copyright © 2011 Apple Inc., Apache License 2.0. Changes from the +//! reference: hostile input returns errors instead of reading or writing out of bounds, integer +//! arithmetic wraps where C would overflow, the 4- and 8-tap predictor loops are folded into the +//! general one (same values), the sample-width variants of the matrix routines into one routine +//! over `i32`, and the encoder fills in the cookie's average bit rate. + +// The bit I/O, coder, predictor and matrix modules serve both halves; with only one of +// `alac-decode` and `alac-encode` built, the other half's routines are unused. +#![cfg_attr(not(all(feature = "alac-decode", feature = "alac-encode")), allow(dead_code))] + +mod ag; +mod bits; +#[cfg(feature = "alac-decode")] +mod decoder; +mod dp; +#[cfg(feature = "alac-encode")] +mod encoder; +mod matrix; + +#[cfg(feature = "alac-decode")] +pub use decoder::Decoder; +#[cfg(feature = "decode-native")] +pub(crate) use decoder::header_frames; +#[cfg(feature = "alac-encode")] +pub use encoder::{Encoder, EncoderConfig}; + +/// The frames per packet Apple's encoder writes by default (`kALACDefaultFramesPerPacket`). +pub const DEFAULT_FRAME_LENGTH: u32 = 4096; + +/// The most frames a packet may hold here: the decoder allocates a packet's samples up front, and +/// the demuxers allow packets of at most this many frames. +pub const MAX_FRAME_LENGTH: u32 = 1 << 20; + +/// The most channels a stream has (`kALACMaxChannels`). +pub const MAX_CHANNELS: u8 = 8; + +/// Size of the `ALACSpecificConfig` at the start of a cookie. +const CONFIG_LEN: usize = 24; + +/// Size of the channel layout atom a cookie may carry after the config (`kChannelAtomSize` plus +/// `ALACAudioChannelLayout`). +const CHANNEL_LAYOUT_LEN: usize = 24; + +/// The CoreAudio channel layout tag of each channel count, the only layouts ALAC uses +/// (`ALACChannelLayoutTags`): mono C; stereo L R; then C L R, C L R Cs, C L R Ls Rs, +/// C L R Ls Rs LFE, C L R Ls Rs Cs LFE and C Lc Rc L R Ls Rs LFE. +pub const CHANNEL_LAYOUT_TAGS: [u32; 8] = + [(100 << 16) | 1, (101 << 16) | 2, (113 << 16) | 3, (116 << 16) | 4, (120 << 16) | 5, (124 << 16) | 6, (142 << 16) | 7, (127 << 16) | 8]; + +/// Element tags (`ID_SCE` … `ID_END`). +const ID_SCE: u32 = 0; +const ID_CPE: u32 = 1; +const ID_CCE: u32 = 2; +const ID_LFE: u32 = 3; +const ID_DSE: u32 = 4; +const ID_PCE: u32 = 5; +const ID_FIL: u32 = 6; +const ID_END: u32 = 7; + +/// The elements a frame of `channels` channels holds, in order (`sChannelMaps`): single channels +/// for C, the LFE and the 6.1 back centre, pairs for the rest. +pub(crate) fn element_map(channels: usize) -> &'static [u32] { + match channels { + 1 => &[ID_SCE], + 2 => &[ID_CPE], + 3 => &[ID_SCE, ID_CPE], + 4 => &[ID_SCE, ID_CPE, ID_SCE], + 5 => &[ID_SCE, ID_CPE, ID_CPE], + 6 => &[ID_SCE, ID_CPE, ID_CPE, ID_SCE], + 7 => &[ID_SCE, ID_CPE, ID_CPE, ID_SCE, ID_SCE], + 8 => &[ID_SCE, ID_CPE, ID_CPE, ID_CPE, ID_SCE], + _ => &[], + } +} + +/// The WAVEFORMATEXTENSIBLE speaker mask of the layout for `channels` (the tags in +/// [`CHANNEL_LAYOUT_TAGS`], surrounds as back speakers): 0 for a count ALAC does not have. +pub fn channel_mask(channels: u8) -> u32 { + match channels { + 1 => 0x4, + 2 => 0x3, + 3 => 0x7, + 4 => 0x107, + 5 => 0x37, + 6 => 0x3F, + 7 => 0x13F, + 8 => 0xFF, + _ => 0, + } +} + +/// Channel order for WAV/SMPTE output: WAV channel → decoded (ALAC-order) channel, for 3 to 8 +/// channels; `None` for mono and stereo, which need none, and other counts. The inverse of +/// [`alac_order`]. +pub fn wav_order(channels: u8) -> Option<&'static [usize]> { + Some(match channels { + 3 => &[1, 2, 0], + 4 => &[1, 2, 0, 3], + 5 => &[1, 2, 0, 3, 4], + 6 => &[1, 2, 0, 5, 3, 4], + 7 => &[1, 2, 0, 6, 3, 4, 5], + 8 => &[3, 4, 0, 7, 5, 6, 1, 2], + _ => return None, + }) +} + +/// Channel order for encoding WAV/SMPTE-order audio: ALAC channel → WAV channel (the +/// encoder's channel `j` is WAV channel `alac_order(n)[j]`), for 3 to 8 channels; `None` for +/// mono, stereo and other counts. The inverse of [`wav_order`]. +pub fn alac_order(channels: u8) -> Option<&'static [usize]> { + Some(match channels { + 3 => &[2, 0, 1], + 4 => &[2, 0, 1, 3], + 5 => &[2, 0, 1, 3, 4], + 6 => &[2, 0, 1, 4, 5, 3], + 7 => &[2, 0, 1, 4, 5, 6, 3], + 8 => &[2, 6, 7, 0, 1, 4, 5, 3], + _ => return None, + }) +} + +/// Errors from cookie parsing, decoding and encoding. +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum Error { + /// The magic cookie is malformed or outside the supported range. + #[error("invalid configuration: {0}")] + InvalidConfig(&'static str), + /// A valid stream uses something this module does not implement. + #[error("unsupported feature: {0}")] + Unsupported(&'static str), + /// Malformed packet. + #[error("invalid bitstream: {0}")] + Bitstream(&'static str), + /// The packet ended early. + #[error("unexpected end of packet")] + Eof, + /// Samples or settings the encoder cannot take. + #[error("invalid encoder input: {0}")] + InvalidInput(&'static str), +} + +/// Result with this module's [`Error`]. +pub type Result = std::result::Result; + +/// The `ALACSpecificConfig` of a magic cookie, with the channel layout tag when the cookie has one. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SpecificConfig { + /// Frames per packet (the last packet may hold fewer). + pub frame_length: u32, + /// 0 for every stream the reference writes or reads. + pub compatible_version: u8, + /// Bits per source sample (16, 20, 24 or 32 from Apple's encoder). + pub bit_depth: u8, + /// Rice coder tuning: history multiplier (40). + pub pb: u8, + /// Rice coder tuning: initial history (10). + pub mb: u8, + /// Rice coder tuning: largest Rice parameter (14). + pub kb: u8, + /// Channel count. + pub channels: u8, + /// Unused by the coder (255). + pub max_run: u16, + /// Largest packet in bytes, 0 when unknown. + pub max_frame_bytes: u32, + /// Average bit rate, 0 when unknown. + pub avg_bit_rate: u32, + /// Sample rate in Hz. + pub sample_rate: u32, + /// The CoreAudio channel layout tag of the cookie's `chan` atom, when it has one. + pub channel_layout_tag: Option, +} + +impl SpecificConfig { + /// A configuration with the reference encoder's tuning and no statistics. + pub fn new(sample_rate: u32, channels: u8, bit_depth: u8, frame_length: u32) -> Self { + SpecificConfig { + frame_length, + compatible_version: 0, + bit_depth, + pb: ag::PB0 as u8, + mb: ag::MB0 as u8, + kb: ag::KB0 as u8, + channels, + max_run: ag::MAX_RUN_DEFAULT as u16, + max_frame_bytes: 0, + avg_bit_rate: 0, + sample_rate, + channel_layout_tag: None, + } + } + + /// Parse a magic cookie. As in the reference decoder, `frma` and `alac` atom headers in front + /// of the config (cookies from older QuickTime files) are skipped. The fields are not checked + /// here; [`Decoder::new`] checks what it needs. + pub fn parse(cookie: &[u8]) -> Result { + let mut c = cookie; + for atom in [b"frma", b"alac"] { + if c.get(4..8) == Some(atom.as_slice()) { + c = c.get(12..).ok_or(Error::InvalidConfig("magic cookie shorter than its atom header"))?; + } + } + let b = c.get(..CONFIG_LEN).ok_or(Error::InvalidConfig("magic cookie shorter than 24 bytes"))?; + let u32_at = |i: usize| b.get(i..i + 4).and_then(|s| s.try_into().ok()).map_or(0, u32::from_be_bytes); + let byte = |i: usize| b.get(i).copied().unwrap_or(0); + // `chan` atom: size, 'chan', version and flags, layout tag, bitmap, description count. + let rest = c.get(CONFIG_LEN..).unwrap_or_default(); + // Apple's reference encoder copies the tag in host byte order (little-endian on x86 and + // ARM) although the cookie is big-endian: a byte-swapped standard tag is read as that tag. + let channel_layout_tag = (rest.get(4..8) == Some(b"chan".as_slice())) + .then(|| rest.get(12..16).and_then(|s| s.try_into().ok()).map(u32::from_be_bytes)) + .flatten() + .map(|t| if !CHANNEL_LAYOUT_TAGS.contains(&t) && CHANNEL_LAYOUT_TAGS.contains(&t.swap_bytes()) { t.swap_bytes() } else { t }); + Ok(SpecificConfig { + frame_length: u32_at(0), + compatible_version: byte(4), + bit_depth: byte(5), + pb: byte(6), + mb: byte(7), + kb: byte(8), + channels: byte(9), + max_run: u16::from_be_bytes([byte(10), byte(11)]), + max_frame_bytes: u32_at(12), + avg_bit_rate: u32_at(16), + sample_rate: u32_at(20), + channel_layout_tag, + }) + } + + /// The 24-byte `ALACSpecificConfig`, as MP4's `alac` box and Matroska's CodecPrivate hold it. + pub fn to_bytes(&self) -> Vec { + let mut out = Vec::with_capacity(CONFIG_LEN + CHANNEL_LAYOUT_LEN); + out.extend_from_slice(&self.frame_length.to_be_bytes()); + out.extend_from_slice(&[self.compatible_version, self.bit_depth, self.pb, self.mb, self.kb, self.channels]); + out.extend_from_slice(&self.max_run.to_be_bytes()); + out.extend_from_slice(&self.max_frame_bytes.to_be_bytes()); + out.extend_from_slice(&self.avg_bit_rate.to_be_bytes()); + out.extend_from_slice(&self.sample_rate.to_be_bytes()); + out + } + + /// The magic cookie as the reference encoder vends it (`GetMagicCookie`) and CAF stores it: + /// [`Self::to_bytes`], then for more than two channels a `chan` atom with + /// [`Self::channel_layout_tag`] (the standard tag for the count when `None`). The tag is + /// written big-endian, as Apple's cookie description specifies (the reference encoder writes + /// it in host byte order). + pub fn to_cookie(&self) -> Vec { + let mut out = self.to_bytes(); + let standard = CHANNEL_LAYOUT_TAGS.get(usize::from(self.channels).wrapping_sub(1)).copied(); + if let Some(tag) = self.channel_layout_tag.or(standard).filter(|_| self.channels > 2) { + out.extend_from_slice(&(CHANNEL_LAYOUT_LEN as u32).to_be_bytes()); + out.extend_from_slice(b"chan"); + out.extend_from_slice(&[0; 4]); + out.extend_from_slice(&tag.to_be_bytes()); + out.extend_from_slice(&[0; 8]); + } + out + } + + /// Whether the channels are the standard layout for their count: the cookie names that + /// layout, or none (the reference decoder ignores the tag and every ALAC encoder writes the + /// standard order). + pub fn has_standard_layout(&self) -> bool { + let standard = CHANNEL_LAYOUT_TAGS.get(usize::from(self.channels).wrapping_sub(1)).copied(); + standard.is_some() && self.channel_layout_tag.is_none_or(|t| Some(t) == standard) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cookies_roundtrip() { + let mut c = SpecificConfig::new(48_000, 6, 24, 4096); + c.max_frame_bytes = 12_345; + c.avg_bit_rate = 2_000_000; + let bytes = c.to_cookie(); + assert_eq!(bytes.len(), 48); + let mut parsed = SpecificConfig::parse(&bytes).unwrap(); + assert_eq!(parsed.channel_layout_tag, Some((124 << 16) | 6)); + assert!(parsed.has_standard_layout()); + parsed.channel_layout_tag = None; + assert_eq!(parsed, c); + assert_eq!(c.to_bytes(), bytes[..24]); + let stereo = SpecificConfig::new(44_100, 2, 16, 4096).to_cookie(); + assert_eq!(stereo.len(), 24); + assert_eq!(&stereo[..12], &[0, 0, 0x10, 0, 0, 16, 40, 10, 14, 2, 0, 255]); + } + + #[test] + fn a_little_endian_layout_tag_is_read() { + // as Apple's reference encoder writes the tag of a 7.1 stream + let mut cookie = SpecificConfig::new(44_100, 8, 16, 4096).to_cookie(); + cookie[36..40].copy_from_slice(&((127u32 << 16) | 8).to_le_bytes()); + let c = SpecificConfig::parse(&cookie).unwrap(); + assert_eq!(c.channel_layout_tag, Some((127 << 16) | 8)); + assert!(c.has_standard_layout()); + } + + #[test] + fn old_quicktime_atoms_are_skipped() { + let config = SpecificConfig::new(44_100, 2, 16, 4096).to_cookie(); + let mut wrapped = vec![0, 0, 0, 12, b'f', b'r', b'm', b'a', b'a', b'l', b'a', b'c', 0, 0, 0, 36, b'a', b'l', b'a', b'c', 0, 0, 0, 0]; + wrapped.extend_from_slice(&config); + assert_eq!(SpecificConfig::parse(&wrapped).unwrap(), SpecificConfig::parse(&config).unwrap()); + assert!(SpecificConfig::parse(&wrapped[..30]).is_err()); + assert!(SpecificConfig::parse(&[0; 5]).is_err()); + } + + #[test] + fn the_orders_are_permutations() { + for n in 3..=8u8 { + let order = wav_order(n).unwrap(); + let inverse = alac_order(n).unwrap(); + assert!((0..usize::from(n)).all(|i| inverse[order[i]] == i && order[inverse[i]] == i), "{n} channels"); + let mut sorted = order.to_vec(); + sorted.sort_unstable(); + assert_eq!(sorted, (0..usize::from(n)).collect::>()); + assert_eq!(channel_mask(n).count_ones(), u32::from(n)); + let elements: usize = element_map(usize::from(n)).iter().map(|&e| if e == ID_CPE { 2 } else { 1 }).sum(); + assert_eq!(elements, usize::from(n)); + } + } +} diff --git a/crates/audio/src/layout.rs b/crates/audio/src/layout.rs index a1da0b6c2..f3cab1b7f 100644 --- a/crates/audio/src/layout.rs +++ b/crates/audio/src/layout.rs @@ -145,7 +145,14 @@ impl TryFrom for ChannelLayout { } /// A channel count as the API holds it, saturating at `u16::MAX`. -#[cfg(any(feature = "aac-decode", feature = "opus-decode", feature = "ac3-decode", feature = "mpa-decode", feature = "pcm-encode"))] +#[cfg(any( + feature = "aac-decode", + feature = "alac-decode", + feature = "opus-decode", + feature = "ac3-decode", + feature = "mpa-decode", + feature = "pcm-encode" +))] pub(crate) fn count(n: usize) -> u16 { u16::try_from(n).unwrap_or(u16::MAX) } diff --git a/crates/audio/src/lib.rs b/crates/audio/src/lib.rs index 36940b02e..f5bf0a100 100644 --- a/crates/audio/src/lib.rs +++ b/crates/audio/src/lib.rs @@ -8,11 +8,11 @@ //! `wasm32-unknown-unknown`. //! //! - [`decode`], [`probe`], [`Decoder`] (streaming, seekable), [`tracks`], [`detect_container`] -//! (feature `decode`): WAV and AIFF are read natively; AAC, Opus, AC-3/E-AC-3, MP1/MP2 and PCM -//! in MP4/MOV, Matroska/WebM, Ogg or raw streams go through craft-media's demuxers and -//! [`TrackDecoder`]; everything else (MP3, FLAC, ALAC, Vorbis, CAF, ADPCM, A-law, µ-law) -//! through `symphonia`, which is to be replaced by native decoders. Feature `decode-native` is -//! the same without symphonia. +//! (feature `decode`): WAV and AIFF are read natively; AAC, ALAC, Opus, AC-3/E-AC-3, MP1/MP2 and +//! PCM in MP4/MOV, Matroska/WebM, Ogg or raw streams go through craft-media's demuxers and +//! [`TrackDecoder`]; everything else (MP3, FLAC, Vorbis, CAF, ADPCM, A-law, µ-law) through +//! `symphonia`, which is to be replaced by native decoders. Feature `decode-native` is the +//! same without symphonia. //! - [`TrackDecoder`] (any decoder feature): one track's packets from any demuxer to planar //! `f32` in WAV channel order. //! - [`encode`], [`FileEncoder`]: WAV/BWF/RF64 and AIFF/AIFF-C (feature `pcm-encode`, which also @@ -21,7 +21,8 @@ //! `lossless-encode` and `aac-encode`. //! - [`peaks`] (feature `peaks`): min/max/RMS pyramid for waveform drawing. //! - Codec modules for packet-level use, each with its own features: [`aac`] (`aac-decode`, -//! `aac-encode`), [`opus`] (`opus-decode`), [`ac3`] (`ac3-decode`), [`mpa`] (`mpa-decode`). +//! `aac-encode`), [`alac`] (`alac-decode`, `alac-encode`), [`opus`] (`opus-decode`), [`ac3`] +//! (`ac3-decode`), [`mpa`] (`mpa-decode`). //! //! The shared types below build with no features. @@ -29,19 +30,21 @@ pub mod aac; #[cfg(feature = "ac3-decode")] pub mod ac3; +#[cfg(any(feature = "alac-decode", feature = "alac-encode"))] +pub mod alac; mod buffer; mod layout; #[cfg(feature = "mpa-decode")] pub mod mpa; #[cfg(feature = "opus-decode")] pub mod opus; -#[cfg(any(feature = "aac-decode", feature = "opus-decode", feature = "ac3-decode", feature = "mpa-decode"))] +#[cfg(any(feature = "aac-decode", feature = "alac-decode", feature = "opus-decode", feature = "ac3-decode", feature = "mpa-decode"))] mod pcm; #[cfg(feature = "peaks")] pub mod peaks; #[cfg(feature = "decode-native")] mod read; -#[cfg(any(feature = "aac-decode", feature = "opus-decode", feature = "ac3-decode", feature = "mpa-decode"))] +#[cfg(any(feature = "aac-decode", feature = "alac-decode", feature = "opus-decode", feature = "ac3-decode", feature = "mpa-decode"))] mod track; #[cfg(any(feature = "decode-native", feature = "pcm-encode"))] mod wav; @@ -58,7 +61,7 @@ pub use layout::{ChannelLayout, Speaker}; pub use read::SharedSource; #[cfg(feature = "decode-native")] pub use read::{DEFAULT_MAX_SAMPLES, DecodeOptions, Decoder, TrackInfo, decode, decode_with, detect_container, probe, probe_with, tracks, tracks_of}; -#[cfg(any(feature = "aac-decode", feature = "opus-decode", feature = "ac3-decode", feature = "mpa-decode"))] +#[cfg(any(feature = "aac-decode", feature = "alac-decode", feature = "opus-decode", feature = "ac3-decode", feature = "mpa-decode"))] pub use track::{LOOKAHEAD_PACKETS, TrackDecoder}; #[cfg(feature = "pcm-encode")] pub use write::wav_float_header; @@ -135,6 +138,17 @@ impl From for AudioError { } } +#[cfg(any(feature = "alac-decode", feature = "alac-encode"))] +impl From for AudioError { + fn from(e: alac::Error) -> Self { + match e { + alac::Error::Unsupported(_) => AudioError::Unsupported(e.to_string()), + alac::Error::InvalidInput(_) => AudioError::Encode(e.to_string()), + _ => AudioError::Malformed(e.to_string()), + } + } +} + /// The file formats [`encode`] writes. Each needs its feature: WAV and AIFF `pcm-encode`, FLAC /// `flac-encode`; asking for one that is not built returns [`AudioError::Unsupported`]. #[non_exhaustive] diff --git a/crates/audio/src/read/native.rs b/crates/audio/src/read/native.rs index ed7ef2d78..40c913696 100644 --- a/crates/audio/src/read/native.rs +++ b/crates/audio/src/read/native.rs @@ -177,10 +177,9 @@ impl Packets<'_> { *pending = Some(p); // Timestamps are rounded to the file's timestamp scale (often 1 ms), so the // packet's index places it exactly where packets have a known length. - let by_index = point.sample.and_then(|k| frames.start(src, k)); - Ok(by_index.unwrap_or_else(|| { - rescale_round(u64::try_from(pts_ns.saturating_sub(*first_ns)).unwrap_or(0), 1_000_000_000, u64::from(*rate)) - })) + let timed = rescale_round(u64::try_from(pts_ns.saturating_sub(*first_ns)).unwrap_or(0), 1_000_000_000, u64::from(*rate)); + let start = point.sample.and_then(|k| frames.start(src, k, timed, |i| demux.read_sample(*track, i).ok())); + Ok(start.unwrap_or(timed)) } Some(Err(e)) => Err(e), None => Ok(frame), @@ -192,8 +191,12 @@ impl Packets<'_> { /// How the decoded-timeline start of a Matroska packet is found from its index. enum MkvFrames { - /// Every packet decodes to this many frames. - Constant(u64), + /// Every packet decodes to `frames` frames, except where a packet's timestamp is more than + /// `slack` frames (the timestamps' rounding) from that place: after a short packet. Then the + /// packets before it are counted with `count` (reading each packet's frames from its header, + /// into `starts`, the starts of the packets counted so far), or without one the timestamp + /// places it. + Constant { frames: u64, slack: u64, count: Option, starts: Vec }, /// PCM: the stored sizes of the track's packets, and the bytes per frame. Pcm { sizes: Vec, frame_bytes: u64 }, /// E-AC-3, whose packets can differ in length: each packet's length from the headers of its @@ -208,10 +211,23 @@ enum MkvFrames { } impl MkvFrames { - /// The decoded-timeline start of packet `k`, when the packets say. - fn start(&mut self, src: &Src<'_>, k: usize) -> Option { + /// The decoded-timeline start of packet `k`, when the packets say (`src`: the file, for + /// packet headers; `timed`: the start its timestamp gives; `read`: packet `i`'s data). + fn start(&mut self, src: &Src<'_>, k: usize, timed: u64, mut read: impl FnMut(usize) -> Option>) -> Option { match self { - MkvFrames::Constant(n) => Some(k as u64 * *n), + MkvFrames::Constant { frames, slack, count, starts } => { + let even = (k as u64).saturating_mul(*frames); + if even.abs_diff(timed) <= *slack { + return Some(even); + } + let count = (*count)?; + while starts.len() <= k { + let last = starts.last().copied().unwrap_or(0); + let n = read(starts.len() - 1).and_then(|p| count(&p, *frames)).unwrap_or(*frames); + starts.push(last.saturating_add(n)); + } + starts.get(k).copied() + } MkvFrames::Pcm { sizes, frame_bytes } => Some(sizes.iter().take(k).map(|&s| u64::from(s)).sum::() / *frame_bytes), MkvFrames::Eac3 { samples, prefix, starts } => { // Headers are read only as far as packet `k`; a later seek extends the list. A @@ -858,11 +874,17 @@ fn open_mkv<'a>(src: &Src<'a>, container: Container, track: Option) -> Op (0..t.samples.len().min(crate::track::LOOKAHEAD_PACKETS)).filter_map(|i| demux.read_sample(track, i).ok()).collect(); let probe = TrackDecoder::new(¶ms, &lookahead.iter().map(Vec::as_slice).collect::>()).ok()?; let rate = probe.sample_rate(); - let frames_of = match (probe.frames_per_packet(), params.pcm) { + // Packet starts come from the packets where they can: block timestamps are rounded (to + // milliseconds by default). ALAC packets are all its frame length except the last, so every + // start is a multiple of it, unless a short packet came earlier: then the timestamp is further + // from that multiple than its rounding allows, and the earlier packets' headers are counted. + let (num, den) = t.timebase; + let tick = rescale_round(num, den.max(1), u64::from(rate)).saturating_add(1); + let frames_of = match (probe.full_packet_frames(), params.pcm) { _ if probe.codec() == CodecId::Eac3 => { MkvFrames::Eac3 { samples: t.samples.iter().map(|s| (s.offset, s.size)).collect(), prefix: t.frame_prefix(0), starts: Vec::new() } } - (Some(n), _) => MkvFrames::Constant(n as u64), + (Some(n), _) => MkvFrames::Constant { frames: n as u64, slack: tick, count: probe.packet_frames_reader(), starts: vec![0] }, (None, Some(pcm)) => { let frame_bytes = u64::from(pcm.bits.div_ceil(8)) * u64::from(params.channels.unwrap_or(1).max(1)); MkvFrames::Pcm { sizes: t.samples.iter().map(|s| s.size).collect(), frame_bytes: frame_bytes.max(1) } @@ -878,7 +900,6 @@ fn open_mkv<'a>(src: &Src<'a>, container: Container, track: Option) -> Op // and the presentation starts `CodecDelay` later. Both are counted in timestamp ticks, the // delay rounded to one, so a file whose first frame is stored at 0 presents from exactly 0 // although its timestamps are rounded (AAC's 1024-frame priming is 21.33 ms, stored as -21). - let (num, den) = t.timebase; let last = t.samples.iter().max_by_key(|s| s.pts); let delay_ticks = i64::try_from(rescale_round(t.codec_delay_ns.saturating_mul(den), num.saturating_mul(1_000_000_000), 1)).unwrap_or(i64::MAX); let start = t.samples.iter().map(|s| s.pts).min().unwrap_or(0).saturating_add(delay_ticks); @@ -888,7 +909,7 @@ fn open_mkv<'a>(src: &Src<'a>, container: Container, track: Option) -> Op } else { // ffmpeg 6 writes AAC and E-AC-3 without block durations, and records the end in a tag. let n = match &frames_of { - MkvFrames::Constant(n) => *n, + MkvFrames::Constant { frames: n, .. } => *n, MkvFrames::Eac3 { prefix, .. } => eac3_packet_frames(src, prefix, s.offset, s.size)? as u64, _ => return None, }; @@ -1311,6 +1332,103 @@ mod adts_tests { let opened = Decoder::open(Failing { bytes, good: 300_000 }, Some("aac"), &DecodeOptions::default()); assert!(matches!(opened, Err(AudioError::Io(_))), "{:?}", opened.err()); } + + /// An escaped (uncompressed) stereo 16-bit ALAC packet of `chunk`, partial when shorter than + /// `frame_length`. + fn alac_escaped_stereo(chunk: &[[i16; 2]], frame_length: usize) -> Vec { + let mut w = craft_media::bitstream::BitWriter::new(); + let partial = chunk.len() != frame_length; + for (value, bits) in [(1, 3), (0, 4), (0, 12), (u32::from(partial) << 3 | 1, 4)] { + w.write_bits(value, bits); + } + if partial { + w.write_bits(chunk.len() as u32, 32); + } + for f in chunk { + w.write_bits(u32::from(f[0] as u16), 16); + w.write_bits(u32::from(f[1] as u16), 16); + } + w.write_bits(7, 3); + w.finish() + } + + /// ALAC in MP4 decodes natively (without symphonia this is the only path): every sample, + /// the short last packet included, and a seek lands on the exact frame. + #[test] + fn alac_in_mp4_decodes_and_seeks_natively() { + use craft_media::isobmff::{Brand, CodecConfig, FourCc, Mp4Writer, SampleEntry, TrackConfig, WriteSample, WriterOptions}; + // Escaped (uncompressed) stereo 16-bit packets of 4 frames, the last one of 2. + let frames: Vec<[i16; 2]> = (0..10).map(|i| [i * 100 - 300, -(i * 77)]).collect(); + let packet = |chunk: &[[i16; 2]]| alac_escaped_stereo(chunk, 4); + let cookie = crate::alac::SpecificConfig::new(48_000, 2, 16, 4).to_bytes(); + let entry = SampleEntry::audio(FourCc(*b"alac"), CodecConfig::Alac { cookie }, 2, 48_000.0, 16); + let mut w = Mp4Writer::new(std::io::Cursor::new(Vec::new()), WriterOptions::new(Brand::Mp4)).unwrap(); + let track = w.add_track(TrackConfig::new(entry, 48_000)).unwrap(); + for chunk in frames.chunks(4) { + w.write_sample(track, WriteSample { data: &packet(chunk), duration: chunk.len() as u32, composition_offset: 0, is_sync: true }).unwrap(); + } + let file = w.finish().unwrap().into_inner(); + let (info, buf) = decode(&file, None).unwrap(); + assert_eq!((info.codec, info.sample_format, info.sample_rate, info.frames), (crate::CodecId::Alac, crate::SampleFormat::Int16, 48_000, 10)); + let ints: Vec<[i16; 2]> = (0..buf.frames()).map(|i| [0, 1].map(|c| (buf.channels[c][i] * 32768.0) as i16)).collect(); + assert_eq!(ints, frames); + let mut d = Decoder::from_bytes(&file, None, &DecodeOptions::default()).unwrap(); + d.seek(5).unwrap(); + let block = d.next_block().unwrap().unwrap(); + assert_eq!((block.channels[0][0] * 32768.0) as i16, frames[5][0]); + } + + /// ALAC in Matroska seeks to the exact frame: packet starts come from the frame length, not + /// from the millisecond timestamps (packet 1 starts at frame 4096, stored as 85 ms = 4080). + #[test] + fn alac_in_matroska_seeks_to_the_exact_frame() { + use craft_media::matroska::{MkvWriter, MuxOptions, TrackKind, TrackSpec}; + let frames: Vec<[i16; 2]> = (0..10_000).map(|i| [i as i16, -(i as i16)]).collect(); + let mut spec = TrackSpec::new(TrackKind::Audio, "A_ALAC"); + spec.codec_private = crate::alac::SpecificConfig::new(48_000, 2, 16, 4096).to_bytes(); + spec.audio = Some((48_000.0, 2, Some(16))); + let mut m = MkvWriter::new(std::io::Cursor::new(Vec::new()), vec![spec], MuxOptions::default()).unwrap(); + for (k, chunk) in frames.chunks(4096).enumerate() { + let pts_ns = (k * 4096) as i64 * 1_000_000_000 / 48_000; + let duration_ns = chunk.len() as u64 * 1_000_000_000 / 48_000; + m.write_frame(0, pts_ns, true, &alac_escaped_stereo(chunk, 4096), Some(duration_ns)).unwrap(); + } + let file = m.finish().unwrap().into_inner(); + let (info, buf) = decode(&file, Some("mka")).unwrap(); + assert_eq!((info.codec, buf.frames()), (crate::CodecId::Alac, 10_000)); + let mut d = Decoder::from_bytes(&file, Some("mka"), &DecodeOptions::default()).unwrap(); + for target in [5000u64, 4096, 8191, 9999] { + d.seek(target).unwrap(); + let block = d.next_block().unwrap().unwrap(); + assert_eq!((block.channels[0][0] * 32768.0).round() as i64, target as i64, "seek to {target}"); + } + } + + /// A short ALAC packet before the end moves every later packet: seeks after it still land on + /// the exact frame. + #[test] + fn alac_in_matroska_seeks_past_a_short_packet() { + use craft_media::matroska::{MkvWriter, MuxOptions, TrackKind, TrackSpec}; + let frames: Vec<[i16; 2]> = (0..12_000).map(|i| [i as i16, -(i as i16)]).collect(); + let mut spec = TrackSpec::new(TrackKind::Audio, "A_ALAC"); + spec.codec_private = crate::alac::SpecificConfig::new(48_000, 2, 16, 4096).to_bytes(); + spec.audio = Some((48_000.0, 2, Some(16))); + let mut m = MkvWriter::new(std::io::Cursor::new(Vec::new()), vec![spec], MuxOptions::default()).unwrap(); + let mut start = 0; + for len in [4096, 1000, 4096, 2808] { + let chunk = &frames[start..start + len]; + let pts_ns = start as i64 * 1_000_000_000 / 48_000; + m.write_frame(0, pts_ns, true, &alac_escaped_stereo(chunk, 4096), Some(len as u64 * 1_000_000_000 / 48_000)).unwrap(); + start += len; + } + let file = m.finish().unwrap().into_inner(); + let mut d = Decoder::from_bytes(&file, Some("mka"), &DecodeOptions::default()).unwrap(); + for target in [6000u64, 5096, 9191, 11_999, 100] { + d.seek(target).unwrap(); + let block = d.next_block().unwrap().unwrap(); + assert_eq!((block.channels[0][0] * 32768.0).round() as i64, target as i64, "seek to {target}"); + } + } } #[cfg(all(test, feature = "ac3-decode"))] diff --git a/crates/audio/src/track.rs b/crates/audio/src/track.rs index 434b53a93..3d9ed7dfb 100644 --- a/crates/audio/src/track.rs +++ b/crates/audio/src/track.rs @@ -6,6 +6,8 @@ use crate::aac; #[cfg(feature = "ac3-decode")] use crate::ac3; +#[cfg(feature = "alac-decode")] +use crate::alac; #[cfg(feature = "mpa-decode")] use crate::mpa; #[cfg(feature = "opus-decode")] @@ -26,6 +28,13 @@ enum Inner { /// The format ADTS packets must keep, when their headers gave the configuration. adts_format: Option, }, + #[cfg(feature = "alac-decode")] + Alac { + dec: alac::Decoder, + order: Option<&'static [usize]>, + /// 2^-(bit depth - 1): full scale to ±1. + scale: f64, + }, /// The decoder, and what the packets read so far say of the track's packets. #[cfg(feature = "ac3-decode")] Ac3(Box, Ac3Packets), @@ -42,14 +51,18 @@ enum Inner { }, } +/// Reads the frames a packet holds from its data, given the frames of a full packet. +#[cfg(feature = "decode-native")] +pub(crate) type PacketFrames = fn(&[u8], u64) -> Option; + /// Decodes the packets of one audio track to planar `f32`, in WAV channel order when /// [`TrackDecoder::layout`] is ordered. /// /// It takes the track's [`CodecParams`] (from `isobmff::Track::codec_params`, /// `matroska::Track::codec_params`, `ogg::Stream::codec_params`, or built by hand for another -/// container) and decodes AAC (LC; HE-AAC as its upsampled LC core), Opus, AC-3/E-AC-3, MP1/MP2 -/// and PCM. Container timing (AAC priming, Opus pre-skip, end trimming) is the caller's: every -/// decoded sample is returned. +/// container) and decodes AAC (LC; HE-AAC as its upsampled LC core), ALAC, Opus, AC-3/E-AC-3, +/// MP1/MP2 and PCM. Container timing (AAC priming, Opus pre-skip, end trimming) is the caller's: +/// every decoded sample is returned. /// /// ```no_run /// # fn demo(params: &craft_audio::CodecParams, packets: &[Vec]) -> craft_audio::Result<()> { @@ -101,10 +114,14 @@ impl TrackDecoder { /// Opus with empty `extradata` and 1 or 2 `channels` (RTP, MPEG-TS) needs no `OpusHead`. Other /// missing configuration is [`AudioError::Malformed`]. /// - /// Codecs without a native packet decoder yet (MP3, FLAC, ALAC, Vorbis) are + /// ALAC needs its magic cookie in `extradata`. + /// + /// Codecs without a native packet decoder yet (MP3, FLAC, Vorbis) are /// [`AudioError::Unsupported`]; `craft_audio::decode` decodes them through symphonia when the /// `symphonia` feature is on. pub fn new(params: &CodecParams, lookahead: &[&[u8]]) -> Result { + #[cfg(not(feature = "aac-decode"))] + let _ = lookahead; let rate = params.sample_rate.unwrap_or(0); let channels = usize::from(params.channels.unwrap_or(0)); // AC-3 and MPEG audio read their parameters from the first frame; AAC reads the lookahead; @@ -144,6 +161,32 @@ impl TrackDecoder { let order = layout.is_ordered().then(|| aac::wav_order(cfg)).flatten(); (Inner::Aac { dec: Box::new(dec), up, order, adts_format }, codec, out, layout, SampleFormat::Compressed) } + #[cfg(feature = "alac-decode")] + CodecId::Alac => { + if params.extradata.is_empty() { + return Err(AudioError::Malformed("ALAC track without a magic cookie (extradata)".into())); + } + let dec = alac::Decoder::new(¶ms.extradata)?; + let cfg = *dec.config(); + // The cookie's rate is the codec's; the container's stands in when it is 0. + let rate = if cfg.sample_rate > 0 { cfg.sample_rate } else { rate }; + if rate == 0 { + return Err(AudioError::Malformed("ALAC track without a sample rate in its cookie or container".into())); + } + let (layout, order) = if cfg.has_standard_layout() { + (ChannelLayout::from_mask(alac::channel_mask(cfg.channels)), alac::wav_order(cfg.channels)) + } else { + (ChannelLayout::unordered(u16::from(cfg.channels)), None) + }; + let sample_format = match cfg.bit_depth { + 1..=8 => SampleFormat::Int8, + 9..=16 => SampleFormat::Int16, + 17..=24 => SampleFormat::Int24, + _ => SampleFormat::Int32, + }; + let scale = 2f64.powi(1 - i32::from(cfg.bit_depth)); + (Inner::Alac { dec, order, scale }, CodecId::Alac, rate, layout, sample_format) + } #[cfg(feature = "ac3-decode")] CodecId::Ac3 | CodecId::Eac3 => match ac3::parse_header(first) { Ok(h) => { @@ -265,6 +308,12 @@ impl TrackDecoder { } reorder(out, *order) } + #[cfg(feature = "alac-decode")] + Inner::Alac { dec, order, scale } => { + let decoded = dec.decode(packet)?; + let floats = decoded.into_iter().map(|c| c.into_iter().map(|v| (f64::from(v) * *scale) as f32).collect()).collect(); + reorder(floats, *order) + } #[cfg(feature = "mpa-decode")] Inner::Mpa(d) => { let mut out: Vec> = Vec::new(); @@ -359,6 +408,12 @@ impl TrackDecoder { None => Some(per_unit), } } + // A damaged packet whose header survives says its length; otherwise a packet is as + // long as the last one (a full one at the start). + #[cfg(feature = "alac-decode")] + Inner::Alac { dec, .. } => { + Some(dec.packet_frames(data).unwrap_or(if self.last_frames > 0 { self.last_frames } else { dec.config().frame_length as usize })) + } #[cfg(feature = "ac3-decode")] Inner::Ac3(..) => Some(ac3_packet_frames(data)), #[cfg(feature = "mpa-decode")] @@ -372,12 +427,15 @@ impl TrackDecoder { /// The frames every packet decodes to, for codecs whose packets all have one length: AAC /// (1024, 2048 for HE-AAC; a packet of several ADTS frames decodes to that many times this) and /// AC-3 / E-AC-3 (the frames of the last decoded packet, else of the first packet's primary - /// syncframes, else 1536) and MP1/MP2 (one frame per packet); `None` for Opus and PCM, and for - /// AC-3 / E-AC-3 whose packets read so far differ in length. + /// syncframes, else 1536) and MP1/MP2 (one frame per packet); `None` for Opus, PCM and ALAC + /// (whose last packet is shorter; the container's timing says), and for AC-3 / E-AC-3 whose + /// packets read so far differ in length. pub fn frames_per_packet(&self) -> Option { match &self.inner { #[cfg(feature = "aac-decode")] Inner::Aac { up, .. } => Some(aac::FRAME_LEN * if up.is_some() { 2 } else { 1 }), + #[cfg(feature = "alac-decode")] + Inner::Alac { .. } => None, #[cfg(feature = "ac3-decode")] Inner::Ac3(_, shape) if shape.varies => None, #[cfg(feature = "ac3-decode")] @@ -393,15 +451,40 @@ impl TrackDecoder { } } + /// The frames of a full packet, for codecs whose packets all have that length except ones + /// at the end of the stream: ALAC's frame length (only the last packet is shorter). Where + /// [`Self::frames_per_packet`] is `Some`, the same value. + #[cfg(feature = "decode-native")] + pub(crate) fn full_packet_frames(&self) -> Option { + match &self.inner { + #[cfg(feature = "alac-decode")] + Inner::Alac { dec, .. } => Some(dec.config().frame_length as usize), + _ => self.frames_per_packet(), + } + } + + /// How many frames a packet holds, read from the packet alone, for codecs whose packets + /// say: ALAC's headers (given the frame length). + #[cfg(feature = "decode-native")] + pub(crate) fn packet_frames_reader(&self) -> Option { + match &self.inner { + #[cfg(feature = "alac-decode")] + Inner::Alac { .. } => Some(crate::alac::header_frames), + _ => None, + } + } + /// Frames to decode before a seek target and drop, so the decoder has settled when the target /// is reached: two AAC frames, 80 ms of Opus (RFC 7845 §4.6), one AC-3 or MPEG audio frame - /// (their filter banks overlap one frame), none for PCM. E-AC-3 syncframes of fewer than six - /// blocks may reuse exponents from any syncframe before them, so once one has been read the - /// stream decodes from its start (`usize::MAX`). + /// (their filter banks overlap one frame), none for ALAC (its packets decode independently) + /// and PCM. E-AC-3 syncframes of fewer than six blocks may reuse exponents from any syncframe + /// before them, so once one has been read the stream decodes from its start (`usize::MAX`). pub fn preroll(&self) -> usize { match &self.inner { #[cfg(feature = "aac-decode")] Inner::Aac { up, .. } => 2 * aac::FRAME_LEN * if up.is_some() { 2 } else { 1 }, + #[cfg(feature = "alac-decode")] + Inner::Alac { .. } => 0, #[cfg(feature = "ac3-decode")] Inner::Ac3(_, shape) if shape.short => usize::MAX, #[cfg(feature = "ac3-decode")] @@ -428,6 +511,8 @@ impl TrackDecoder { up.reset(); } } + #[cfg(feature = "alac-decode")] + Inner::Alac { .. } => {} #[cfg(feature = "ac3-decode")] Inner::Ac3(d, _) => d.reset(), #[cfg(feature = "opus-decode")] @@ -442,6 +527,7 @@ impl TrackDecoder { fn unsupported(codec: CodecId) -> AudioError { let feature = match codec { CodecId::Aac | CodecId::HeAac => Some("aac-decode"), + CodecId::Alac => Some("alac-decode"), CodecId::Ac3 | CodecId::Eac3 => Some("ac3-decode"), CodecId::Opus => Some("opus-decode"), CodecId::Mp1 | CodecId::Mp2 => Some("mpa-decode"), @@ -450,7 +536,7 @@ fn unsupported(codec: CodecId) -> AudioError { match feature { // A codec this crate decodes, in a build without its feature. Some(f) => AudioError::Unsupported(format!("decoding {} needs the `{f}` feature", codec.name())), - None if cfg!(feature = "symphonia") && matches!(codec, CodecId::Mp3 | CodecId::Flac | CodecId::Alac | CodecId::Vorbis) => { + None if cfg!(feature = "symphonia") && matches!(codec, CodecId::Mp3 | CodecId::Flac | CodecId::Vorbis) => { AudioError::Unsupported(format!("no native {} packet decoder yet (craft_audio::decode reads it through symphonia)", codec.name())) } None => AudioError::Unsupported(format!("no native {} packet decoder yet", codec.name())), @@ -459,7 +545,7 @@ fn unsupported(codec: CodecId) -> AudioError { /// `channels` reordered by `order` (output channel → decoded channel); unchanged when the counts /// differ. -#[cfg(any(feature = "aac-decode", feature = "opus-decode"))] +#[cfg(any(feature = "aac-decode", feature = "alac-decode", feature = "opus-decode"))] fn reorder(mut channels: Vec>, order: Option<&[usize]>) -> Vec> { match order { Some(order) if order.len() == channels.len() => order.iter().map(|&c| channels.get_mut(c).map(std::mem::take).unwrap_or_default()).collect(), @@ -1056,13 +1142,56 @@ mod tests { #[test] fn codecs_without_a_packet_decoder_are_unsupported() { - for codec in [CodecId::Flac, CodecId::Vorbis, CodecId::Alac, CodecId::Mp3, CodecId::H264] { + for codec in [CodecId::Flac, CodecId::Vorbis, CodecId::Mp3, CodecId::H264] { assert!(matches!(TrackDecoder::new(&CodecParams::new(codec), &[]), Err(AudioError::Unsupported(_))), "{codec:?}"); } // A codec whose feature is off names the feature. + #[cfg(not(feature = "aac-decode"))] + assert!(matches!(TrackDecoder::new(&CodecParams::new(CodecId::Aac), &[]), Err(AudioError::Unsupported(m)) if m.contains("aac-decode"))); + #[cfg(not(feature = "alac-decode"))] + assert!(matches!(TrackDecoder::new(&CodecParams::new(CodecId::Alac), &[]), Err(AudioError::Unsupported(m)) if m.contains("alac-decode"))); #[cfg(not(feature = "opus-decode"))] assert!(matches!(TrackDecoder::new(&CodecParams::new(CodecId::Opus), &[]), Err(AudioError::Unsupported(m)) if m.contains("opus-decode"))); // PCM needs its sample format. assert!(TrackDecoder::new(&CodecParams::new(CodecId::Pcm).with_channels(2).with_sample_rate(48_000), &[]).is_err()); } + + #[cfg(feature = "alac-decode")] + #[test] + fn alac_needs_a_sample_rate() { + let cookie = alac::SpecificConfig::new(0, 2, 16, 4096).to_bytes(); + assert!(matches!(TrackDecoder::new(&CodecParams::new(CodecId::Alac).with_extradata(cookie.clone()), &[]), Err(AudioError::Malformed(_)))); + let dec = TrackDecoder::new(&CodecParams::new(CodecId::Alac).with_extradata(cookie).with_sample_rate(32_000), &[]).unwrap(); + assert_eq!(dec.sample_rate(), 32_000); + } + + #[cfg(feature = "alac-decode")] + #[test] + fn alac_needs_its_cookie() { + assert!(matches!(TrackDecoder::new(&CodecParams::new(CodecId::Alac), &[]), Err(AudioError::Malformed(_)))); + let bad = CodecParams::new(CodecId::Alac).with_extradata(vec![0; 10]); + assert!(matches!(TrackDecoder::new(&bad, &[]), Err(AudioError::Malformed(_)))); + } + + /// ALAC 5.1 comes out in WAV order at full scale, and a lost packet is as long as the last. + #[cfg(all(feature = "alac-decode", feature = "alac-encode"))] + #[test] + fn alac_decodes_in_wav_order() { + // ALAC order C L R Ls Rs LFE: channel i holds the constant i * 1000. + let input: Vec> = (0..6).map(|i| vec![i * 1000; 300]).collect(); + let mut enc = alac::Encoder::new(alac::EncoderConfig::new(48_000, 6, 16).with_frame_length(1024)).unwrap(); + let packet = enc.encode(&input.iter().map(Vec::as_slice).collect::>()).unwrap(); + let params = CodecParams::new(CodecId::Alac).with_extradata(enc.cookie()).with_sample_rate(44_100); + let mut dec = TrackDecoder::new(¶ms, &[]).unwrap(); + // a damaged packet whose header survives is concealed with its own length + assert_eq!(dec.conceal(Some(&packet[..8])).unwrap().frames(), 300); + assert_eq!((dec.sample_rate(), dec.layout(), dec.sample_format()), (48_000, ChannelLayout::SURROUND_5_1, SampleFormat::Int16)); + assert_eq!(dec.conceal(None).unwrap().frames(), 1024); + let out = dec.decode(&packet).unwrap(); + // WAV order L R C LFE Ls Rs + let firsts: Vec = out.channels.iter().map(|c| c[0] * 32768.0).collect(); + assert_eq!(firsts, vec![1000.0, 2000.0, 0.0, 5000.0, 3000.0, 4000.0]); + assert_eq!(dec.conceal(None).unwrap().frames(), 300); + assert_eq!((dec.frames_per_packet(), dec.preroll()), (None, 0)); + } } diff --git a/crates/audio/tests/audio/malformed.rs b/crates/audio/tests/audio/malformed.rs index 0e712055a..a4feb37da 100644 --- a/crates/audio/tests/audio/malformed.rs +++ b/crates/audio/tests/audio/malformed.rs @@ -305,7 +305,8 @@ fn packets_longer_than_any_codec_uses_are_refused() { // read, and nothing is allocated from it. symphonia read it, so SoundCraft refused the file. let (_, buf) = decode(&mp4(480, &lpcm(1 << 21), |_, a| a), None).unwrap(); assert_eq!(buf.frames(), 480); - // ALAC still goes to symphonia, behind the same guard. + // ALAC: a cookie declaring such packets is refused by the native decoder and then by the + // symphonia path's guard. assert!(matches!(decode(&mp4(480, &alac(1 << 21), |_, a| a), None), Err(AudioError::Malformed(m)) if m.contains("declares packets"))); } diff --git a/crates/audio/tests/codecs/alac.rs b/crates/audio/tests/codecs/alac.rs new file mode 100644 index 000000000..5b42f2e83 --- /dev/null +++ b/crates/audio/tests/codecs/alac.rs @@ -0,0 +1,155 @@ +//! 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> { + 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], bits: u32) -> Vec { + 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> { + 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], bits: u32, frame_length: u32) -> Vec { + 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, 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::>()).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> { + 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"); + } + } +} diff --git a/crates/audio/tests/codecs/main.rs b/crates/audio/tests/codecs/main.rs index 3f6bea5f6..910cf3516 100644 --- a/crates/audio/tests/codecs/main.rs +++ b/crates/audio/tests/codecs/main.rs @@ -9,6 +9,7 @@ mod testkit; mod aac; mod ac3_oracle; +mod alac; mod concealment; mod mpa_oracle; mod opus;