diff --git a/Cargo.toml b/Cargo.toml index d59fd35ed..576efca6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,10 +91,10 @@ craft-keymap = { path = "crates/keymap" } craft-assuan = { path = "crates/assuan" } craft-kra = { path = "crates/kra" } craft-audio = { path = "crates/audio" } -# No default features, so craft-audio's `encode` builds craft-media without `isobmff`. craft-latex = { path = "crates/latex" } craft-markdown = { path = "crates/markdown" } craft-math = { path = "crates/math" } +# No default features: craft-audio turns on the craft-media modules each of its features needs. craft-media = { path = "crates/media", default-features = false } craft-materialx = { path = "crates/materialx" } craft-media-testkit = { path = "crates/media-testkit" } diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index 9b6200198..d132f23da 100644 --- a/crates/audio/Cargo.toml +++ b/crates/audio/Cargo.toml @@ -23,6 +23,7 @@ decode-native = ["aac-decode", "alac-decode", "opus-decode", "ac3-decode", "mpa- # 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, ALAC, AC-3/E-AC-3, MP3 and Opus encoders. +# All but Opus are also file formats of `encode` and `FileEncoder`. encode = ["lossless-encode", "aac-encode", "alac-encode", "ac3-encode", "eac3-encode", "mp3-encode", "opus-encode"] # Encode WAV/BWF/RF64, AIFF/AIFF-C and FLAC files. lossless-encode = ["pcm-encode", "flac-encode"] @@ -32,16 +33,18 @@ pcm-encode = [] flac-encode = ["craft-media/bitstream"] # Codec modules on their own, for packet-level use. aac-decode = ["craft-media/bitstream"] -aac-encode = ["craft-media/bitstream"] +# The AAC encoder (`aac::Encoder`), and AAC in M4A through `encode` and `FileEncoder`. +aac-encode = ["craft-media/bitstream", "craft-media/isobmff"] opus-decode = [] opus-encode = [] ac3-decode = ["craft-media/bitstream"] mpa-decode = ["craft-media/bitstream"] # MPEG-1/2 Layer III (MP3) decoder (`mp3::Decoder`). mp3-decode = ["mpa-decode"] -# ALAC packet decoder and encoder (`alac`), ported from Apple's reference implementation. +# ALAC packet decoder and encoder (`alac`), ported from Apple's reference implementation. The +# encoder also writes ALAC in M4A through `encode` and `FileEncoder`. alac-decode = [] -alac-encode = [] +alac-encode = ["craft-media/isobmff"] ac3-encode = ["craft-media/bitstream"] eac3-encode = ["craft-media/bitstream"] # MPEG-1/2 Layer III (MP3) encoder (`mp3`). diff --git a/crates/audio/README.md b/crates/audio/README.md index 518dbeb4c..5a5df4f2f 100644 --- a/crates/audio/README.md +++ b/crates/audio/README.md @@ -29,19 +29,19 @@ No default features. The shared types (`AudioBuffer`, `AudioInfo`, `ChannelLayou | `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`); `wav_pcm`, `aiff_pcm` (a file's integer PCM bit for bit, as `RawPcm`) | the six 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, ALAC, AC-3/E-AC-3, MP3 and Opus encoders | `lossless-encode`, `aac-encode`, `alac-encode`, `ac3-encode`, `eac3-encode`, `mp3-encode`, `opus-encode` | +| `encode` | every encoder, and every format of `encode` and `FileEncoder` | `lossless-encode`, `aac-encode`, `alac-encode`, `ac3-encode`, `eac3-encode`, `mp3-encode`, `opus-encode` | | `lossless-encode` | WAV, AIFF and FLAC writing | `pcm-encode`, `flac-encode` | | `pcm-encode` | `encode`, `FileEncoder` for WAV and AIFF; `wav_float_header`; `pcm_wav_file`, `pcm_wav_header`, `pcm_aiff_file`, `pcm_aiff_header` (minimal files around integer PCM bytes, for essence embedded in AAF / OMF) | none | | `flac-encode` | `encode`, `FileEncoder` for FLAC; `flac::Encoder`, the packet encoder for callers that mux FLAC themselves | craft-media `bitstream` | -| `aac-decode`, `aac-encode` | `aac::Decoder`, `aac::Encoder`; with `aac-decode`, `aac::latm_config` / `latm_split` for LOAS / LATM frames (AAC in MPEG transport streams) | craft-media `bitstream` | +| `aac-decode`, `aac-encode` | `aac::Decoder`, `aac::Encoder`; with `aac-decode`, `aac::latm_config` / `latm_split` for LOAS / LATM frames (AAC in MPEG transport streams); with `aac-encode`, `encode`, `FileEncoder` for AAC in M4A | craft-media `bitstream`; `aac-encode` also `isobmff` | | `alac-decode` | `alac::Decoder` | none | -| `alac-encode` | `alac::Encoder` | none | +| `alac-encode` | `alac::Encoder`; `encode`, `FileEncoder` for ALAC in M4A | craft-media `isobmff` | | `opus-decode`, `opus-encode` | `opus::Decoder`/`StreamDecoder`, `opus::Encoder` | none | | `ac3-decode` | `ac3::Decoder`, `ac3::ProgrammeDecoder` | craft-media `bitstream` | -| `ac3-encode`, `eac3-encode` | `ac3::encoder::Encoder` | craft-media `bitstream` | +| `ac3-encode`, `eac3-encode` | `ac3::encoder::Encoder`; `encode`, `FileEncoder` for AC-3 and E-AC-3 streams | craft-media `bitstream` | | `mpa-decode` | `mpa::Decoder` | craft-media `bitstream` | | `mp3-decode` | `mp3::Decoder`, `mp3::InfoTag` | `mpa-decode` | -| `mp3-encode` | `mp3::Encoder` | craft-media `bitstream` | +| `mp3-encode` | `mp3::Encoder`; `encode`, `FileEncoder` for MP3 | craft-media `bitstream` | | `peaks` | `peaks::Peaks` | none | | `rayon` | parallel AAC encoding (with `aac-encode`) | `rayon` | | `serde` | `Serialize`/`Deserialize` on the shared types | `serde` | @@ -230,17 +230,86 @@ let wav = encode(&surround, &EncodeOptions::default().with_layout(ChannelLayout: let mut enc = FileEncoder::new(std::fs::File::create("take.wav")?, &EncodeOptions::default(), 48_000, 2)?; enc.write(&buf.channels, buf.frames())?; // one slice (or Vec) per channel enc.finish()?; + +// The coded formats go the same way: AAC in M4A at 192 kbit/s, 5.1 as E-AC-3, 16-bit ALAC. +let m4a = encode(&buf, &EncodeOptions::new(EncodeFormat::Aac).with_bitrate(192_000))?; +let eac3 = encode(&surround, &EncodeOptions::new(EncodeFormat::Eac3).with_layout(ChannelLayout::SURROUND_5_1_SIDE))?; +let alac = encode(&buf, &EncodeOptions::new(EncodeFormat::Alac).with_bit_depth(BitDepth::Int16))?; ``` `EncodeOptions::default()` is 24-bit WAV. `with_bwf` adds a BWF `bext` chunk and `with_force_rf64` writes RF64 below 4 GiB; both apply to WAV only. `encode_format_for("aiff")` -maps an extension to an `EncodeFormat`, and `ENCODE_EXTENSIONS` lists the extensions the enabled -features can write. `EncodeFormat::ALL` lists the formats; `extension()` is the extension a new -file gets and `mime_type()` its media type. `write` refuses a block without one slice per channel, or with a slice -shorter than `frames`, and writes nothing. After a write fails for another reason, every later +maps an extension to an `EncodeFormat` (`"m4a"` to AAC; `"alac"` names ALAC), and `ENCODE_EXTENSIONS` lists the extensions the enabled +features can write. `EncodeFormat::ALL` lists the formats and `is_built()` says whether this build +writes one; `extension()` is the extension a new file gets and `mime_type()` its media type. +`write` refuses a block without one slice per channel, or with a slice shorter than `frames`, and +writes nothing. After a write fails for another reason, every later `write` and `finish` returns an error. -### Encode AAC +| `EncodeFormat` | File | Audio | Options used | +|---|---|---|---| +| `Wav`, `Aiff`, `Flac` | WAV / BWF / RF64, AIFF / AIFF-C, FLAC | any rate; FLAC up to 8 channels | `bit_depth`, `dither`, `layout` (WAV, FLAC), `bwf`, `force_rf64` (WAV) | +| `Alac` | M4A | 1–8 channels, any rate | `bit_depth` (16, 24, 32), `dither`, `layout` | +| `Aac` | M4A | AAC-LC, 1–8 channels, the 13 AAC rates (7350–96000 Hz) | `rate_control`, `layout` | +| `Mp3` | `.mp3` stream | mono or stereo; 32, 44.1, 48 kHz or 16, 22.05, 24 kHz | `rate_control` (bit rate), `layout` | +| `Ac3`, `Eac3` | `.ac3` / `.eac3` stream | up to 5.1 (E-AC-3: 7.1); 32, 44.1, 48 kHz | `rate_control` (bit rate), `layout` | + +`with_bitrate(bits_per_second)` sets a constant bit rate for all channels together. A rate the +stream cannot carry is moved to the nearest one it can: MP3 and AC-3 use the highest rate of their +tables not above it, AAC and E-AC-3 keep it inside their range (AAC from 6 kbit/s or a quarter of +the sample rate per channel, whichever is more: from about half of that down the encoder codes +silence). `with_quality(0.0..=1.0)` is AAC's +constant quality (VBR); the other lossy formats return `Unsupported` for it. Without either, AAC +is written at 64 kbit/s per channel, MP3 at 96 kbit/s per channel, and AC-3 and E-AC-3 at +96 kbit/s per full-bandwidth channel (the LFE not counted). + +The audio is in WAV channel order for every format; `layout` (the usual layout for the channel +count when unset) names the speakers, and the channels are reordered into the codec's own order. +Each coded format has a fixed set of layouts, and audio with other speakers is `Unsupported` +rather than written to the wrong ones: + +- AAC: C; L R; L R C; L R C Cb; L R C Ls Rs; 5.1; 6.1; and for 8 channels L R C LFE Lb Rb Lc Rc + (a front pair left and right of centre, mask `0xFF`). ALAC: the same, with 6.1's back centre last. + WAV 7.1 (backs and sides) is not that 7.1, and quad (the usual 4-channel layout) has no centre: + both need another format. +- AC-3 and E-AC-3: the layout picks the coding mode (1/0 to 3/2, with or without LFE; E-AC-3 also + 7.1 with backs and sides). +- A surround pair is a surround pair: side and back surrounds are interchangeable where the format + has one pair. + +Before a file is created, the options and the format say what will happen: + +```rust +let opts = EncodeOptions::new(EncodeFormat::Ac3).with_layout(info.layout).with_bitrate(200_000); +opts.check(info.sample_rate, info.channels)?; // the error FileEncoder::new would return, nothing written +let bps = opts.bitrate(info.sample_rate, info.channels)?; // Some(192_000): the rate actually used; None when lossless or by quality + +EncodeFormat::Ac3.sample_rates(); // Some(&[32_000, 44_100, 48_000]); None when any rate will do +EncodeFormat::Ac3.max_channels(); // 6 +EncodeFormat::Ac3.fixed_layouts(3); // L R C, L R Cb, L R LFE; None for WAV, AIFF and FLAC (any layout) +EncodeFormat::Ac3.supports_layout(&info.layout); // one of those, sides and backs standing for each other +EncodeFormat::Ac3.supports_quality(); // false: AAC only +``` + +`FileEncoder::bitrate()` gives the rate of a file being written. The `EncodeFormat` queries answer +for a format whose feature is off too; `check` and `EncodeOptions::bitrate` (there with any +encoder feature) return `Unsupported` for it. + +The layout `decode` reports (`AudioInfo::layout`) can be passed to `with_layout` as it is. When a +coded format refuses it (2.1 or quad into AAC, a single channel tagged as left, channels with no +speaker assignment), the choices are another format, or audio rearranged into one of the layouts +above with that layout set; leaving `layout` unset declares the usual layout for the count. This +crate's decoder returns 7- and 8-channel AAC without a speaker assignment, in AAC order, so such a +file needs that step before it is written again. + +An M4A's edit list skips the AAC encoder's priming (1024 frames) and ends at the last frame +written, so the file decodes to the length that went in. An MP3, AC-3 or E-AC-3 stream has no +place for that (see [Limits](#limits)). For a container of your own, use the codec modules below. + +### Encode AAC packets + +For an `.m4a` file, use `EncodeFormat::Aac` above. `aac::Encoder` is the packet encoder behind it, +for muxing into a container of your own: ```rust use craft_audio::AudioError; @@ -343,14 +412,14 @@ match craft_audio::decode(&bytes, None) { | WAV, BWF, RF64/BW64 | native | native | | AIFF, AIFF-C | native | native | | FLAC | symphonia | native | -| AAC-LC | native | native | +| AAC-LC | native | native; M4A through `encode` / `FileEncoder`, packets through `aac::Encoder` | | HE-AAC v1 | native (SBR) | — | | HE-AAC v2 | native, without parametric stereo (see [Limits](#limits)) | — | -| ALAC | native | native, packets (`alac::Encoder`; no file writer) | +| ALAC | native | native; M4A through `encode` / `FileEncoder`, packets through `alac::Encoder` | | MP1, MP2 (MPEG audio Layer I/II) | native | — | -| MP3 (Layer III) | native | native, frames (`mp3::Encoder`; the frames in a row are an `.mp3` file) | -| Opus | native | native | -| AC-3, E-AC-3 | native | native packet encoder | +| MP3 (Layer III) | native | native; `.mp3` through `encode` / `FileEncoder`, frames through `mp3::Encoder` | +| Opus | native | native, packets (`opus::Encoder`; no file writer) | +| AC-3, E-AC-3 | native | native; `.ac3` / `.eac3` through `encode` / `FileEncoder`, packets through `ac3::encoder::Encoder` | | Vorbis, ADPCM, A-law, µ-law | symphonia | — | | Container | Demuxer | Timing | @@ -410,10 +479,26 @@ match craft_audio::decode(&bytes, None) { - `TrackDecoder` returns `Unsupported` for 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: craft-media's MP4 muxer writes no `chan` box, so an M4A of more than two channels (from + `EncodeFormat::Alac` or muxed from `alac::Encoder` packets) carries no explicit layout; decoders + then use the standard ALAC layout for the count, which is the one written. +- `EncodeFormat::Mp3`, `Ac3` and `Eac3` write raw streams, which cannot signal the codec's delay or + the length: the file decodes with 1057 (MP3) or 256 (AC-3, E-AC-3) frames of delay in front and + padded to whole frames at the end. MP3 gets no Xing / Info frame. `EncodeFormat::Aac` and `Alac` + (M4A) decode to the length written. +- An AAC M4A's length is in the edit list and the last packet's duration. This crate, Apple's + decoders and ffmpeg 9 end the audio there; ffmpeg 8 skips the priming but plays the last + packet's padding (under 1024 frames of near silence). +- `EncodeFormat::Aac` and `Alac` write the `moov` box after the audio (no fast start), and no + metadata. Raw ADTS AAC is not a file format of `encode`; `aac::EncoderConfig::with_adts` gives + the frames. +- This crate's file reader plays a raw E-AC-3 7.1 stream as its 5.1 independent substream. +- `EncodeFormat::Aac` with 8 channels is AAC channel configuration 7 (C, Lc Rc, L R, Ls Rs, LFE), + which Apple's decoders read as written. ffmpeg assumes such a stream is a mislabelled 7.1 with + backs and sides and moves the channels accordingly, unless run with `-strict 1`. +- A file with no audio: MP3, AC-3 and E-AC-3 are then zero bytes, which nothing can probe. An + empty AAC M4A (a track with no samples) is read by ffmpeg; this crate's native reader passes it + on, so `decode-native` alone refuses it and symphonia reads only some as zero frames. - 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. @@ -481,7 +566,7 @@ finite count as silence and the rest are clamped to ±4, so no input makes an in decoder's 7680-bit input buffer. - Not yet: block switching (short blocks; transients pre-echo), `scfsi` and `preflag`, intensity stereo, a tonality-aware model, VBR/ABR, the Xing/Info and LAME gapless tags (the delay is not - signalled), CRC, free format, and an `EncodeFormat` entry for `encode`/`FileEncoder`. + signalled), CRC and free format. ### AC-3 and E-AC-3 encoding diff --git a/crates/audio/examples/encode.rs b/crates/audio/examples/encode.rs index 28de0f5ad..51cdecc84 100644 --- a/crates/audio/examples/encode.rs +++ b/crates/audio/examples/encode.rs @@ -1,4 +1,4 @@ -//! Write a tone as WAV and FLAC, whole and streamed, and as AAC packets. +//! Write a tone as WAV, FLAC and AAC in M4A, whole and streamed, and as AAC packets. //! //! `cargo run -p craft-audio --example encode --features lossless-encode,aac-encode -- out` @@ -28,7 +28,11 @@ fn main() -> Result<(), Box> { } enc.finish()?; - // AAC packets for an MP4 or Matroska muxer, with the configuration its sample entry needs. + // AAC in M4A at 160 kbit/s: the same call, with a bit rate in place of a bit depth. + let m4a = encode(&stereo, &EncodeOptions::new(EncodeFormat::Aac).with_bitrate(160_000))?; + std::fs::write(dir.join("tone.m4a"), m4a)?; + + // AAC packets for a muxer of your own, with the configuration its sample entry needs. let mut aac = craft_audio::aac::Encoder::new(craft_audio::aac::EncoderConfig::cbr(rate as u32, 2, 128_000))?; let mut units = aac.encode(&[&tone, &tone]); units.extend(aac.flush()); diff --git a/crates/audio/src/aac/encoder/mod.rs b/crates/audio/src/aac/encoder/mod.rs index 5a0ea5ac8..c641b790b 100644 --- a/crates/audio/src/aac/encoder/mod.rs +++ b/crates/audio/src/aac/encoder/mod.rs @@ -162,6 +162,16 @@ const HISTORY: usize = 2048; /// Bits per channel in a silent frame, rounded up: the smallest frame the encoder writes. const MIN_FRAME_BITS_PER_CHANNEL: u64 = 48; +/// The lowest and highest constant bit rate (bits per second) [`Encoder::new`] accepts for a +/// stream without ADTS headers: at least 1 kbit/s per channel and a silent frame, at most the +/// 6144 bits per channel and frame of the decoder buffer. +pub(crate) fn cbr_range(sample_rate: u32, channels: usize) -> (u32, u32) { + let (rate, channels) = (u64::from(sample_rate), channels as u64); + let silent = (MIN_FRAME_BITS_PER_CHANNEL * channels * rate).div_ceil(FRAME_LEN as u64); + let most = 6144 * channels * rate / FRAME_LEN as u64; + (u32::try_from(silent.max(1000 * channels)).unwrap_or(u32::MAX), u32::try_from(most).unwrap_or(u32::MAX)) +} + /// AAC-LC encoder. pub struct Encoder { cfg: EncoderConfig, diff --git a/crates/audio/src/aac/mod.rs b/crates/audio/src/aac/mod.rs index 297aa7972..f423a4e22 100644 --- a/crates/audio/src/aac/mod.rs +++ b/crates/audio/src/aac/mod.rs @@ -64,6 +64,8 @@ pub use config::{AdtsHeader, AudioSpecificConfig, ElementType, ProgramConfig, co #[cfg(feature = "aac-decode")] pub use decoder::Decoder; #[cfg(feature = "aac-encode")] +pub(crate) use encoder::cbr_range; +#[cfg(feature = "aac-encode")] pub use encoder::{BitrateMode, Encoder, EncoderConfig, EncoderStats}; #[cfg(feature = "aac-decode")] pub use latm::{LatmConfig, latm_config, latm_split}; diff --git a/crates/audio/src/ac3/mod.rs b/crates/audio/src/ac3/mod.rs index a8e8920d2..6bb19df8d 100644 --- a/crates/audio/src/ac3/mod.rs +++ b/crates/audio/src/ac3/mod.rs @@ -61,7 +61,7 @@ pub enum Error { pub type Result = std::result::Result; /// Data rate in kb/s by frmsizecod / 2 (Table 5.18). -const BITRATES: [u32; 19] = [32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640]; +pub(crate) const BITRATES: [u32; 19] = [32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512, 576, 640]; /// Full-bandwidth channels per acmod (Table 5.8). const NFCHANS: [usize; 8] = [2, 1, 2, 3, 3, 4, 4, 5]; diff --git a/crates/audio/src/layout.rs b/crates/audio/src/layout.rs index f3cab1b7f..d0c5ae39e 100644 --- a/crates/audio/src/layout.rs +++ b/crates/audio/src/layout.rs @@ -283,6 +283,40 @@ pub(crate) fn flac_assumed_mask(channels: usize) -> u32 { } } +/// The surround speaker a layout may name in place of `speaker`: formats with one surround pair +/// do not tell side from back, so a pair of either kind feeds it. +fn surround_twin(speaker: Speaker) -> Option { + Some(match speaker { + Speaker::SideLeft => Speaker::BackLeft, + Speaker::SideRight => Speaker::BackRight, + Speaker::BackLeft => Speaker::SideLeft, + Speaker::BackRight => Speaker::SideRight, + _ => return None, + }) +} + +/// How audio with `layout` feeds a format whose layout for the channel count is `mask`: for each +/// channel the format takes, the channel of the audio that feeds it. `order` is the format's +/// channel order over `mask` (for each of its channels, the WAV-order channel of `mask`; `None` +/// when it takes WAV order). `None` when `layout` is not the format's layout: it is unordered, or +/// has a speaker the format has not. +pub(crate) fn channel_map(mask: u32, order: Option<&[usize]>, layout: &ChannelLayout) -> Option> { + let speakers = ChannelLayout::from_mask(mask).speakers(); + if speakers.len() != usize::from(layout.channels()) { + return None; + } + let mut map = Vec::with_capacity(speakers.len()); + for j in 0..speakers.len() { + let speaker = *speakers.get(order.and_then(|o| o.get(j).copied()).unwrap_or(j))?; + let at = layout.index_of(speaker).or_else(|| surround_twin(speaker).and_then(|t| layout.index_of(t)))?; + if map.contains(&at) { + return None; + } + map.push(at); + } + Some(map) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/audio/src/lib.rs b/crates/audio/src/lib.rs index f8441a51d..1c6c73dcf 100644 --- a/crates/audio/src/lib.rs +++ b/crates/audio/src/lib.rs @@ -16,9 +16,10 @@ //! - [`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 -//! gives [`wav_float_header`]) and FLAC (feature `flac-encode`), whole-buffer or incremental into -//! any `Write + Seek`. Feature `lossless-encode` turns on both; feature `encode` turns on -//! `lossless-encode` and every codec's encoder. +//! gives [`wav_float_header`]), FLAC (feature `flac-encode`), AAC-LC and ALAC in M4A +//! (`aac-encode`, `alac-encode`) and MP3, AC-3 and E-AC-3 streams (`mp3-encode`, `ac3-encode`, +//! `eac3-encode`), whole-buffer or incremental into any `Write + Seek`. Feature +//! `lossless-encode` turns on WAV, AIFF and FLAC; feature `encode` turns on every format and encoder. //! - [`wav_pcm`], [`aiff_pcm`] (feature `decode-native`) and [`pcm_wav_file`], [`pcm_wav_header`], //! [`pcm_aiff_file`], [`pcm_aiff_header`] (feature `pcm-encode`): the integer PCM of a WAV or AIFF //! file bit for bit, and minimal files around such PCM, for essence embedded in other containers. @@ -54,7 +55,15 @@ mod read; mod track; #[cfg(any(feature = "decode-native", feature = "pcm-encode"))] mod wav; -#[cfg(any(feature = "pcm-encode", feature = "flac-encode"))] +#[cfg(any( + feature = "pcm-encode", + feature = "flac-encode", + feature = "aac-encode", + feature = "alac-encode", + feature = "mp3-encode", + feature = "ac3-encode", + feature = "eac3-encode" +))] mod write; /// The FLAC packet encoder (RFC 9639), for a caller that muxes FLAC itself (into MP4, Matroska or @@ -100,7 +109,15 @@ pub use read::{RawPcm, aiff_pcm, wav_pcm}; pub use track::{LOOKAHEAD_PACKETS, TrackDecoder}; #[cfg(feature = "pcm-encode")] pub use write::wav_float_header; -#[cfg(any(feature = "pcm-encode", feature = "flac-encode"))] +#[cfg(any( + feature = "pcm-encode", + feature = "flac-encode", + feature = "aac-encode", + feature = "alac-encode", + feature = "mp3-encode", + feature = "ac3-encode", + feature = "eac3-encode" +))] pub use write::{ENCODE_EXTENSIONS, FileEncoder, encode, encode_format_for}; #[cfg(feature = "pcm-encode")] pub use write::{pcm_aiff_file, pcm_aiff_header, pcm_wav_file, pcm_wav_header}; @@ -215,7 +232,8 @@ impl From for AudioError { } /// 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`]. +/// `flac-encode`, AAC `aac-encode`, ALAC `alac-encode`, MP3 `mp3-encode`, AC-3 `ac3-encode` and +/// E-AC-3 `eac3-encode`; asking for one that is not built returns [`AudioError::Unsupported`]. #[non_exhaustive] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -226,20 +244,46 @@ pub enum EncodeFormat { Aiff, /// FLAC (integer depths). Flac, + /// AAC-LC in an M4A (MP4) file, 1–8 channels at one of the 13 AAC sample rates. The edit list + /// skips the encoder's priming and ends at the last frame, so the file decodes to the length + /// that was written. + Aac, + /// Apple Lossless in an M4A (MP4) file (integer depths), 1–8 channels. + Alac, + /// An MP3 stream: mono or stereo at 32, 44.1 or 48 kHz, or 16, 22.05 or 24 kHz. + Mp3, + /// An AC-3 stream: up to 5.1 at 32, 44.1 or 48 kHz. + Ac3, + /// An E-AC-3 stream: up to 7.1 at 32, 44.1 or 48 kHz. + Eac3, } impl EncodeFormat { /// Every format, in the order menus list them. Whether [`encode`] writes one depends on its - /// feature. - pub const ALL: &[EncodeFormat] = &[EncodeFormat::Wav, EncodeFormat::Aiff, EncodeFormat::Flac]; + /// feature ([`EncodeFormat::is_built`]). + pub const ALL: &[EncodeFormat] = &[ + EncodeFormat::Wav, + EncodeFormat::Aiff, + EncodeFormat::Flac, + EncodeFormat::Alac, + EncodeFormat::Aac, + EncodeFormat::Mp3, + EncodeFormat::Ac3, + EncodeFormat::Eac3, + ]; /// The extension a new file of this format gets, lower case and without the dot: `"wav"`, - /// `"aif"`, `"flac"`. [`encode_format_for`] maps it back. + /// `"aif"`, `"flac"`, `"m4a"` (AAC and ALAC), `"mp3"`, `"ac3"`, `"eac3"`. + /// [`encode_format_for`] maps it back, `"m4a"` to AAC. pub fn extension(self) -> &'static str { match self { EncodeFormat::Wav => "wav", EncodeFormat::Aiff => "aif", EncodeFormat::Flac => "flac", + EncodeFormat::Aac | EncodeFormat::Alac => "m4a", + EncodeFormat::Mp3 => "mp3", + EncodeFormat::Ac3 => "ac3", + EncodeFormat::Eac3 => "eac3", } } @@ -249,6 +293,10 @@ impl EncodeFormat { EncodeFormat::Wav => "audio/wav", EncodeFormat::Aiff => "audio/aiff", EncodeFormat::Flac => "audio/flac", + EncodeFormat::Aac | EncodeFormat::Alac => "audio/mp4", + EncodeFormat::Mp3 => "audio/mpeg", + EncodeFormat::Ac3 => "audio/ac3", + EncodeFormat::Eac3 => "audio/eac3", } } @@ -258,8 +306,154 @@ impl EncodeFormat { EncodeFormat::Wav => Container::Wav, EncodeFormat::Aiff => Container::Aiff, EncodeFormat::Flac => Container::Flac, + EncodeFormat::Aac | EncodeFormat::Alac => Container::Mp4, + EncodeFormat::Mp3 => Container::MpegAudio, + EncodeFormat::Ac3 | EncodeFormat::Eac3 => Container::Ac3, } } + + /// The codec of the audio this format writes. + pub fn codec(self) -> CodecId { + match self { + EncodeFormat::Wav | EncodeFormat::Aiff => CodecId::Pcm, + EncodeFormat::Flac => CodecId::Flac, + EncodeFormat::Aac => CodecId::Aac, + EncodeFormat::Alac => CodecId::Alac, + EncodeFormat::Mp3 => CodecId::Mp3, + EncodeFormat::Ac3 => CodecId::Ac3, + EncodeFormat::Eac3 => CodecId::Eac3, + } + } + + /// The feature that builds this format's writer. + pub fn feature(self) -> &'static str { + match self { + EncodeFormat::Wav | EncodeFormat::Aiff => "pcm-encode", + EncodeFormat::Flac => "flac-encode", + EncodeFormat::Aac => "aac-encode", + EncodeFormat::Alac => "alac-encode", + EncodeFormat::Mp3 => "mp3-encode", + EncodeFormat::Ac3 => "ac3-encode", + EncodeFormat::Eac3 => "eac3-encode", + } + } + + /// Whether [`encode`] and [`FileEncoder`] write this format in this build (its + /// [`feature`](Self::feature) is on): what to filter [`EncodeFormat::ALL`] by for a menu. + pub fn is_built(self) -> bool { + match self { + EncodeFormat::Wav | EncodeFormat::Aiff => cfg!(feature = "pcm-encode"), + EncodeFormat::Flac => cfg!(feature = "flac-encode"), + EncodeFormat::Aac => cfg!(feature = "aac-encode"), + EncodeFormat::Alac => cfg!(feature = "alac-encode"), + EncodeFormat::Mp3 => cfg!(feature = "mp3-encode"), + EncodeFormat::Ac3 => cfg!(feature = "ac3-encode"), + EncodeFormat::Eac3 => cfg!(feature = "eac3-encode"), + } + } + + /// The sample rates the format takes, ascending, or `None` when it has no fixed list (WAV, + /// AIFF, FLAC and ALAC take any rate their headers can hold). + pub fn sample_rates(self) -> Option<&'static [u32]> { + match self { + EncodeFormat::Aac => Some(&[7350, 8000, 11_025, 12_000, 16_000, 22_050, 24_000, 32_000, 44_100, 48_000, 64_000, 88_200, 96_000]), + EncodeFormat::Mp3 => Some(&[16_000, 22_050, 24_000, 32_000, 44_100, 48_000]), + EncodeFormat::Ac3 | EncodeFormat::Eac3 => Some(&[32_000, 44_100, 48_000]), + EncodeFormat::Wav | EncodeFormat::Aiff | EncodeFormat::Flac | EncodeFormat::Alac => None, + } + } + + /// The most channels a file of this format holds. A WAV's frame size is a 16-bit field, so it + /// holds 65535 / bytes per sample: 32767 at 16 bits (the count given here), 21845 at 24 and + /// 16383 at 32; [`EncodeOptions::check`] answers for the depth chosen. + pub fn max_channels(self) -> u16 { + match self { + EncodeFormat::Wav | EncodeFormat::Aiff => 32_767, + EncodeFormat::Flac | EncodeFormat::Aac | EncodeFormat::Alac | EncodeFormat::Eac3 => 8, + EncodeFormat::Ac3 => 6, + EncodeFormat::Mp3 => 2, + } + } + + /// The speaker layouts of a format that has a fixed set of them, as WAV masks, for a channel + /// count: one each for AAC, ALAC and MP3, one per coding mode for AC-3 and E-AC-3. Empty for a + /// count the format has not, and for WAV, AIFF and FLAC, which have no such set. + pub(crate) fn layout_masks(self, channels: u16) -> &'static [u32] { + let per_count: &[&[u32]] = match self { + // Channel configurations 1–6, the 6.1 program config element, and configuration 7 + // (7.1 with a front pair left and right of centre). + EncodeFormat::Aac => &[&[0x4], &[0x3], &[0x7], &[0x107], &[0x607], &[0x60F], &[0x70F], &[0xFF]], + // Apple's layout tags for 1–8 channels, surrounds as back speakers. + EncodeFormat::Alac => &[&[0x4], &[0x3], &[0x7], &[0x107], &[0x37], &[0x3F], &[0x13F], &[0xFF]], + EncodeFormat::Mp3 => &[&[0x4], &[0x3]], + // The coding modes 1/0 to 3/2 with and without an LFE, by channel count. + EncodeFormat::Ac3 => &[&[0x4], &[0x3, 0xC], &[0x7, 0x103, 0xB], &[0x107, 0x603, 0xF, 0x10B], &[0x607, 0x10F, 0x60B], &[0x60F]], + // The same, and 7.1 with backs and sides. + EncodeFormat::Eac3 => { + &[&[0x4], &[0x3, 0xC], &[0x7, 0x103, 0xB], &[0x107, 0x603, 0xF, 0x10B], &[0x607, 0x10F, 0x60B], &[0x60F], &[], &[0x63F]] + } + EncodeFormat::Wav | EncodeFormat::Aiff | EncodeFormat::Flac => &[], + }; + usize::from(channels).checked_sub(1).and_then(|i| per_count.get(i)).copied().unwrap_or_default() + } + + /// The speaker layouts a format with a fixed set of them has for `channels` channels: one for + /// AAC, ALAC and MP3, one per coding mode for AC-3 and E-AC-3; empty when the format cannot + /// hold that many channels. `None` for WAV, AIFF and FLAC, which take any layout. Audio in + /// another layout has to be rearranged into one of these before it is written. + /// + /// ``` + /// use craft_audio::{ChannelLayout, EncodeFormat}; + /// + /// assert_eq!(EncodeFormat::Aac.fixed_layouts(6), Some(vec![ChannelLayout::SURROUND_5_1_SIDE])); + /// assert_eq!(EncodeFormat::Mp3.fixed_layouts(6), Some(vec![])); + /// assert_eq!(EncodeFormat::Wav.fixed_layouts(6), None); + /// ``` + pub fn fixed_layouts(self, channels: u16) -> Option> { + match self { + EncodeFormat::Wav | EncodeFormat::Aiff | EncodeFormat::Flac => None, + _ => Some(self.layout_masks(channels).iter().map(|&mask| ChannelLayout::from_mask(mask)).collect()), + } + } + + /// Whether the format holds audio with `layout`: for WAV, AIFF and FLAC any layout of up to + /// [`max_channels`](Self::max_channels) channels, for the others one of + /// [`fixed_layouts`](Self::fixed_layouts), where side and back surrounds stand for each other + /// in a layout with one surround pair. + pub fn supports_layout(self, layout: &ChannelLayout) -> bool { + match self { + EncodeFormat::Wav | EncodeFormat::Aiff | EncodeFormat::Flac => (1..=self.max_channels()).contains(&layout.channels()), + _ => self.layout_masks(layout.channels()).iter().any(|&mask| layout::channel_map(mask, None, layout).is_some()), + } + } + + /// Whether the format takes [`RateControl::Quality`]: AAC. The other lossy formats are + /// written at a constant bit rate only. + pub fn supports_quality(self) -> bool { + self == EncodeFormat::Aac + } + + /// Whether the format loses audio: AAC, MP3, AC-3 and E-AC-3. These take + /// [`EncodeOptions::rate_control`] and ignore the bit depth and dither. + pub fn is_lossy(self) -> bool { + matches!(self, EncodeFormat::Aac | EncodeFormat::Mp3 | EncodeFormat::Ac3 | EncodeFormat::Eac3) + } +} + +/// How a lossy format spends bits ([`EncodeOptions::rate_control`]). +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum RateControl { + /// A constant bit rate in bits per second, for all channels together. A format with a fixed + /// set of rates (MP3, AC-3) uses the highest one not above it; a rate outside what the + /// stream can carry is moved to the nearest one inside. AAC is written at 6 kbit/s or a + /// quarter of the sample rate per channel at least: from about half of that down its encoder + /// codes silence. + Bitrate(u32), + /// A constant quality from 0.0 (smallest) to 1.0 (best), at whatever bit rate each frame + /// needs. AAC only; values outside the range are clamped, and NaN is an error. + Quality(f32), } /// Stored sample representation. @@ -418,9 +612,9 @@ pub enum BitDepth { pub struct EncodeOptions { /// Output file format. pub format: EncodeFormat, - /// Sample depth to write. + /// Sample depth to write (ignored by the lossy formats). pub bit_depth: BitDepth, - /// Add TPDF dither when quantizing to an integer depth. + /// Add TPDF dither when quantizing to an integer depth (ignored by the lossy formats). pub dither: bool, /// Write a BWF `bext` chunk (WAV only; ignored for other formats). pub bwf: Option, @@ -431,15 +625,27 @@ pub struct EncodeOptions { /// the count (they read 5 and 6 channels with side surrounds and 7 as 6.1 with sides, so the /// usual layouts for those counts get one; see the README's limits); AIFF has a fixed channel /// order and ignores it. It must have as many channels as the audio. + /// + /// AAC, ALAC, MP3, AC-3 and E-AC-3 store channels in their own order, so here the layout says + /// which speaker each channel of the audio (in WAV order) is, and the channels are reordered + /// to match. Each of these formats has a fixed set of layouts (AAC and ALAC one per channel + /// count, AC-3 one per coding mode); a layout outside it is [`AudioError::Unsupported`]. Side + /// and back surrounds are taken as the same pair where the format has only one. pub layout: Option, /// Write WAV in the RF64 layout even below 4 GiB (WAV only; ignored for other formats). pub force_rf64: bool, + /// The bit rate or quality of a lossy format (ignored by the others). `None` is a constant + /// 64 kbit/s per channel for AAC, 96 kbit/s per channel for MP3, and 96 kbit/s per + /// full-bandwidth channel (the LFE not counted) for AC-3 and E-AC-3, moved to the nearest rate + /// the stream can carry. + pub rate_control: Option, } impl EncodeOptions { - /// Options for `format`: 24-bit, no dither, no BWF chunk, the usual layout. + /// Options for `format`: 24-bit, no dither, no BWF chunk, the usual layout, the format's + /// default bit rate. pub fn new(format: EncodeFormat) -> Self { - Self { format, bit_depth: BitDepth::Int24, dither: false, bwf: None, layout: None, force_rf64: false } + Self { format, bit_depth: BitDepth::Int24, dither: false, bwf: None, layout: None, force_rf64: false, rate_control: None } } /// Sets `format`. @@ -477,6 +683,18 @@ impl EncodeOptions { self.force_rf64 = force_rf64; self } + + /// Sets `rate_control` to a constant bit rate in bits per second ([`RateControl::Bitrate`]). + pub fn with_bitrate(mut self, bits_per_second: u32) -> Self { + self.rate_control = Some(RateControl::Bitrate(bits_per_second)); + self + } + + /// Sets `rate_control` to a constant quality from 0.0 to 1.0 ([`RateControl::Quality`]). + pub fn with_quality(mut self, quality: f32) -> Self { + self.rate_control = Some(RateControl::Quality(quality)); + self + } } impl Default for EncodeOptions { diff --git a/crates/audio/src/write/coded.rs b/crates/audio/src/write/coded.rs new file mode 100644 index 000000000..0d0dd626f --- /dev/null +++ b/crates/audio/src/write/coded.rs @@ -0,0 +1,506 @@ +//! The coded formats of [`FileEncoder`](crate::FileEncoder): AAC-LC and ALAC in M4A (through +//! craft-media's MP4 muxer) and MP3, AC-3 and E-AC-3 as raw streams. [`Writer`] maps the caller's +//! WAV-order channels onto the encoder's order, drives the packet encoder, and for M4A records the +//! encoder's priming and the exact length in the edit list. + +// Each codec is its own feature, and so are the two sinks: with only some of them built, the +// other halves of the shared code are unused. +#![cfg_attr( + not(all(feature = "aac-encode", feature = "alac-encode", feature = "mp3-encode", feature = "ac3-encode", feature = "eac3-encode")), + allow(dead_code, unused_imports, unused_variables) +)] + +use super::failed_write_error; +use super::pcm::validate_layout; +use crate::layout::channel_map; +use crate::{AudioBuffer, AudioError, ChannelLayout, EncodeFormat, EncodeOptions, RateControl, Result}; +use std::io::{Cursor, Seek, Write}; + +/// Frames handed to the encoder at a time, so its buffers and the packets waiting to be written +/// stay small however long a block is. Also the frames of an ALAC packet. +const CHUNK: usize = 4096; + +/// Bits per second per channel when no rate is asked for: AAC. +const AAC_DEFAULT_BPS_PER_CHANNEL: u32 = 64_000; +/// Kilobits per second per channel when no rate is asked for: MP3, and AC-3 and E-AC-3 (per +/// full-bandwidth channel). +const DEFAULT_KBPS_PER_CHANNEL: u32 = 96; + +/// The speakers of `mask` as labels: `"L R C LFE Ls Rs"`. +fn labels(mask: u32) -> String { + ChannelLayout::from_mask(mask).speakers().iter().map(|s| s.label()).collect::>().join(" ") +} + +/// [`channel_map`] for a format with one layout per channel count ([`EncodeFormat::layout_masks`]), +/// or why the audio does not fit. +fn fixed_map(format: EncodeFormat, name: &str, order: Option<&[usize]>, layout: &ChannelLayout) -> Result> { + let Some(&mask) = format.layout_masks(layout.channels()).first() else { + return Err(AudioError::Unsupported(format!("{name} cannot hold {} channels", layout.channels()))); + }; + channel_map(mask, order, layout).ok_or_else(|| { + AudioError::Unsupported(format!("{name} holds {} channels as {}; the audio is {}", layout.channels(), labels(mask), given(layout))) + }) +} + +/// The speakers of the caller's layout, for an error message. +fn given(layout: &ChannelLayout) -> String { + layout.mask().map_or_else(|| "without speaker assignment".into(), labels) +} + +/// The encoder's channel order over AAC's layout for the count: [`crate::aac::aac_order`], and for 8 channels +/// configuration 7's C, Lc Rc, L R, Ls Rs, LFE over the WAV order L R C LFE Ls Rs Lc Rc. +#[cfg(feature = "aac-encode")] +fn aac_order(channels: usize) -> Option<&'static [usize]> { + if channels == 8 { Some(&[2, 6, 7, 0, 1, 4, 5, 3]) } else { crate::aac::aac_order(channels) } +} + +/// The lowest AAC bit rate the writer uses, per channel: 6 kbit/s, and a quarter of the sample +/// rate (256 bits a frame). The encoder accepts less, and from about half of this down codes +/// silence; a file of silence is no answer to a low bit rate (or to kbit/s given as bit/s). +#[cfg(feature = "aac-encode")] +fn aac_floor_per_channel(sample_rate: u32) -> u32 { + (sample_rate / 4).max(6000) +} + +/// The AAC rate control for `rate`: a bit rate is kept within what the stream can carry, a +/// quality of 0–1 spans the encoder's VBR range. +#[cfg(feature = "aac-encode")] +fn aac_mode(rate: Option, sample_rate: u32, channels: usize) -> crate::aac::BitrateMode { + use crate::aac::BitrateMode; + let (lo, hi) = crate::aac::cbr_range(sample_rate, channels); + let lo = lo.max(aac_floor_per_channel(sample_rate).saturating_mul(channels as u32)).min(hi.max(lo)); + // An empty range (a rate or channel count the encoder refuses anyway) leaves the rate alone. + let fit = |bps: u32| if lo <= hi { bps.clamp(lo, hi) } else { bps }; + match rate { + None => BitrateMode::Cbr(fit(AAC_DEFAULT_BPS_PER_CHANNEL.saturating_mul(channels as u32))), + Some(RateControl::Bitrate(bps)) => BitrateMode::Cbr(fit(bps)), + // NaN stays NaN, which the encoder refuses. + Some(RateControl::Quality(q)) => BitrateMode::Vbr(1.0 + 4.0 * q.clamp(0.0, 1.0)), + } +} + +/// The bit rate in kbit/s for a codec with constant bit rates only. +fn constant_kbps(name: &str, rate: Option, default: u32) -> Result { + match rate { + None => Ok(default), + Some(RateControl::Bitrate(bps)) => Ok(bps / 1000), + Some(RateControl::Quality(_)) => { + Err(AudioError::Unsupported(format!("{name} is encoded at a constant bit rate; set a bit rate, not a quality"))) + } + } +} + +/// The AC-3 or E-AC-3 settings for audio with `layout`, and the channel map onto them: the coding +/// mode whose speakers the layout names, at the nearest bit rate the stream can carry. +#[cfg(any(feature = "ac3-encode", feature = "eac3-encode"))] +fn ac3_config( + codec: crate::ac3::encoder::Codec, + sample_rate: u32, + layout: &ChannelLayout, + rate: Option, +) -> Result<(crate::ac3::encoder::Config, Vec)> { + use crate::ac3::encoder::{Codec, CodingMode, Config}; + use CodingMode::*; + let name = if codec == Codec::Ac3 { "AC-3" } else { "E-AC-3" }; + let modes = [Mono, Stereo, ThreeFront, StereoSurround, ThreeFrontSurround, StereoTwoSurround, ThreeFrontTwoSurround, Surround71]; + let found = modes.iter().flat_map(|&mode| [false, true].map(|lfe| (mode, lfe))).find_map(|(mode, lfe)| { + // 7.1 is an E-AC-3 programme with an LFE; the encoder has no other. + if mode == Surround71 && (codec != Codec::Eac3 || !lfe) { + return None; + } + let config = Config { codec, sample_rate, bitrate_kbps: 0, mode, lfe }; + let map = channel_map(config.channel_layout().mask()?, None, layout)?; + Some((config, map)) + }); + let Some((mut config, map)) = found else { + return Err(AudioError::Unsupported(format!("{name} has no coding mode for {} channels that are {}", layout.channels(), given(layout)))); + }; + let full = config.mode.channels() as u32; + let want = constant_kbps(name, rate, DEFAULT_KBPS_PER_CHANNEL * full)?; + // What `Config::validate` asks of the rate: enough for the channels, and for AC-3 an entry of + // the frame size table, for E-AC-3 a syncframe its 11-bit word count can hold. + let least = if config.mode == Surround71 { 384 } else { 32 * full }; + config.bitrate_kbps = match codec { + Codec::Ac3 => { + let floor = crate::ac3::BITRATES.iter().copied().find(|&r| r >= least).unwrap_or(least); + crate::ac3::BITRATES.iter().rev().copied().find(|&r| r <= want).filter(|&r| r >= floor).unwrap_or(floor) + } + Codec::Eac3 => { + let most = if config.mode == Surround71 { 1024 } else { (2048 * u64::from(sample_rate) / 96_000).min(1024) as u32 }; + want.clamp(least, most.max(least)) + } + }; + Ok((config, map)) +} + +/// One encoded packet and the frames it decodes to. +struct Packet { + data: Vec, + frames: u32, +} + +/// A packet encoder of any of the codecs, taking planar `f32` in its own channel order. +enum Codec { + #[cfg(feature = "aac-encode")] + Aac(Box), + #[cfg(feature = "alac-encode")] + Alac { + enc: Box, + quantizers: Vec, + /// Quantized samples of the packet being filled, per channel. + pending: Vec>, + }, + #[cfg(feature = "mp3-encode")] + Mp3(Box), + #[cfg(any(feature = "ac3-encode", feature = "eac3-encode"))] + Ac3 { + enc: Box, + /// The block with every sample finite and within ±1, as the encoder requires. + planes: Vec>, + }, +} + +impl Codec { + /// Encodes `planes` (one slice per encoder channel, equally long, at most [`CHUNK`] frames) + /// and appends the packets completed. + fn push(&mut self, planes: &[&[f32]], out: &mut Vec) -> Result<()> { + match self { + #[cfg(feature = "aac-encode")] + Codec::Aac(enc) => out.extend(enc.encode(planes).into_iter().map(|data| Packet { data, frames: crate::aac::FRAME_LEN as u32 })), + #[cfg(feature = "alac-encode")] + Codec::Alac { enc, quantizers, pending } => { + let frames = planes.first().map_or(0, |p| p.len()); + let mut at = 0; + while at < frames { + let filled = pending.first().map_or(0, Vec::len); + let end = frames.min(at + CHUNK.saturating_sub(filled).max(1)); + for ((dst, q), src) in pending.iter_mut().zip(quantizers.iter_mut()).zip(planes) { + dst.extend(src.get(at..end).unwrap_or_default().iter().map(|&s| q.quantize(s))); + } + at = end; + if pending.first().is_some_and(|p| p.len() >= CHUNK) { + alac_packet(enc, pending, out)?; + } + } + } + #[cfg(feature = "mp3-encode")] + Codec::Mp3(enc) => { + let data = enc.encode(planes); + if !data.is_empty() { + out.push(Packet { data, frames: 0 }); + } + } + #[cfg(any(feature = "ac3-encode", feature = "eac3-encode"))] + Codec::Ac3 { enc, planes: clean } => { + for (dst, src) in clean.iter_mut().zip(planes) { + dst.clear(); + dst.extend(src.iter().map(|&s| if s.is_nan() { 0.0 } else { s.clamp(-1.0, 1.0) })); + } + let refs: Vec<&[f32]> = clean.iter().map(Vec::as_slice).collect(); + out.extend(enc.push(&refs)?.into_iter().map(|p| Packet { data: p.data, frames: p.samples })); + } + } + Ok(()) + } + + /// Encodes what the encoder still holds and appends the last packets. + fn finish(&mut self, out: &mut Vec) -> Result<()> { + match self { + #[cfg(feature = "aac-encode")] + Codec::Aac(enc) => out.extend(enc.flush().into_iter().map(|data| Packet { data, frames: crate::aac::FRAME_LEN as u32 })), + #[cfg(feature = "alac-encode")] + Codec::Alac { enc, pending, .. } => { + if pending.first().is_some_and(|p| !p.is_empty()) { + alac_packet(enc, pending, out)?; + } + } + #[cfg(feature = "mp3-encode")] + Codec::Mp3(enc) => { + let data = enc.flush(); + if !data.is_empty() { + out.push(Packet { data, frames: 0 }); + } + } + #[cfg(any(feature = "ac3-encode", feature = "eac3-encode"))] + Codec::Ac3 { enc, .. } => out.extend(enc.finish()?.packets.into_iter().map(|p| Packet { data: p.data, frames: p.samples })), + } + Ok(()) + } + + /// Decoded frames before the first frame of the audio. + fn priming(&self) -> u32 { + match self { + #[cfg(feature = "aac-encode")] + Codec::Aac(enc) => enc.priming_samples(), + #[allow(unreachable_patterns)] + _ => 0, + } + } + + /// The MP4 sample entry of the stream as encoded so far (`bytes` in packets of `frames` + /// frames in all): the configuration a decoder needs, with the average bit rate. + #[cfg(any(feature = "aac-encode", feature = "alac-encode"))] + fn sample_entry(&self, sample_rate: u32, channels: u32, bytes: u64, frames: u64) -> Option { + use craft_media::isobmff::{CodecConfig, FourCc, SampleEntry}; + match self { + #[cfg(feature = "aac-encode")] + Codec::Aac(enc) => { + let mut entry = SampleEntry::aac(enc.audio_specific_config(), channels, sample_rate); + if let CodecConfig::Aac(config) = &mut entry.codec + && frames > 0 + { + let bps = u128::from(bytes) * 8 * u128::from(sample_rate) / u128::from(frames); + config.avg_bitrate = u32::try_from(bps).unwrap_or(u32::MAX); + } + Some(entry) + } + #[cfg(feature = "alac-encode")] + Codec::Alac { enc, .. } => { + let bits = u16::from(enc.encoder_config().bit_depth); + Some(SampleEntry::audio(FourCc(*b"alac"), CodecConfig::Alac { cookie: enc.cookie() }, channels, f64::from(sample_rate), bits)) + } + #[allow(unreachable_patterns)] + _ => None, + } + } +} + +/// Encodes the pending samples as one ALAC packet and empties them. +#[cfg(feature = "alac-encode")] +fn alac_packet(enc: &mut crate::alac::Encoder, pending: &mut [Vec], out: &mut Vec) -> Result<()> { + let refs: Vec<&[i32]> = pending.iter().map(Vec::as_slice).collect(); + let frames = refs.first().map_or(0, |p| p.len()) as u32; + let data = enc.encode(&refs)?; + pending.iter_mut().for_each(Vec::clear); + out.push(Packet { data, frames }); + Ok(()) +} + +/// A muxer's error as this crate's: a failed write stays an I/O error. +#[cfg(any(feature = "aac-encode", feature = "alac-encode"))] +fn mux_error(e: craft_media::isobmff::Error) -> AudioError { + match e { + craft_media::isobmff::Error::Io(e) => AudioError::Io(e), + e => AudioError::Encode(format!("M4A: {e}")), + } +} + +/// An M4A file being written: one audio track whose samples are the packets. +/// +/// The last packet is held back until the length is known: its duration is cut to the audio's +/// last frame, so the sample durations add up to the priming plus the audio, and the edit that +/// skips the priming ends where the audio does. The movie timescale is the sample rate, which +/// keeps that edit exact. +#[cfg(any(feature = "aac-encode", feature = "alac-encode"))] +struct M4a { + mux: craft_media::isobmff::Mp4Writer, + /// The track, added with the first packet: a file with no audio gets a track without an edit. + track: Option, + entry: craft_media::isobmff::SampleEntry, + sample_rate: u32, + channels: u32, + priming: u32, + held: Option, + /// Frames and bytes of the packets written, the held one not counted. + frames: u64, + bytes: u64, +} + +#[cfg(any(feature = "aac-encode", feature = "alac-encode"))] +impl M4a { + fn new(out: W, codec: &Codec, sample_rate: u32, channels: u32) -> Result { + use craft_media::isobmff::{Brand, Mp4Writer, WriterOptions}; + let entry = codec.sample_entry(sample_rate, channels, 0, 0).ok_or_else(|| AudioError::Unsupported("M4A holds AAC and ALAC".into()))?; + let mut options = WriterOptions::new(Brand::Mp4); + options.movie_timescale = sample_rate; + let mux = Mp4Writer::new(out, options).map_err(mux_error)?; + Ok(M4a { mux, track: None, entry, sample_rate, channels, priming: codec.priming(), held: None, frames: 0, bytes: 0 }) + } + + fn track(&mut self, has_audio: bool) -> Result { + if let Some(track) = self.track { + return Ok(track); + } + let mut config = craft_media::isobmff::TrackConfig::new(self.entry.clone(), self.sample_rate); + if has_audio && self.priming > 0 { + config.media_start = Some(i64::from(self.priming)); + // AAC's overlap: a packet decodes right only after the one before it. Apple's + // decoders take the priming from the edit only when the track says so. + config.roll_distance = Some(-1); + } + let track = self.mux.add_track(config).map_err(mux_error)?; + self.track = Some(track); + Ok(track) + } + + fn sample(&mut self, packet: &Packet, duration: u32) -> Result<()> { + let track = self.track(true)?; + let sample = craft_media::isobmff::WriteSample { data: &packet.data, duration, composition_offset: 0, is_sync: true }; + self.mux.write_sample(track, sample).map_err(mux_error)?; + self.frames += u64::from(duration); + self.bytes += packet.data.len() as u64; + Ok(()) + } + + fn write(&mut self, packet: Packet) -> Result<()> { + if let Some(held) = self.held.replace(packet) { + self.sample(&held, held.frames)?; + } + Ok(()) + } + + /// Writes the last packet, cut to the end of `audio_frames` frames of audio, and the `moov`. + fn finish(mut self, codec: &Codec, audio_frames: u64) -> Result { + if let Some(held) = self.held.take() { + let rest = (u64::from(self.priming) + audio_frames).saturating_sub(self.frames); + self.sample(&held, rest.min(u64::from(held.frames)) as u32)?; + } + let track = self.track(false)?; + // The configuration is complete only now: the bit rate, and ALAC's largest packet. + if let Some(entry) = codec.sample_entry(self.sample_rate, self.channels, self.bytes, self.frames) { + self.mux.set_sample_entry(track, entry).map_err(mux_error)?; + } + self.mux.finish().map_err(mux_error) + } +} + +/// Where the packets go. +enum Sink { + #[cfg(any(feature = "aac-encode", feature = "alac-encode"))] + M4a(Box>), + /// The packets in a row are the file. + Raw(W), +} + +/// A file of one of the coded formats written block by block. +pub(crate) struct Writer { + codec: Codec, + sink: Sink, + /// For each encoder channel, the caller's channel that feeds it. + map: Vec, + /// The constant bit rate the encoder was set to, bits per second; `None` for ALAC and for + /// AAC at a constant quality. + bitrate: Option, + frames: u64, + packets: Vec, + /// Set when a write has failed: the encoder and the sink no longer agree, so the writer + /// refuses every later call. + failed: bool, +} + +impl Writer { + pub(crate) fn new(out: W, opts: &EncodeOptions, sample_rate: u32, channels: usize) -> Result { + validate_layout(channels, sample_rate)?; + let layout = opts.layout.unwrap_or_else(|| ChannelLayout::default_for(channels as u16)); + let (codec, map, bitrate) = match opts.format { + #[cfg(feature = "aac-encode")] + EncodeFormat::Aac => { + let map = fixed_map(EncodeFormat::Aac, "AAC", aac_order(channels), &layout)?; + let mode = aac_mode(opts.rate_control, sample_rate, channels); + let config = crate::aac::EncoderConfig { mode, ..crate::aac::EncoderConfig::cbr(sample_rate, channels, 0) }; + let bitrate = match mode { + crate::aac::BitrateMode::Cbr(bps) => Some(bps), + crate::aac::BitrateMode::Vbr(_) => None, + }; + (Codec::Aac(Box::new(crate::aac::Encoder::new(config)?)), map, bitrate) + } + #[cfg(feature = "alac-encode")] + EncodeFormat::Alac => { + let count = u8::try_from(channels).unwrap_or(u8::MAX); + let map = fixed_map(EncodeFormat::Alac, "ALAC", crate::alac::alac_order(count), &layout)?; + let bits = match opts.bit_depth { + crate::BitDepth::Int16 => 16, + crate::BitDepth::Int24 => 24, + crate::BitDepth::Int32 => 32, + crate::BitDepth::Float32 => return Err(AudioError::Unsupported("ALAC cannot store 32-bit float samples".into())), + }; + let enc = crate::alac::Encoder::new(crate::alac::EncoderConfig::new(sample_rate, count, bits).with_frame_length(CHUNK as u32))?; + let quantizers = super::pcm::Quantizer::per_channel(u32::from(bits), opts.dither, channels); + (Codec::Alac { enc: Box::new(enc), quantizers, pending: vec![Vec::new(); channels] }, map, None) + } + #[cfg(feature = "mp3-encode")] + EncodeFormat::Mp3 => { + let map = fixed_map(EncodeFormat::Mp3, "MP3", None, &layout)?; + let want = constant_kbps("MP3", opts.rate_control, DEFAULT_KBPS_PER_CHANNEL * channels as u32)?; + // A sample rate MP3 has not is the encoder's to refuse. + let kbps = crate::mp3::nearest_bitrate(sample_rate, want).unwrap_or(want); + let enc = crate::mp3::Encoder::new(crate::mp3::Config::cbr(sample_rate, channels, kbps))?; + (Codec::Mp3(Box::new(enc)), map, Some(kbps.saturating_mul(1000))) + } + #[cfg(any(feature = "ac3-encode", feature = "eac3-encode"))] + EncodeFormat::Ac3 | EncodeFormat::Eac3 => { + let codec = if opts.format == EncodeFormat::Ac3 { crate::ac3::encoder::Codec::Ac3 } else { crate::ac3::encoder::Codec::Eac3 }; + let (config, map) = ac3_config(codec, sample_rate, &layout, opts.rate_control)?; + let enc = crate::ac3::encoder::Encoder::new(config)?; + (Codec::Ac3 { enc: Box::new(enc), planes: vec![Vec::new(); channels] }, map, Some(config.bitrate_kbps.saturating_mul(1000))) + } + #[allow(unreachable_patterns)] + f => return Err(AudioError::Unsupported(format!("writing {f:?}"))), + }; + let sink = match opts.format { + #[cfg(any(feature = "aac-encode", feature = "alac-encode"))] + EncodeFormat::Aac | EncodeFormat::Alac => Sink::M4a(Box::new(M4a::new(out, &codec, sample_rate, channels as u32)?)), + _ => Sink::Raw(out), + }; + Ok(Writer { codec, sink, map, bitrate, frames: 0, packets: Vec::new(), failed: false }) + } + + pub(crate) fn bitrate(&self) -> Option { + self.bitrate + } + + /// Hands the packets waiting to the sink. + fn drain(&mut self) -> Result<()> { + for packet in self.packets.drain(..) { + match &mut self.sink { + #[cfg(any(feature = "aac-encode", feature = "alac-encode"))] + Sink::M4a(m4a) => m4a.write(packet)?, + Sink::Raw(out) => out.write_all(&packet.data)?, + } + } + Ok(()) + } + + /// Appends `frames` frames of `block`, which holds every channel at least that long. + pub(crate) fn write>(&mut self, block: &[C], frames: usize) -> Result<()> { + if self.failed { + return Err(failed_write_error()); + } + for at in (0..frames).step_by(CHUNK) { + let end = frames.min(at + CHUNK); + let planes: Vec<&[f32]> = self.map.iter().map(|&c| block.get(c).and_then(|p| p.as_ref().get(at..end)).unwrap_or_default()).collect(); + let written = self.codec.push(&planes, &mut self.packets).and_then(|()| self.drain()); + if let Err(e) = written { + self.failed = true; + return Err(e); + } + } + self.frames = self.frames.saturating_add(frames as u64); + Ok(()) + } + + pub(crate) fn finish(mut self) -> Result { + if self.failed { + return Err(failed_write_error()); + } + self.codec.finish(&mut self.packets)?; + self.drain()?; + match self.sink { + #[cfg(any(feature = "aac-encode", feature = "alac-encode"))] + Sink::M4a(m4a) => m4a.finish(&self.codec, self.frames), + Sink::Raw(mut out) => { + out.flush()?; + Ok(out) + } + } + } +} + +pub(crate) fn encode(buf: &AudioBuffer, opts: &EncodeOptions) -> Result> { + let mut w = Writer::new(Cursor::new(Vec::new()), opts, buf.sample_rate, buf.channels.len())?; + w.write(&buf.channels, buf.frames())?; + Ok(w.finish()?.into_inner()) +} + +#[cfg(test)] +mod tests; diff --git a/crates/audio/src/write/coded/tests.rs b/crates/audio/src/write/coded/tests.rs new file mode 100644 index 000000000..cbdbd3375 --- /dev/null +++ b/crates/audio/src/write/coded/tests.rs @@ -0,0 +1,167 @@ +use super::*; + +use crate::Speaker; + +fn map(mask: u32, order: Option<&[usize]>, layout: u32) -> Option> { + channel_map(mask, order, &ChannelLayout::from_mask(layout)) +} + +/// Channels are matched by speaker, not by position: a layout that lists the same speakers in +/// another WAV order is reordered, and one with other speakers is refused. +#[test] +fn channels_are_mapped_by_speaker() { + // L R C LFE Ls Rs onto itself, and onto an encoder order C L R Ls Rs LFE. + assert_eq!(map(0x60F, None, 0x60F), Some(vec![0, 1, 2, 3, 4, 5])); + assert_eq!(map(0x60F, Some(&[2, 0, 1, 4, 5, 3]), 0x60F), Some(vec![2, 0, 1, 4, 5, 3])); + // Back surrounds feed a format that has one surround pair, and the other way round. + assert_eq!(map(0x60F, None, 0x3F), Some(vec![0, 1, 2, 3, 4, 5])); + assert_eq!(map(0x3F, None, 0x60F), Some(vec![0, 1, 2, 3, 4, 5])); + // 6.1 with the back centre after the pair (L R C LFE Lb Rb Cb) onto L R C LFE Cb Ls Rs. + assert_eq!(map(0x70F, None, 0x13F), Some(vec![0, 1, 2, 3, 6, 4, 5])); + // Quad has no centre; 7.1 with sides and backs has no front pair left and right of centre. + assert_eq!(map(0x107, None, 0x33), None); + assert_eq!(map(0xFF, None, 0x63F), None); + // With both pairs in the format, one pair cannot feed both. + assert_eq!(map(0x63F, None, 0xFF), None); + assert_eq!(channel_map(0x3, None, &ChannelLayout::unordered(2)), None); + assert_eq!(map(0x3, None, 0x7), None); +} + +#[cfg(feature = "aac-encode")] +#[test] +fn every_aac_channel_count_has_a_layout_and_an_order() { + for channels in 1..=8 { + let mask = EncodeFormat::Aac.layout_masks(channels as u16)[0]; + assert_eq!(mask.count_ones() as usize, channels); + let mut order = map(mask, aac_order(channels), mask).unwrap(); + order.sort_unstable(); + assert_eq!(order, (0..channels).collect::>()); + } + assert!(EncodeFormat::Aac.layout_masks(9).is_empty() && EncodeFormat::Aac.layout_masks(0).is_empty()); + // The layouts this crate's decoder reports for what the encoder writes (`track::aac_layout`), + // and configuration 7's speakers for 8 channels. + let masks: Vec = (1..=8).map(|channels| EncodeFormat::Aac.layout_masks(channels)[0]).collect(); + assert_eq!(masks, [0x4, 0x3, 0x7, 0x107, 0x607, 0x60F, 0x70F, 0xFF]); + assert_eq!( + (EncodeFormat::Mp3.layout_masks(1), EncodeFormat::Mp3.layout_masks(2), EncodeFormat::Mp3.layout_masks(3)), + (&[0x4][..], &[0x3][..], &[][..]) + ); + // Configuration 7: C, Lc Rc, L R, Ls Rs, LFE. + let wide: Vec = ChannelLayout::from_mask(0xFF).speakers(); + let named: Vec = aac_order(8).unwrap().iter().map(|&c| wide[c]).collect(); + use Speaker::*; + assert_eq!(named, [FrontCenter, FrontLeftOfCenter, FrontRightOfCenter, FrontLeft, FrontRight, BackLeft, BackRight, Lfe]); +} + +/// The range the writer keeps a bit rate in is the one the encoder accepts. +#[cfg(feature = "aac-encode")] +#[test] +fn the_aac_bit_rate_range_is_the_encoders() { + use crate::aac::{Encoder, EncoderConfig, SAMPLE_RATES}; + for &rate in &SAMPLE_RATES { + for channels in 1..=8 { + let (lo, hi) = crate::aac::cbr_range(rate, channels); + assert!(lo <= hi, "{rate} Hz, {channels} channels"); + let accepts = |bps| Encoder::new(EncoderConfig::cbr(rate, channels, bps)).is_ok(); + assert!(accepts(lo) && accepts(hi), "{rate} Hz, {channels} channels: {lo}..={hi}"); + assert!(!accepts(lo - 1) && !accepts(hi + 1), "{rate} Hz, {channels} channels: {lo}..={hi}"); + for want in [None, Some(RateControl::Bitrate(0)), Some(RateControl::Bitrate(u32::MAX))] { + let mode = aac_mode(want, rate, channels); + assert!( + Encoder::new(EncoderConfig { mode, ..EncoderConfig::cbr(rate, channels, 0) }).is_ok(), + "{rate} Hz, {channels} channels, {want:?}" + ); + } + } + } +} + +/// Every layout the AC-3 encoders have is found from its speakers, at a bit rate the encoder +/// accepts whatever rate was asked for. +#[cfg(all(feature = "ac3-encode", feature = "eac3-encode"))] +#[test] +fn ac3_settings_follow_the_layout_and_validate() { + use crate::ac3::encoder::{Codec, CodingMode}; + let layouts = [ + (0x4, CodingMode::Mono, false), + (0x3, CodingMode::Stereo, false), + (0xB, CodingMode::Stereo, true), + (0x7, CodingMode::ThreeFront, false), + (0x103, CodingMode::StereoSurround, false), + (0x107, CodingMode::ThreeFrontSurround, false), + (0x33, CodingMode::StereoTwoSurround, false), + (0x603, CodingMode::StereoTwoSurround, false), + (0x37, CodingMode::ThreeFrontTwoSurround, false), + (0x3F, CodingMode::ThreeFrontTwoSurround, true), + (0x60F, CodingMode::ThreeFrontTwoSurround, true), + ]; + for codec in [Codec::Ac3, Codec::Eac3] { + for sample_rate in [32_000, 44_100, 48_000] { + for (mask, mode, lfe) in layouts { + for want in [None, Some(RateControl::Bitrate(0)), Some(RateControl::Bitrate(200_000)), Some(RateControl::Bitrate(u32::MAX))] { + let layout = ChannelLayout::from_mask(mask); + let (config, map) = ac3_config(codec, sample_rate, &layout, want).unwrap(); + assert_eq!((config.mode, config.lfe), (mode, lfe), "{mask:#x}"); + assert_eq!(map, (0..usize::from(layout.channels())).collect::>()); + assert_eq!(config.validate(), Ok(()), "{codec:?} {sample_rate} {mask:#x} {want:?}: {} kbit/s", config.bitrate_kbps); + } + } + let seven_one = ac3_config(codec, sample_rate, &ChannelLayout::SURROUND_7_1, Some(RateControl::Bitrate(u32::MAX))); + match codec { + Codec::Ac3 => assert!(matches!(seven_one, Err(AudioError::Unsupported(_)))), + Codec::Eac3 => assert_eq!(seven_one.unwrap().0.validate(), Ok(())), + } + } + } + // The highest table rate not above the one asked for; the default is 96 kbit/s per channel. + let rate = |codec, mask, want| ac3_config(codec, 48_000, &ChannelLayout::from_mask(mask), want).unwrap().0.bitrate_kbps; + assert_eq!(rate(Codec::Ac3, 0x3, Some(RateControl::Bitrate(200_000))), 192); + assert_eq!(rate(Codec::Ac3, 0x3, None), 192); + assert_eq!(rate(Codec::Ac3, 0x60F, None), 448); + assert_eq!(rate(Codec::Ac3, 0x60F, Some(RateControl::Bitrate(1))), 160); + assert_eq!(rate(Codec::Eac3, 0x3, Some(RateControl::Bitrate(200_500))), 200); + assert!(matches!(ac3_config(Codec::Ac3, 48_000, &ChannelLayout::STEREO, Some(RateControl::Quality(0.5))), Err(AudioError::Unsupported(_)))); + assert!(matches!(ac3_config(Codec::Ac3, 48_000, &ChannelLayout::unordered(2), None), Err(AudioError::Unsupported(_)))); +} + +/// The layouts `EncodeFormat` lists are the encoders' own: ALAC's layout per channel count, and +/// the speakers of every AC-3 and E-AC-3 coding mode the encoder has. +#[cfg(all(feature = "alac-encode", feature = "ac3-encode", feature = "eac3-encode"))] +#[test] +fn the_listed_layouts_are_the_encoders() { + use crate::ac3::encoder::{Codec, CodingMode::*, Config}; + for channels in 0..=10u16 { + let alac = crate::alac::channel_mask(channels as u8); + assert_eq!(EncodeFormat::Alac.layout_masks(channels), if alac == 0 { &[][..] } else { std::slice::from_ref(&alac) }, "ALAC, {channels}"); + for (format, codec) in [(EncodeFormat::Ac3, Codec::Ac3), (EncodeFormat::Eac3, Codec::Eac3)] { + let modes = [Mono, Stereo, ThreeFront, StereoSurround, ThreeFrontSurround, StereoTwoSurround, ThreeFrontTwoSurround, Surround71]; + let mut encoder: Vec = modes + .iter() + .flat_map(|&mode| [false, true].map(|lfe| Config { codec, sample_rate: 48_000, bitrate_kbps: 640, mode, lfe })) + .filter(|config| config.validate().is_ok() && config.channels() == usize::from(channels)) + .filter_map(|config| config.channel_layout().mask()) + .collect(); + let mut listed = format.layout_masks(channels).to_vec(); + encoder.sort_unstable(); + listed.sort_unstable(); + assert_eq!(listed, encoder, "{format:?}, {channels} channels"); + for &mask in &listed { + let layout = ChannelLayout::from_mask(mask); + assert_eq!(ac3_config(codec, 48_000, &layout, None).unwrap().0.channel_layout(), layout); + } + } + } +} + +/// The sample rates `EncodeFormat` lists are the encoders'. +#[cfg(all(feature = "aac-encode", feature = "mp3-encode"))] +#[test] +fn the_listed_sample_rates_are_the_encoders() { + let sorted = |rates: &[u32]| { + let mut rates = rates.to_vec(); + rates.sort_unstable(); + rates + }; + assert_eq!(EncodeFormat::Aac.sample_rates().unwrap(), sorted(&crate::aac::SAMPLE_RATES)); + assert_eq!(EncodeFormat::Mp3.sample_rates().unwrap(), sorted(&crate::mp3::SAMPLE_RATES)); +} diff --git a/crates/audio/src/write/mod.rs b/crates/audio/src/write/mod.rs index 2355a7b05..0de55c394 100644 --- a/crates/audio/src/write/mod.rs +++ b/crates/audio/src/write/mod.rs @@ -1,8 +1,11 @@ -//! Encoding: WAV/BWF/RF64 and AIFF/AIFF-C writers (`pcm-encode`) and the FLAC encoder -//! (`flac-encode`), whole-buffer and streaming. +//! Encoding: WAV/BWF/RF64 and AIFF/AIFF-C writers (`pcm-encode`), the FLAC encoder +//! (`flac-encode`) and the coded formats (AAC and ALAC in M4A, MP3, AC-3 and E-AC-3 streams, each +//! with its codec's feature), whole-buffer and streaming. #[cfg(feature = "pcm-encode")] mod aiff; +#[cfg(any(feature = "aac-encode", feature = "alac-encode", feature = "mp3-encode", feature = "ac3-encode", feature = "eac3-encode"))] +mod coded; #[cfg(feature = "flac-encode")] pub(crate) mod flac; mod pcm; @@ -16,9 +19,14 @@ use crate::{AudioBuffer, AudioError, ChannelLayout, EncodeFormat, EncodeOptions, pub use raw::{pcm_aiff_file, pcm_aiff_header, pcm_wav_file, pcm_wav_header}; use std::io::{Seek, Write}; -/// Encode `buf` as WAV/BWF, AIFF/AIFF-C or FLAC. Integer depths clamp samples to [-1, 1]; float +/// Encode `buf` as the file `opts.format` names. Integer depths clamp samples to [-1, 1]; float /// keeps values above full scale. `opts.layout`, when set, must have `buf`'s channel count. /// +/// The lossy formats take `opts.rate_control`. AAC and ALAC are written as M4A, which records the +/// encoder's priming and the exact length, so the file decodes to `buf`'s frames. MP3, AC-3 and +/// E-AC-3 are raw streams, which have no place for that: they decode with the codec's delay in +/// front and padded to whole frames. +/// /// ``` /// # #[cfg(feature = "pcm-encode")] { /// use craft_audio::{AudioBuffer, BitDepth, EncodeFormat, EncodeOptions, encode}; @@ -32,6 +40,7 @@ use std::io::{Seek, Write}; pub fn encode(buf: &AudioBuffer, opts: &EncodeOptions) -> Result> { check_built(opts.format)?; // Checked for every format; WAV and FLAC record it. + #[cfg_attr(not(any(feature = "pcm-encode", feature = "flac-encode")), allow(unused_variables))] let mask = opts.layout.map(|l| wav_mask(&l, buf.channels.len())).transpose()?; match opts.format { #[cfg(feature = "pcm-encode")] @@ -40,6 +49,8 @@ pub fn encode(buf: &AudioBuffer, opts: &EncodeOptions) -> Result> { EncodeFormat::Aiff => aiff::encode(buf, opts.bit_depth, opts.dither), #[cfg(feature = "flac-encode")] EncodeFormat::Flac => flac::encode(buf, opts.bit_depth, opts.dither, mask), + #[cfg(any(feature = "aac-encode", feature = "alac-encode", feature = "mp3-encode", feature = "ac3-encode", feature = "eac3-encode"))] + EncodeFormat::Aac | EncodeFormat::Alac | EncodeFormat::Mp3 | EncodeFormat::Ac3 | EncodeFormat::Eac3 => coded::encode(buf, opts), #[allow(unreachable_patterns)] f => Err(AudioError::Unsupported(format!("writing {f:?}"))), } @@ -47,31 +58,48 @@ pub fn encode(buf: &AudioBuffer, opts: &EncodeOptions) -> Result> { /// Refuses a format whose writer is not built, before anything else about the request is checked. fn check_built(format: EncodeFormat) -> Result<()> { - let (built, feature) = - if format == EncodeFormat::Flac { (cfg!(feature = "flac-encode"), "flac-encode") } else { (cfg!(feature = "pcm-encode"), "pcm-encode") }; - if built { Ok(()) } else { Err(AudioError::Unsupported(format!("writing {format:?} needs the `{feature}` feature"))) } + if format.is_built() { Ok(()) } else { Err(AudioError::Unsupported(format!("writing {format:?} needs the `{}` feature", format.feature()))) } } -/// The file extensions of the formats [`encode`] can write (those whose features are on). -#[cfg(all(feature = "pcm-encode", feature = "flac-encode"))] -pub const ENCODE_EXTENSIONS: &[&str] = &["wav", "wave", "bwf", "aif", "aiff", "aifc", "flac"]; -/// The file extensions of the formats [`encode`] can write (those whose features are on). -#[cfg(all(feature = "pcm-encode", not(feature = "flac-encode")))] -pub const ENCODE_EXTENSIONS: &[&str] = &["wav", "wave", "bwf", "aif", "aiff", "aifc"]; -/// The file extensions of the formats [`encode`] can write (those whose features are on). -#[cfg(all(feature = "flac-encode", not(feature = "pcm-encode")))] -pub const ENCODE_EXTENSIONS: &[&str] = &["flac"]; +/// Every extension [`ENCODE_EXTENSIONS`] can list, and how many of them the features built give. +const fn extensions() -> ([&'static str; 12], usize) { + let mut all = [""; 12]; + let mut n = 0; + macro_rules! add { + ($built:expr, $($extension:literal),+) => { + if $built { + $( + all[n] = $extension; + n += 1; + )+ + } + }; + } + add!(cfg!(feature = "pcm-encode"), "wav", "wave", "bwf", "aif", "aiff", "aifc"); + add!(cfg!(feature = "flac-encode"), "flac"); + add!(cfg!(any(feature = "aac-encode", feature = "alac-encode")), "m4a"); + add!(cfg!(feature = "mp3-encode"), "mp3"); + add!(cfg!(feature = "ac3-encode"), "ac3"); + add!(cfg!(feature = "eac3-encode"), "eac3", "ec3"); + (all, n) +} -/// The format [`encode`] writes for a file extension or format name (`wav`, `aiff`, `flac`, -/// case-insensitive, with or without the dot), or `None` when it cannot write it. `aifc` is -/// AIFF: float AIFF is written as AIFF-C. +const EXTENSIONS: ([&str; 12], usize) = extensions(); + +/// The file extensions of the formats [`encode`] can write (those whose features are on). +pub const ENCODE_EXTENSIONS: &[&str] = EXTENSIONS.0.split_at(EXTENSIONS.1).0; + +/// The format [`encode`] writes for a file extension or format name (`wav`, `aiff`, `flac`, `m4a`, +/// `mp3`, `ac3`, `eac3`, case-insensitive, with or without the dot), or `None` when it cannot +/// write it. `aifc` is AIFF: float AIFF is written as AIFF-C. `m4a` is AAC when the AAC encoder +/// is built and ALAC otherwise; `alac` always names ALAC. /// /// ``` /// # #[cfg(feature = "pcm-encode")] { /// use craft_audio::{EncodeFormat, encode_format_for}; /// /// assert_eq!(encode_format_for(".AIFF"), Some(EncodeFormat::Aiff)); -/// assert_eq!(encode_format_for("mp3"), None); +/// assert_eq!(encode_format_for("ogg"), None); /// # } /// ``` pub fn encode_format_for(name: &str) -> Option { @@ -82,6 +110,18 @@ pub fn encode_format_for(name: &str) -> Option { "aif" | "aiff" | "aifc" => Some(EncodeFormat::Aiff), #[cfg(feature = "flac-encode")] "flac" => Some(EncodeFormat::Flac), + #[cfg(feature = "aac-encode")] + "m4a" => Some(EncodeFormat::Aac), + #[cfg(all(feature = "alac-encode", not(feature = "aac-encode")))] + "m4a" => Some(EncodeFormat::Alac), + #[cfg(feature = "alac-encode")] + "alac" => Some(EncodeFormat::Alac), + #[cfg(feature = "mp3-encode")] + "mp3" => Some(EncodeFormat::Mp3), + #[cfg(feature = "ac3-encode")] + "ac3" => Some(EncodeFormat::Ac3), + #[cfg(feature = "eac3-encode")] + "eac3" | "ec3" => Some(EncodeFormat::Eac3), _ => None, } } @@ -102,11 +142,11 @@ fn failed_write_error() -> AudioError { AudioError::Encode("the stream encoder already failed on an earlier write; its output is incomplete".into()) } -/// Writes WAV/BWF (RF64 above 4 GiB), AIFF / AIFF-C `fl32` or FLAC block by block, producing -/// the same bytes [`encode`] would for the whole buffer: a file of any length never -/// has to fit in memory. The file starts at the sink's position, and nothing may follow that -/// position (a new file, or one truncated there): the encoder cannot truncate a `Write + Seek`, -/// so the bytes of a longer file already there would stay after the new one. +/// Writes a file of any [`EncodeFormat`] block by block, producing the same bytes [`encode`] +/// would for the whole buffer: a file of any length never has to fit in memory. The file starts +/// at the sink's position, and nothing may follow that position (a new file, or one truncated +/// there): the encoder cannot truncate a `Write + Seek`, so the bytes of a longer file already +/// there would stay after the new one. /// /// ``` /// # #[cfg(feature = "flac-encode")] { @@ -125,30 +165,27 @@ fn failed_write_error() -> AudioError { /// ``` pub struct FileEncoder { writer: Writer, + format: EncodeFormat, channels: usize, } impl std::fmt::Debug for FileEncoder { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let format = match &self.writer { - #[cfg(feature = "pcm-encode")] - Writer::Wav(_) => EncodeFormat::Wav, - #[cfg(feature = "pcm-encode")] - Writer::Aiff(_) => EncodeFormat::Aiff, - #[cfg(feature = "flac-encode")] - Writer::Flac(_) => EncodeFormat::Flac, - }; - f.debug_struct("FileEncoder").field("format", &format).finish_non_exhaustive() + f.debug_struct("FileEncoder").field("format", &self.format).finish_non_exhaustive() } } -enum Writer { +// The writers differ in size with the features built; the coded one is boxed for its encoder. +#[allow(clippy::large_enum_variant)] +enum Writer { #[cfg(feature = "pcm-encode")] Wav(wav::Writer), #[cfg(feature = "pcm-encode")] Aiff(aiff::Writer), #[cfg(feature = "flac-encode")] Flac(flac::Writer), + #[cfg(any(feature = "aac-encode", feature = "alac-encode", feature = "mp3-encode", feature = "ac3-encode", feature = "eac3-encode"))] + Coded(Box>), } impl FileEncoder { @@ -156,7 +193,10 @@ impl FileEncoder { /// `opts.layout`, when set, must have `channels` channels. pub fn new(out: W, opts: &EncodeOptions, sample_rate: u32, channels: u16) -> Result { check_built(opts.format)?; - let (depth, dither, channels) = (opts.bit_depth, opts.dither, usize::from(channels)); + let channels = usize::from(channels); + #[cfg_attr(not(any(feature = "pcm-encode", feature = "flac-encode")), allow(unused_variables))] + let (depth, dither) = (opts.bit_depth, opts.dither); + #[cfg_attr(not(any(feature = "pcm-encode", feature = "flac-encode")), allow(unused_variables))] let mask = opts.layout.map(|l| wav_mask(&l, channels)).transpose()?; let writer = match opts.format { #[cfg(feature = "pcm-encode")] @@ -172,24 +212,45 @@ impl FileEncoder { EncodeFormat::Aiff => Writer::Aiff(aiff::Writer::new(out, channels, sample_rate, depth, dither)?), #[cfg(feature = "flac-encode")] EncodeFormat::Flac => Writer::Flac(flac::Writer::new(out, channels, sample_rate, depth, dither, mask)?), + #[cfg(any(feature = "aac-encode", feature = "alac-encode", feature = "mp3-encode", feature = "ac3-encode", feature = "eac3-encode"))] + EncodeFormat::Aac | EncodeFormat::Alac | EncodeFormat::Mp3 | EncodeFormat::Ac3 | EncodeFormat::Eac3 => { + Writer::Coded(Box::new(coded::Writer::new(out, opts, sample_rate, channels)?)) + } #[allow(unreachable_patterns)] f => return Err(AudioError::Unsupported(format!("writing {f:?}"))), }; - Ok(FileEncoder { writer, channels }) + Ok(FileEncoder { writer, format: opts.format, channels }) + } + + /// The constant bit rate the file is written at, in bits per second for all channels + /// together: the rate asked for or the format's default, moved to one the stream + /// can carry as [`RateControl::Bitrate`](crate::RateControl::Bitrate) describes. `None` for the lossless + /// formats and for AAC at a constant quality. + pub fn bitrate(&self) -> Option { + match &self.writer { + #[cfg(any(feature = "aac-encode", feature = "alac-encode", feature = "mp3-encode", feature = "ac3-encode", feature = "eac3-encode"))] + Writer::Coded(w) => w.bitrate(), + #[allow(unreachable_patterns)] + _ => None, + } } /// Fails, without writing anything, when `frames` more frames would not fit the format (an - /// AIFF holds 4 GiB, a FLAC 2^36 - 1 frames). + /// AIFF holds 4 GiB, a FLAC 2^36 - 1 frames). The coded formats have no limit a file reaches. pub fn check_room(&self, frames: u64) -> Result<()> { match &self.writer { #[cfg(feature = "pcm-encode")] - Writer::Wav(w) => w.check_room(frames), + Writer::Wav(w) => w.check_room(frames).map(drop), #[cfg(feature = "pcm-encode")] - Writer::Aiff(w) => w.check_room(frames), + Writer::Aiff(w) => w.check_room(frames).map(drop), #[cfg(feature = "flac-encode")] - Writer::Flac(w) => w.check_room(frames), + Writer::Flac(w) => w.check_room(frames).map(drop), + #[cfg(any(feature = "aac-encode", feature = "alac-encode", feature = "mp3-encode", feature = "ac3-encode", feature = "eac3-encode"))] + Writer::Coded(_) => { + let _ = frames; + Ok(()) + } } - .map(drop) } /// Appends the first `frames` frames of `block`, one slice per channel (`&[Vec]`, @@ -212,10 +273,13 @@ impl FileEncoder { Writer::Aiff(w) => w.write(block, frames), #[cfg(feature = "flac-encode")] Writer::Flac(w) => w.write(block, frames), + #[cfg(any(feature = "aac-encode", feature = "alac-encode", feature = "mp3-encode", feature = "ac3-encode", feature = "eac3-encode"))] + Writer::Coded(w) => w.write(block, frames), } } - /// Completes the headers and hands back `out`, positioned at the end of the file. + /// Encodes what is still buffered, completes the headers and hands back `out`, positioned at + /// the end of the file. pub fn finish(self) -> Result { match self.writer { #[cfg(feature = "pcm-encode")] @@ -224,10 +288,41 @@ impl FileEncoder { Writer::Aiff(w) => w.finish(), #[cfg(feature = "flac-encode")] Writer::Flac(w) => w.finish(), + #[cfg(any(feature = "aac-encode", feature = "alac-encode", feature = "mp3-encode", feature = "ac3-encode", feature = "eac3-encode"))] + Writer::Coded(w) => w.finish(), } } } +impl EncodeOptions { + /// Whether a file of `channels` channels at `sample_rate` can be written with these options: + /// `Ok`, or the error [`FileEncoder::new`] and [`encode`] would return, with nothing written. + /// For choosing a format before a file is created; [`EncodeFormat::sample_rates`], + /// [`EncodeFormat::max_channels`] and [`EncodeFormat::supports_layout`] say what a format takes. + /// + /// ``` + /// # #[cfg(feature = "mp3-encode")] { + /// use craft_audio::{EncodeFormat, EncodeOptions}; + /// + /// let mp3 = EncodeOptions::new(EncodeFormat::Mp3).with_bitrate(200_000); + /// assert!(mp3.check(48_000, 2).is_ok()); + /// assert!(mp3.check(96_000, 2).is_err()); // not an MP3 sample rate + /// assert_eq!(mp3.bitrate(48_000, 2).unwrap(), Some(192_000)); // the table rate below 200 kbit/s + /// # } + /// ``` + pub fn check(&self, sample_rate: u32, channels: u16) -> Result<()> { + self.bitrate(sample_rate, channels).map(drop) + } + + /// The constant bit rate such a file would be written at ([`FileEncoder::bitrate`]), or the + /// error [`EncodeOptions::check`] returns. + pub fn bitrate(&self, sample_rate: u32, channels: u16) -> Result> { + // The encoder a file would get, on a sink that is thrown away: what it accepts and the + // rate it settles on are then the writer's by construction. + Ok(FileEncoder::new(std::io::Cursor::new(Vec::new()), self, sample_rate, channels)?.bitrate()) + } +} + /// The header of a 32-bit float WAV holding `frames` frames (little-endian samples follow it). Its /// length doesn't depend on `frames`, so a recorder can rewrite it in place as the file grows; it /// switches to RF64 above 4 GiB. @@ -252,7 +347,7 @@ pub fn wav_float_header(sample_rate: u32, channels: u16, frames: u64) -> Result< // With only one of `pcm-encode` and `flac-encode`, the other's formats are refused, not silently // written as something else. -#[cfg(all(test, not(all(feature = "pcm-encode", feature = "flac-encode"))))] +#[cfg(all(test, any(feature = "pcm-encode", feature = "flac-encode"), not(all(feature = "pcm-encode", feature = "flac-encode"))))] mod tests { use super::*; @@ -265,6 +360,7 @@ mod tests { let enc = FileEncoder::new(std::io::Cursor::new(Vec::new()), &opts, 48_000, 1); assert!(matches!(enc, Err(AudioError::Unsupported(_)))); assert_eq!(encode_format_for(name), None); + assert!(!format.is_built() && EncodeFormat::ALL.iter().any(|f| f.is_built())); // Also with a layout it could not use. let opts = opts.with_layout(ChannelLayout::STEREO); assert!(matches!(encode(&buf, &opts), Err(AudioError::Unsupported(_)))); diff --git a/crates/audio/src/write/pcm.rs b/crates/audio/src/write/pcm.rs index 614d5a4a7..402b9bebc 100644 --- a/crates/audio/src/write/pcm.rs +++ b/crates/audio/src/write/pcm.rs @@ -1,6 +1,9 @@ //! PCM helpers shared by the WAV, AIFF and FLAC writers: quantization (with optional TPDF //! dither), interleaving and layout checks. +// The coded formats use the layout check, and of those only ALAC the quantizer. +#![cfg_attr(not(any(feature = "pcm-encode", feature = "flac-encode")), allow(dead_code))] + use crate::{AudioBuffer, AudioError, Result}; #[cfg(feature = "pcm-encode")] use {crate::BitDepth, std::io::Write}; diff --git a/crates/audio/tests/audio/decode_extensions.rs b/crates/audio/tests/audio/decode_extensions.rs index e8fe22d58..27a5573de 100644 --- a/crates/audio/tests/audio/decode_extensions.rs +++ b/crates/audio/tests/audio/decode_extensions.rs @@ -62,11 +62,21 @@ fn every_decode_extension_probes_and_decodes() { fn every_encode_format_has_an_extension_it_is_written_and_read_under() { for &format in EncodeFormat::ALL { let ext = format.extension(); - assert_eq!(craft_audio::encode_format_for(ext), Some(format), "{ext}"); - assert!(craft_audio::ENCODE_EXTENSIONS.contains(&ext), "{ext}"); + // The extension names the format, or for `m4a`, which two formats share, AAC before ALAC. + // A format whose feature is off has no name. + match craft_audio::encode_format_for(ext) { + Some(named) => { + let shares_m4a = |f| matches!(f, EncodeFormat::Aac | EncodeFormat::Alac); + assert!(named == format || (shares_m4a(named) && shares_m4a(format)), "{ext}"); + assert!(named.is_built(), "{ext}"); + assert!(craft_audio::ENCODE_EXTENSIONS.contains(&ext), "{ext}"); + } + None => assert!(!matches!(format, EncodeFormat::Wav | EncodeFormat::Aiff | EncodeFormat::Flac), "{ext}"), + } assert!(DECODE_EXTENSIONS.contains(&ext), "{ext}: what is written reads back"); assert!(format.mime_type().starts_with("audio/"), "{format:?}"); } - assert_eq!(EncodeFormat::ALL.iter().map(|f| f.extension()).collect::>(), ["wav", "aif", "flac"]); + let extensions: Vec<_> = EncodeFormat::ALL.iter().map(|f| f.extension()).collect(); + assert_eq!(extensions, ["wav", "aif", "flac", "m4a", "m4a", "mp3", "ac3", "eac3"]); assert_eq!(EncodeFormat::Aiff.mime_type(), "audio/aiff"); } diff --git a/crates/audio/tests/audio/encode_format.rs b/crates/audio/tests/audio/encode_format.rs index 30b992685..7713f37a5 100644 --- a/crates/audio/tests/audio/encode_format.rs +++ b/crates/audio/tests/audio/encode_format.rs @@ -14,7 +14,7 @@ fn only_formats_the_encoder_writes_are_named() { for name in craft_audio::ENCODE_EXTENSIONS { assert!(encode_format_for(name).is_some(), "{name}"); } - for other in ["mp3", "ogg", "m4a", "xyz", ""] { + for other in ["ogg", "opus", "aac", "mp4", "xyz", ""] { assert_eq!(encode_format_for(other), None, "{other}"); } } diff --git a/crates/audio/tests/codecs/file_formats.rs b/crates/audio/tests/codecs/file_formats.rs new file mode 100644 index 000000000..53947df12 --- /dev/null +++ b/crates/audio/tests/codecs/file_formats.rs @@ -0,0 +1,571 @@ +//! The coded formats of `encode` and `FileEncoder` (AAC and ALAC in M4A; MP3, AC-3 and E-AC-3 +//! streams), read back with this crate's decoder and, where ffmpeg is installed, with ffmpeg. + +use std::io::{Cursor, Seek, SeekFrom, Write}; +use std::process::Command; + +use craft_audio::{ + AudioBuffer, AudioError, BitDepth, ChannelLayout, CodecId, Container, ENCODE_EXTENSIONS, EncodeFormat, EncodeOptions, FileEncoder, RateControl, + decode, encode, encode_format_for, +}; + +const CODED: [EncodeFormat; 5] = [EncodeFormat::Aac, EncodeFormat::Alac, EncodeFormat::Mp3, EncodeFormat::Ac3, EncodeFormat::Eac3]; + +/// `channels` channels of `frames` frames, each channel a tone of its own frequency, on the +/// 16-bit grid (so a lossless 16-bit format returns it exactly). +fn tones(sample_rate: u32, channels: usize, frames: usize) -> AudioBuffer { + let channel = |c: usize| { + let step = (60.0 + 17.0 * c as f32) * std::f32::consts::TAU / sample_rate as f32; + (0..frames).map(|i| ((i as f32 * step).sin() * 0.4 * 32768.0).round() / 32768.0).collect() + }; + AudioBuffer { sample_rate, channels: (0..channels).map(channel).collect() } +} + +/// Signal-to-noise ratio in dB of `got` against `want`, over the frames both have. +fn snr(want: &[f32], got: &[f32]) -> f64 { + let (signal, noise) = + want.iter().zip(got).fold((0f64, 0f64), |(s, n), (&w, &g)| (s + f64::from(w).powi(2), n + (f64::from(w) - f64::from(g)).powi(2))); + 10.0 * (signal / noise.max(1e-30)).log10() +} + +fn is_unsupported(r: Result) -> bool { + matches!(r, Err(AudioError::Unsupported(_))) +} + +/// An AAC file decodes to the frames that were written, in place: the edit list skips the +/// encoder's priming and cuts the padding of the last packet. +#[test] +fn aac_decodes_to_the_frames_written() { + for frames in [1, 1000, 1024, 1025, 5000, 48_017] { + let buf = tones(48_000, 2, frames); + let file = encode(&buf, &EncodeOptions::new(EncodeFormat::Aac)).unwrap(); + let (info, out) = decode(&file, Some("m4a")).unwrap(); + assert_eq!((info.container, info.codec, info.sample_rate, info.damaged), (Container::Mp4, CodecId::Aac, 48_000, 0)); + assert_eq!((info.frames, out.frames(), info.layout), (frames as u64, frames, ChannelLayout::STEREO), "{frames} frames"); + if frames >= 5000 { + for (want, got) in buf.channels.iter().zip(&out.channels) { + assert!(snr(want, got) > 30.0, "{frames} frames: {} dB", snr(want, got)); + } + } + } +} + +/// The M4A track's durations add up to the priming and the audio, and its one edit skips the +/// priming and lasts as long as the audio, in a movie timescale that is the sample rate. +#[test] +fn aac_timing_is_in_the_sample_table_and_the_edit_list() { + use craft_audio::craft_media::isobmff; + let file = encode(&tones(44_100, 1, 3000), &EncodeOptions::new(EncodeFormat::Aac)).unwrap(); + let mp4 = isobmff::open(file.as_slice()).unwrap(); + let track = &mp4.tracks[0]; + assert_eq!((mp4.timescale, track.timescale), (44_100, 44_100)); + assert_eq!(track.samples.iter().map(|s| u64::from(s.duration)).sum::(), 1024 + 3000); + assert_eq!(track.edits.len(), 1); + assert_eq!((track.edits[0].media_time, track.edits[0].segment_duration), (1024, 3000)); + // Every packet is in a `roll` group of distance -1, which Apple's decoders need to apply the edit. + let groups = &track.sample_groups; + assert_eq!((groups.len(), groups[0].grouping_type), (1, isobmff::FourCc(*b"roll"))); + assert_eq!((groups[0].runs.as_slice(), &groups[0].descriptions), ([(track.samples.len() as u32, 1)].as_slice(), &vec![vec![0xFF, 0xFF]])); +} + +/// The sample entry is completed when the file is: AAC's average bit rate, and the largest packet +/// and average bit rate in ALAC's cookie. +#[test] +fn the_sample_entry_describes_the_finished_stream() { + use craft_audio::craft_media::isobmff::{self, CodecConfig}; + let buf = tones(48_000, 2, 48_000); + let file = encode(&buf, &EncodeOptions::new(EncodeFormat::Aac).with_bitrate(96_000)).unwrap(); + let mp4 = isobmff::open(file.as_slice()).unwrap(); + let CodecConfig::Aac(aac) = &mp4.tracks[0].entries[0].codec else { panic!("not AAC") }; + assert!((90_000..100_000).contains(&aac.avg_bitrate), "{} bit/s", aac.avg_bitrate); + + let file = encode(&buf, &EncodeOptions::new(EncodeFormat::Alac).with_bit_depth(BitDepth::Int16)).unwrap(); + let mp4 = isobmff::open(file.as_slice()).unwrap(); + let CodecConfig::Alac { cookie } = &mp4.tracks[0].entries[0].codec else { panic!("not ALAC") }; + let config = craft_audio::alac::SpecificConfig::parse(cookie).unwrap(); + let largest = mp4.tracks[0].samples.iter().map(|s| s.size).max().unwrap(); + assert_eq!((config.max_frame_bytes, config.bit_depth, config.frame_length), (largest, 16, 4096)); + assert!(config.avg_bit_rate > 0); +} + +/// Dither reaches ALAC's quantizer: a level below one 16-bit step is silence without it. +#[test] +fn alac_dithers_when_asked() { + let buf = AudioBuffer { sample_rate: 48_000, channels: vec![vec![0.3 / 32768.0; 4096]] }; + let opts = EncodeOptions::new(EncodeFormat::Alac).with_bit_depth(BitDepth::Int16); + let (_, plain) = decode(&encode(&buf, &opts).unwrap(), Some("m4a")).unwrap(); + let (_, dithered) = decode(&encode(&buf, &opts.with_dither(true)).unwrap(), Some("m4a")).unwrap(); + assert!(plain.channels[0].iter().all(|&s| s == 0.0)); + assert!(dithered.channels[0].iter().any(|&s| s != 0.0)); +} + +/// ALAC returns the quantized samples exactly, for every channel count, with each channel where +/// its speaker is. +#[test] +fn alac_is_lossless_in_every_layout() { + for channels in 1..=8u8 { + let layout = ChannelLayout::from_mask(craft_audio::alac::channel_mask(channels)); + let buf = tones(44_100, usize::from(channels), 10_000); + let opts = EncodeOptions::new(EncodeFormat::Alac).with_bit_depth(BitDepth::Int16).with_layout(layout); + let file = encode(&buf, &opts).unwrap(); + let (info, out) = decode(&file, Some("m4a")).unwrap(); + assert_eq!((info.container, info.codec, info.frames, info.layout), (Container::Mp4, CodecId::Alac, 10_000, layout), "{channels} channels"); + assert_eq!(out, buf, "{channels} channels"); + } + // 24 bits, and a length that is no whole number of packets. + let buf = tones(96_000, 2, 4096 * 2 + 1); + let (_, out) = decode(&encode(&buf, &EncodeOptions::new(EncodeFormat::Alac)).unwrap(), Some("m4a")).unwrap(); + assert_eq!(out, buf); +} + +/// Channels go to the encoder by speaker. ALAC's 6.1 has the back centre last and AAC's has it +/// before the surround pair; 5.1 with side surrounds is the format's one 5.1. +#[test] +fn channels_reach_their_speakers() { + // WAV 6.1 with a back pair (L R C LFE Lb Rb Cb), the usual 7-channel layout, into ALAC: the same. + let buf = tones(48_000, 7, 8000); + let (info, out) = decode(&encode(&buf, &EncodeOptions::new(EncodeFormat::Alac).with_bit_depth(BitDepth::Int16)).unwrap(), Some("m4a")).unwrap(); + assert_eq!(info.layout, ChannelLayout::from_mask(0x13F)); + assert_eq!(out, buf); + // 5.1 with side surrounds decodes from ALAC as 5.1 with back surrounds, channel for channel. + let buf = tones(48_000, 6, 8000); + let opts = EncodeOptions::new(EncodeFormat::Alac).with_bit_depth(BitDepth::Int16).with_layout(ChannelLayout::SURROUND_5_1_SIDE); + assert_eq!(decode(&encode(&buf, &opts).unwrap(), Some("m4a")).unwrap().1, buf); + + // AAC: the decoded channel each input channel comes back as. 5.1 decodes in WAV order; the + // 6.1 and 7.1 streams have no speaker assignment and decode in AAC order (C L R Ls Rs Cs LFE + // and C Lc Rc L R Ls Rs LFE). + let wide = ChannelLayout::from_mask(0xFF); + let cases: [(usize, Option, &[usize]); 3] = + [(6, None, &[0, 1, 2, 3, 4, 5]), (7, None, &[2, 0, 1, 4, 5, 6, 3]), (8, Some(wide), &[2, 6, 7, 0, 1, 4, 5, 3])]; + for (channels, layout, decoded_from) in cases { + let buf = tones(48_000, channels, 48_000); + let mut opts = EncodeOptions::new(EncodeFormat::Aac); + opts.layout = layout; + let (info, out) = decode(&encode(&buf, &opts).unwrap(), Some("m4a")).unwrap(); + assert_eq!((usize::from(info.channels), out.frames()), (channels, 48_000)); + for (decoded, &input) in decoded_from.iter().enumerate() { + let db = snr(&buf.channels[input], &out.channels[decoded]); + assert!(db > 20.0, "{channels} channels: decoded {decoded} is not input {input} ({db} dB)"); + } + } +} + +/// MP3, AC-3 and E-AC-3 are raw streams: the audio comes back behind the codec's delay. +#[test] +fn raw_streams_decode_behind_the_codec_delay() { + let buf = tones(48_000, 2, 48_000); + for (format, codec, container, delay, floor) in [ + (EncodeFormat::Mp3, CodecId::Mp3, Container::MpegAudio, craft_audio::mp3::DELAY as usize, 25.0), + (EncodeFormat::Ac3, CodecId::Ac3, Container::Ac3, 256, 15.0), + (EncodeFormat::Eac3, CodecId::Eac3, Container::Ac3, 256, 15.0), + ] { + let file = encode(&buf, &EncodeOptions::new(format)).unwrap(); + let (info, out) = decode(&file, Some(format.extension())).unwrap(); + assert_eq!((info.codec, info.container, info.sample_rate, info.layout, info.damaged), (codec, container, 48_000, ChannelLayout::STEREO, 0)); + assert_eq!((format.codec(), format.container()), (codec, container)); + assert!(out.frames() >= 48_000 + delay, "{format:?}: {} frames", out.frames()); + for (want, got) in buf.channels.iter().zip(&out.channels) { + let db = snr(want, &got[delay..]); + assert!(db > floor, "{format:?}: {db} dB"); + } + } + // 5.1 with back surrounds is AC-3's 3/2 mode with an LFE, channel for channel. + let buf = tones(48_000, 6, 48_000); + let (info, out) = decode(&encode(&buf, &EncodeOptions::new(EncodeFormat::Ac3)).unwrap(), Some("ac3")).unwrap(); + assert_eq!(info.layout, ChannelLayout::SURROUND_5_1_SIDE); + for (c, (want, got)) in buf.channels.iter().zip(&out.channels).enumerate() { + // The encoder low-pass filters the LFE, which shifts its phase: it is compared by level. + if c == 3 { + let rms = |x: &[f32]| (x.iter().map(|&s| f64::from(s).powi(2)).sum::() / x.len() as f64).sqrt(); + let level = rms(&got[4800..]) / rms(want); + assert!((0.5..1.5).contains(&level), "LFE at {level} of its level"); + } else { + assert!(snr(want, &got[256..]) > 12.0, "channel {c}: {} dB", snr(want, &got[256..])); + } + } + // 7.1 is an E-AC-3 programme. + let buf = tones(48_000, 8, 4800); + assert!(is_unsupported(encode(&buf, &EncodeOptions::new(EncodeFormat::Ac3)))); + // (Read back by ffmpeg in `ffmpeg_reads_the_files`: this crate's file reader plays the 5.1 + // independent substream of a raw 7.1 stream.) + assert!(!encode(&buf, &EncodeOptions::new(EncodeFormat::Eac3)).unwrap().is_empty()); +} + +/// Blocks of any sizes give the bytes `encode` gives. +#[test] +fn streaming_writes_the_bytes_of_encode() { + let buf = tones(44_100, 2, 30_000); + for format in CODED { + let opts = EncodeOptions::new(format); + let whole = encode(&buf, &opts).unwrap(); + let mut enc = FileEncoder::new(Cursor::new(Vec::new()), &opts, 44_100, 2).unwrap(); + assert!(format!("{enc:?}").contains(&format!("{format:?}"))); + let mut at = 0; + for n in [1, 4095, 4097, 0, 9000, 12_807] { + let block: Vec<&[f32]> = buf.channels.iter().map(|c| &c[at..]).collect(); + enc.check_room(n as u64).unwrap(); + enc.write(&block, n).unwrap(); + at += n; + } + assert_eq!(at, buf.frames()); + assert!(enc.finish().unwrap().into_inner() == whole, "{format:?}"); + } +} + +/// A file starts at the sink's position. +#[test] +fn a_file_starts_at_the_sinks_position() { + let buf = tones(48_000, 1, 6000); + for format in CODED { + let opts = EncodeOptions::new(format); + let mut sink = Cursor::new(b"junk".to_vec()); + sink.seek(SeekFrom::End(0)).unwrap(); + let mut enc = FileEncoder::new(sink, &opts, 48_000, 1).unwrap(); + enc.write(&buf.channels, 6000).unwrap(); + let mut sink = enc.finish().unwrap(); + let end = sink.stream_position().unwrap(); + let bytes = sink.into_inner(); + assert_eq!(end, bytes.len() as u64, "{format:?}"); + assert!(bytes[4..] == encode(&buf, &opts).unwrap(), "{format:?}"); + } +} + +/// A file with no audio is still a file of its format. +#[test] +fn no_audio_gives_an_empty_file() { + let buf = AudioBuffer::new(48_000, 2, 0); + for format in CODED { + let file = encode(&buf, &EncodeOptions::new(format)).unwrap(); + if format.container() == Container::Mp4 { + let mp4 = craft_audio::craft_media::isobmff::open(file.as_slice()).unwrap(); + assert_eq!((mp4.tracks.len(), mp4.tracks[0].samples.len(), mp4.tracks[0].edits.len()), (1, 0, 0), "{format:?}"); + } else { + assert!(file.is_empty(), "{format:?}"); + } + } +} + +/// The bit rate asked for is the one written, moved to the nearest the format has. +#[test] +fn the_bit_rate_is_followed() { + let buf = tones(48_000, 2, 48_000); + let size = |opts: &EncodeOptions| encode(&buf, opts).unwrap().len(); + // MP3 at 48 kHz: 24 bytes per frame and kbit/s, 43 frames for a second and the delay. + assert_eq!(size(&EncodeOptions::new(EncodeFormat::Mp3)), 43 * 192 * 3); + assert_eq!(size(&EncodeOptions::new(EncodeFormat::Mp3).with_bitrate(130_000)), 43 * 128 * 3); + assert_eq!(size(&EncodeOptions::new(EncodeFormat::Mp3).with_bitrate(1)), 43 * 32 * 3); + assert_eq!(size(&EncodeOptions::new(EncodeFormat::Mp3).with_bitrate(u32::MAX)), 43 * 320 * 3); + // AC-3: 32 packets of 1536 frames, 4 bytes per packet and kbit/s. + assert_eq!(size(&EncodeOptions::new(EncodeFormat::Ac3)), 32 * 192 * 4); + assert_eq!(size(&EncodeOptions::new(EncodeFormat::Ac3).with_bitrate(100_000)), 32 * 96 * 4); + assert_eq!(size(&EncodeOptions::new(EncodeFormat::Eac3).with_bitrate(100_000)), 32 * 100 * 4); + // AAC: a second of audio at about the rate asked for, around the 128 kbit/s default. + let (low, default, high) = ( + size(&EncodeOptions::new(EncodeFormat::Aac).with_bitrate(64_000)), + size(&EncodeOptions::new(EncodeFormat::Aac)), + size(&EncodeOptions::new(EncodeFormat::Aac).with_bitrate(256_000)), + ); + assert!((7000..10_000).contains(&low) && (15_000..18_500).contains(&default) && (30_000..35_000).contains(&high), "{low} {default} {high}"); + // A rate no stream can carry is moved into range, not refused. + encode(&buf, &EncodeOptions::new(EncodeFormat::Aac).with_bitrate(u32::MAX)).unwrap(); + // The lowest rate still codes the audio: the encoder accepts rates that leave only silence + // (128 for 128 kbit/s is one), and the writer does not go that low. + for (sample_rate, channels) in [(8000, 1), (16_000, 2), (44_100, 2), (48_000, 6), (96_000, 2)] { + let buf = tones(sample_rate, channels, sample_rate as usize); + let (_, out) = decode(&encode(&buf, &EncodeOptions::new(EncodeFormat::Aac).with_bitrate(128)).unwrap(), Some("m4a")).unwrap(); + let rms = |x: &[f32]| (x.iter().map(|&s| f64::from(s).powi(2)).sum::() / x.len() as f64).sqrt(); + let level = rms(&out.channels[0]) / rms(&buf.channels[0]); + assert!(level > 0.5, "{sample_rate} Hz, {channels} channels: {level} of the level"); + } + // Quality is AAC's; ALAC has no rate to control. + let (small, large) = + (size(&EncodeOptions::new(EncodeFormat::Aac).with_quality(0.0)), size(&EncodeOptions::new(EncodeFormat::Aac).with_quality(1.0))); + assert!(small < large, "{small} {large}"); + assert!(matches!(encode(&buf, &EncodeOptions::new(EncodeFormat::Aac).with_quality(f32::NAN)), Err(AudioError::Encode(_)))); + for format in [EncodeFormat::Mp3, EncodeFormat::Ac3, EncodeFormat::Eac3] { + assert!(is_unsupported(encode(&buf, &EncodeOptions::new(format).with_quality(0.5))), "{format:?}"); + } + let alac = EncodeOptions::new(EncodeFormat::Alac); + assert_eq!(size(&alac), size(&alac.clone().with_bitrate(64_000))); + assert_eq!(alac.rate_control, None); + assert_eq!(alac.with_quality(0.5).rate_control, Some(RateControl::Quality(0.5))); +} + +/// What a format cannot hold is refused before anything is written. +#[test] +fn what_a_format_cannot_hold_is_refused() { + let refused = |format, sample_rate, channels: u16, layout: Option| { + let mut opts = EncodeOptions::new(format); + opts.layout = layout; + let mut sink = Cursor::new(Vec::new()); + let r = FileEncoder::new(&mut sink, &opts, sample_rate, channels).map(drop); + assert!(r.is_ok() || sink.into_inner().is_empty(), "{format:?}: bytes written for a refused file"); + assert_eq!(r.is_err(), encode(&AudioBuffer::new(sample_rate, channels, 100), &opts).is_err()); + r + }; + // Quad has no centre, which AAC's and ALAC's four channels have. + assert!(is_unsupported(refused(EncodeFormat::Aac, 48_000, 4, None))); + assert!(is_unsupported(refused(EncodeFormat::Alac, 48_000, 4, None))); + refused(EncodeFormat::Aac, 48_000, 4, Some(ChannelLayout::from_mask(0x107))).unwrap(); + // WAV 7.1 (backs and sides) is not the 7.1 of AAC and ALAC (a front pair left and right of centre). + assert!(is_unsupported(refused(EncodeFormat::Aac, 48_000, 8, None))); + assert!(is_unsupported(refused(EncodeFormat::Alac, 48_000, 8, Some(ChannelLayout::SURROUND_7_1)))); + assert!(is_unsupported(refused(EncodeFormat::Aac, 48_000, 2, Some(ChannelLayout::unordered(2))))); + for format in CODED { + assert!(is_unsupported(refused(format, 48_000, 9, None)), "{format:?}"); + assert!(matches!(refused(format, 48_000, 2, Some(ChannelLayout::MONO)), Err(AudioError::Encode(_))), "{format:?}"); + assert!(matches!(refused(format, 0, 2, None), Err(AudioError::Encode(_))), "{format:?}"); + assert!(matches!(refused(format, 48_000, 0, None), Err(AudioError::Encode(_))), "{format:?}"); + } + assert!(is_unsupported(refused(EncodeFormat::Mp3, 48_000, 3, None))); + assert!(is_unsupported(refused(EncodeFormat::Ac3, 48_000, 7, None))); + // Sample rates the codec has not. + for format in [EncodeFormat::Aac, EncodeFormat::Mp3, EncodeFormat::Ac3, EncodeFormat::Eac3] { + assert!(matches!(refused(format, 44_000, 2, None), Err(AudioError::Encode(_))), "{format:?}"); + } + refused(EncodeFormat::Alac, 44_000, 2, None).unwrap(); + let float = EncodeOptions::new(EncodeFormat::Alac).with_bit_depth(BitDepth::Float32); + assert!(is_unsupported(encode(&AudioBuffer::new(48_000, 2, 100), &float))); +} + +/// Samples no encoder should see: NaN is silence, and values past full scale do not fail a +/// write (AC-3, whose encoder refuses them, gets them clamped). +#[test] +fn non_finite_and_loud_samples_are_written() { + let mut buf = tones(48_000, 2, 6000); + buf.channels[0][100] = f32::NAN; + buf.channels[0][200] = f32::INFINITY; + buf.channels[1][300] = -7.5; + for format in CODED { + let file = encode(&buf, &EncodeOptions::new(format)).unwrap(); + let (_, out) = decode(&file, Some(format.extension())).unwrap(); + assert!(out.channels.iter().flatten().all(|s| s.is_finite()), "{format:?}"); + } +} + +/// A sink that fails once `room` bytes are written. +struct Full { + inner: Cursor>, + room: usize, +} + +impl Write for Full { + fn write(&mut self, data: &[u8]) -> std::io::Result { + if self.inner.get_ref().len() + data.len() > self.room { + return Err(std::io::Error::other("disk full")); + } + self.inner.write(data) + } + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +impl Seek for Full { + fn seek(&mut self, pos: SeekFrom) -> std::io::Result { + self.inner.seek(pos) + } +} + +/// After a failed write the encoder is unusable: later writes and `finish` fail too. +#[test] +fn a_failed_write_ends_the_encoder() { + let buf = tones(48_000, 2, 48_000); + for format in CODED { + let mut enc = FileEncoder::new(Full { inner: Cursor::new(Vec::new()), room: 2000 }, &EncodeOptions::new(format), 48_000, 2).unwrap(); + assert!(matches!(enc.write(&buf.channels, 48_000), Err(AudioError::Io(_))), "{format:?}"); + assert!(matches!(enc.write(&buf.channels, 1), Err(AudioError::Encode(_))), "{format:?}"); + assert!(matches!(enc.finish(), Err(AudioError::Encode(_))), "{format:?}"); + } +} + +#[test] +fn the_formats_are_named() { + assert_eq!(encode_format_for("m4a"), Some(EncodeFormat::Aac)); + assert_eq!(encode_format_for(".ALAC"), Some(EncodeFormat::Alac)); + assert_eq!(encode_format_for("mp3"), Some(EncodeFormat::Mp3)); + assert_eq!(encode_format_for("ac3"), Some(EncodeFormat::Ac3)); + assert_eq!(encode_format_for("eac3"), Some(EncodeFormat::Eac3)); + assert_eq!(encode_format_for("ec3"), Some(EncodeFormat::Eac3)); + for &format in EncodeFormat::ALL { + assert!(ENCODE_EXTENSIONS.contains(&format.extension()), "{format:?}"); + let named = encode_format_for(format.extension()).unwrap(); + assert_eq!((named.extension(), named.mime_type()), (format.extension(), format.mime_type())); + assert_eq!(format.is_lossy(), !matches!(format.codec(), CodecId::Pcm | CodecId::Flac | CodecId::Alac)); + } + for name in ENCODE_EXTENSIONS { + assert!(encode_format_for(name).is_some(), "{name}"); + } + assert_eq!(ENCODE_EXTENSIONS.len(), 12); + for format in CODED { + assert!(EncodeFormat::ALL.contains(&format) && format.is_built()); + } + let features: Vec<_> = EncodeFormat::ALL.iter().map(|f| f.feature()).collect(); + assert_eq!(features, ["pcm-encode", "pcm-encode", "flac-encode", "alac-encode", "aac-encode", "mp3-encode", "ac3-encode", "eac3-encode"]); +} + +/// ffmpeg's decode of `file` as interleaved `f32`, split into `channels` channels. +fn ffmpeg_decode(ff: &std::path::Path, file: &[u8], name: &str, channels: usize) -> Vec> { + let path = crate::testkit::fixtures_dir("file_formats").join(name); + std::fs::write(&path, file).unwrap(); + let out = Command::new(ff).args(["-v", "error", "-i"]).arg(&path).args(["-f", "f32le", "-"]).output().unwrap(); + assert!(out.status.success() && out.stderr.is_empty(), "{name}: {}", String::from_utf8_lossy(&out.stderr)); + let mut planar = vec![Vec::new(); channels]; + for (i, s) in out.stdout.as_chunks::<4>().0.iter().enumerate() { + planar[i % channels].push(f32::from_le_bytes(*s)); + } + planar +} + +/// ffmpeg reads every format: the M4A files without their priming (it applies the edit list) and, +/// from ffmpeg 9, at the length written; ALAC exactly; the raw streams behind their delay. +#[test] +fn ffmpeg_reads_the_files() { + let ff = require_ffmpeg!(); + for (channels, layout) in [(1, None), (2, None), (6, None), (8, Some(ChannelLayout::from_mask(0xFF)))] { + let buf = tones(48_000, channels, 20_000); + let mut opts = EncodeOptions::new(EncodeFormat::Aac); + opts.layout = layout; + let out = ffmpeg_decode(&ff, &encode(&buf, &opts).unwrap(), &format!("aac_{channels}.m4a"), channels); + // The priming is skipped by every ffmpeg (the channels below line up). The padding of the + // last packet is cut by ffmpeg 9; ffmpeg 8 plays it, to the end of the 20th packet. + assert!([20_000, 20_480].contains(&out[0].len()), "AAC, {channels} channels: {} frames", out[0].len()); + if channels <= 6 { + // ffmpeg also returns 5.1 in WAV order. + for (c, (want, got)) in buf.channels.iter().zip(&out).enumerate() { + assert!(snr(want, got) > 20.0, "AAC, {channels} channels, channel {c}: {} dB", snr(want, got)); + } + } + let opts = opts.with_format(EncodeFormat::Alac).with_bit_depth(BitDepth::Int16); + let opts = if channels == 6 { opts.with_layout(ChannelLayout::SURROUND_5_1) } else { opts }; + let out = ffmpeg_decode(&ff, &encode(&buf, &opts).unwrap(), &format!("alac_{channels}.m4a"), channels); + assert!(out == buf.channels, "ALAC, {channels} channels"); + } + let buf = tones(44_100, 2, 20_000); + for (format, delay, floor) in + [(EncodeFormat::Mp3, craft_audio::mp3::DELAY as usize, 25.0), (EncodeFormat::Ac3, 256, 15.0), (EncodeFormat::Eac3, 256, 15.0)] + { + let name = format!("stream.{}", format.extension()); + let out = ffmpeg_decode(&ff, &encode(&buf, &EncodeOptions::new(format)).unwrap(), &name, 2); + for (want, got) in buf.channels.iter().zip(&out) { + assert!( + got.len() >= 20_000 + delay && snr(want, &got[delay..]) > floor, + "{format:?}: {} frames, {} dB", + got.len(), + snr(want, &got[delay..]) + ); + } + } + // E-AC-3 7.1: the front channels, which the 5.1 downmix leaves as they are. + let buf = tones(48_000, 8, 20_000); + let out = ffmpeg_decode(&ff, &encode(&buf, &EncodeOptions::new(EncodeFormat::Eac3)).unwrap(), "stream_7_1.eac3", 8); + assert!(out[7].len() >= 20_256); + for c in 0..3 { + assert!(snr(&buf.channels[c], &out[c][256..]) > 12.0, "E-AC-3 7.1, channel {c}: {} dB", snr(&buf.channels[c], &out[c][256..])); + } +} + +/// Apple's decoder (macOS `afinfo`; skipped elsewhere) takes the priming and the length of an AAC +/// file from the edit list, which it does only for a track with a `roll` sample group. +#[test] +fn apple_reads_the_aac_priming_and_length() { + let Some(afinfo) = crate::testkit::oracle::find_tool("afinfo") else { return }; + for (frames, remainder) in [(20_000, 480), (1, 1023), (4096, 0)] { + let path = crate::testkit::fixtures_dir("file_formats").join(format!("apple_{frames}.m4a")); + std::fs::write(&path, encode(&tones(48_000, 2, frames), &EncodeOptions::new(EncodeFormat::Aac)).unwrap()).unwrap(); + let out = Command::new(&afinfo).arg(&path).output().unwrap(); + let text = String::from_utf8_lossy(&out.stdout); + assert!(text.contains(&format!("{frames} valid frames + 1024 priming + {remainder} remainder")), "{frames} frames: {text}"); + } +} + +/// What `EncodeFormat` says a format takes is what the writer accepts: for every format, a file +/// can be begun exactly when the sample rate, the channel count and the layout are listed. +#[test] +fn the_format_queries_agree_with_the_writer() { + // Every rate a format lists, each with its neighbours, and some that none lists. + let mut rates: Vec = EncodeFormat::ALL.iter().filter_map(|f| f.sample_rates()).flatten().flat_map(|&r| [r - 1, r, r + 1]).collect(); + rates.extend([44_000, 176_400, 192_000]); + rates.sort_unstable(); + rates.dedup(); + let masks = [ + 0x4, 0x1, 0x3, 0xC, 0x600, 0x7, 0xB, 0x103, 0x33, 0x107, 0x603, 0xF, 0x10B, 0x37, 0x607, 0x10F, 0x60B, 0x3F, 0x60F, 0x13F, 0x70F, 0x63F, 0xFF, + ]; + let layouts = masks.iter().map(|&mask| ChannelLayout::from_mask(mask)).chain((1..=9).map(ChannelLayout::unordered)); + for layout in layouts { + for &format in EncodeFormat::ALL { + // 24 bits is a depth every format has. + let opts = EncodeOptions::new(format).with_layout(layout); + for &sample_rate in &rates { + let listed = format.sample_rates().is_none_or(|r| r.contains(&sample_rate)) + && layout.channels() <= format.max_channels() + && format.supports_layout(&layout); + let checked = opts.check(sample_rate, layout.channels()); + assert_eq!(checked.is_ok(), listed, "{format:?}, {sample_rate} Hz, {layout:?}: {checked:?}"); + let begun = FileEncoder::new(Cursor::new(Vec::new()), &opts, sample_rate, layout.channels()); + assert_eq!(begun.is_ok(), listed, "{format:?}, {sample_rate} Hz, {layout:?}"); + } + if let Some(fixed) = format.fixed_layouts(layout.channels()) { + assert!(fixed.iter().all(|l| format.supports_layout(l) && l.channels() == layout.channels())); + assert_eq!(format.supports_layout(&layout), fixed.iter().any(|l| same_speakers(l, &layout)), "{format:?}, {layout:?}"); + } + } + } + for &format in EncodeFormat::ALL { + assert!(!format.supports_layout(&ChannelLayout::unordered(0))); + assert_eq!(format.supports_quality(), EncodeOptions::new(format).with_quality(0.5).check(48_000, 2).is_ok() && format.is_lossy()); + assert_eq!(format.fixed_layouts(2).is_none(), matches!(format, EncodeFormat::Wav | EncodeFormat::Aiff | EncodeFormat::Flac)); + } + // The most channels is the most: one more is refused, by the layouts of a format that has + // a fixed set and by the writer of one that has not. + let most: Vec = EncodeFormat::ALL.iter().map(|f| f.max_channels()).collect(); + assert_eq!(most, [32_767, 32_767, 8, 8, 8, 2, 6, 8]); + for &format in EncodeFormat::ALL { + let most = format.max_channels(); + match format.fixed_layouts(most) { + Some(layouts) => assert!(!layouts.is_empty() && format.fixed_layouts(most + 1).unwrap().is_empty(), "{format:?}"), + None => { + let opts = EncodeOptions::new(format).with_bit_depth(BitDepth::Int16); + assert!(opts.check(48_000, most).is_ok() && opts.check(48_000, most + 1).is_err(), "{format:?}"); + assert!(format.supports_layout(&ChannelLayout::unordered(most)) && !format.supports_layout(&ChannelLayout::unordered(most + 1))); + } + } + } + assert_eq!(EncodeFormat::Ac3.fixed_layouts(3).unwrap().len(), 3); + assert!(EncodeFormat::Aac.fixed_layouts(9).unwrap().is_empty()); +} + +/// Whether two layouts name the same speakers, a surround pair being either sides or backs. +fn same_speakers(a: &ChannelLayout, b: &ChannelLayout) -> bool { + let one_pair = |mask: u32| if mask & 0x630 == 0x30 { mask & !0x30 | 0x600 } else { mask }; + matches!((a.mask(), b.mask()), (Some(a), Some(b)) if one_pair(a) == one_pair(b)) +} + +/// `bitrate` is the rate the file is written at, for the options and for the encoder they give. +#[test] +fn the_bit_rate_used_can_be_asked_for() { + let rate = |opts: EncodeOptions, sample_rate, channels| { + let asked = opts.bitrate(sample_rate, channels).unwrap(); + assert_eq!(FileEncoder::new(Cursor::new(Vec::new()), &opts, sample_rate, channels).unwrap().bitrate(), asked); + asked + }; + assert_eq!(rate(EncodeOptions::new(EncodeFormat::Mp3), 48_000, 2), Some(192_000)); + assert_eq!(rate(EncodeOptions::new(EncodeFormat::Mp3).with_bitrate(130_000), 48_000, 2), Some(128_000)); + assert_eq!(rate(EncodeOptions::new(EncodeFormat::Mp3).with_bitrate(u32::MAX), 22_050, 1), Some(160_000)); + assert_eq!(rate(EncodeOptions::new(EncodeFormat::Ac3).with_bitrate(191_999), 48_000, 2), Some(160_000)); + assert_eq!(rate(EncodeOptions::new(EncodeFormat::Ac3), 48_000, 6), Some(448_000)); + assert_eq!(rate(EncodeOptions::new(EncodeFormat::Eac3).with_bitrate(100_999), 48_000, 2), Some(100_000)); + assert_eq!(rate(EncodeOptions::new(EncodeFormat::Aac), 48_000, 2), Some(128_000)); + assert_eq!(rate(EncodeOptions::new(EncodeFormat::Aac).with_bitrate(128), 48_000, 2), Some(24_000)); + assert_eq!(rate(EncodeOptions::new(EncodeFormat::Aac).with_bitrate(u32::MAX), 8000, 1), Some(48_000)); + assert_eq!(rate(EncodeOptions::new(EncodeFormat::Aac).with_quality(0.5), 48_000, 2), None); + for format in [EncodeFormat::Wav, EncodeFormat::Aiff, EncodeFormat::Flac, EncodeFormat::Alac] { + assert_eq!(rate(EncodeOptions::new(format).with_bitrate(96_000), 48_000, 2), None, "{format:?}"); + } + // The size of a second of MP3 is the rate reported. + let buf = tones(48_000, 2, 48_000); + let opts = EncodeOptions::new(EncodeFormat::Mp3).with_bitrate(130_000); + assert_eq!(encode(&buf, &opts).unwrap().len() as u32, 43 * 1152 / 8 * opts.bitrate(48_000, 2).unwrap().unwrap() / 48_000); + assert!(is_unsupported(EncodeOptions::new(EncodeFormat::Mp3).bitrate(48_000, 6))); +} diff --git a/crates/audio/tests/codecs/main.rs b/crates/audio/tests/codecs/main.rs index 279246dc6..c1c9e2131 100644 --- a/crates/audio/tests/codecs/main.rs +++ b/crates/audio/tests/codecs/main.rs @@ -12,6 +12,7 @@ mod ac3_encoder_oracle; mod ac3_oracle; mod alac; mod concealment; +mod file_formats; mod mp3_decoder_oracle; mod mp3_encoder_oracle; mod mpa_oracle; diff --git a/crates/media/README.md b/crates/media/README.md index 8eca6ecb5..b7ad5be7a 100644 --- a/crates/media/README.md +++ b/crates/media/README.md @@ -243,6 +243,10 @@ let bytes = w.finish_faststart()?.into_inner(); // or finish() for moov-at-end `ipcm`/`fpcm` + `pcmC` in MP4. A PCM `write_sample` call can hold any whole number of frames, and the track timescale must equal the sample rate. - Edit lists: set `TrackConfig::edits` explicitly, or use `media_start` to get a single edit. +- Audio pre-roll: `TrackConfig::roll_distance` puts every sample in a `roll` sample group (`sgpd` + + `sbgp`). Set -1 for AAC with `media_start` at the priming: Apple's decoders apply that edit only + when the track has the group. +- A file starts at the writer's position: chunk offsets count from there. - Timecode: `add_timecode_track(video, TimecodeConfig, start_frame)` adds a `tmcd` track (with `gmhd`) and a `tref/tmcd` link from the video track. - `WriterOptions::metadata` writes `©xxx` text atoms to `udta`. diff --git a/crates/media/src/isobmff/mux.rs b/crates/media/src/isobmff/mux.rs index e38186e9f..bcadfbaeb 100644 --- a/crates/media/src/isobmff/mux.rs +++ b/crates/media/src/isobmff/mux.rs @@ -51,11 +51,17 @@ pub struct TrackConfig { pub media_start: Option, /// `hdlr` name; a sensible default is used when `None`. pub handler_name: Option, + /// Audio pre-roll: puts every sample in a `roll` sample group (`sgpd` + `sbgp`, ISO/IEC + /// 14496-12 §10.1) with this roll distance, the number of samples a decoder must decode before + /// (negative) a sample for its output to be correct; -1 for AAC. Apple's decoders take an AAC + /// track's priming from the edit list only when the track has this group. Written by + /// [`Mp4Writer`]; [`FragmentedWriter`] ignores it. + pub roll_distance: Option, } impl TrackConfig { pub fn new(entry: SampleEntry, timescale: u32) -> Self { - TrackConfig { entry, timescale, language: "und".into(), edits: Vec::new(), media_start: None, handler_name: None } + TrackConfig { entry, timescale, language: "und".into(), edits: Vec::new(), media_start: None, handler_name: None, roll_distance: None } } } @@ -543,6 +549,23 @@ fn write_stbl(b: &mut BoxBuf, t: &WTrack, qt: bool, shift: u64, force_co64: bool } } b.end(m); + if let Some(distance) = t.cfg.roll_distance + && sample_count > 0 + { + // One description, which every sample maps to. + let m = b.start_full(b"sgpd", 1, 0); + b.bytes(b"roll"); + b.u32(2); + b.u32(1); + b.i16(distance); + b.end(m); + let m = b.start_full(b"sbgp", 0, 0); + b.bytes(b"roll"); + b.u32(1); + b.u32(sample_count); + b.u32(1); + b.end(m); + } b.end(stbl); Ok(()) } @@ -592,12 +615,21 @@ fn validate_track(entry: &SampleEntry, timescale: u32) -> Result> { Ok(None) } +/// The position after `bytes` more. A writer that takes bytes past the last position a file can +/// have is an error here, not an overflow. +fn advance(pos: u64, bytes: usize) -> Result { + pos.checked_add(bytes as u64).ok_or_else(|| Error::Mux("the file would end past the last position".into())) +} + /// Progressive MP4/MOV writer: `ftyp`, then one `mdat` streamed to `W`, then `moov` at [`finish`](Self::finish) /// (or moved in front of `mdat` by [`finish_faststart`](Self::finish_faststart)). pub struct Mp4Writer { w: W, opts: WriterOptions, tracks: Vec, + /// The writer's position when the file was begun: chunk offsets count from here, the first + /// byte of the file, wherever in the writer that is. + start: u64, /// Offset of the placeholder box (8 bytes) preceding the mdat header. pad_pos: u64, mdat_pos: u64, @@ -608,18 +640,22 @@ pub struct Mp4Writer { } impl Mp4Writer { - /// Start a file: writes `ftyp`, a placeholder box and the `mdat` header. + /// Start a file at the writer's current position: writes `ftyp`, a placeholder box and the + /// `mdat` header. pub fn new(mut w: W, opts: WriterOptions) -> Result { let start = w.stream_position()?; let mut b = BoxBuf::new(); write_ftyp(&mut b, opts.brand); - let pad_pos = start + b.len() as u64; + let pad = b.len() as u64; b.leaf(if opts.brand == Brand::Mov { b"wide" } else { b"free" }, &[]); - let mdat_pos = start + b.len() as u64; + let mdat = b.len() as u64; b.leaf(b"mdat", &[]); + // A writer positioned at the very end of what a position can count has no room for the + // header; the positions below are sums that would overflow. + let pos = start.checked_add(b.len() as u64).ok_or_else(|| Error::Mux("the writer's position leaves no room for a file".into()))?; + let (pad_pos, mdat_pos) = (start + pad, start + mdat); w.write_all(&b.buf)?; - let pos = start + b.len() as u64; - Ok(Mp4Writer { w, opts, tracks: Vec::new(), pad_pos, mdat_pos, pos, last_track: None, chapters: Vec::new() }) + Ok(Mp4Writer { w, opts, tracks: Vec::new(), start, pad_pos, mdat_pos, pos, last_track: None, chapters: Vec::new() }) } /// Add a track; returns its index (track ids are index + 1). @@ -708,8 +744,8 @@ impl Mp4Writer { return Err(too_many_samples()); } self.w.write_all(s.data)?; - let offset = self.pos; - self.pos += s.data.len() as u64; + let offset = self.pos - self.start; + self.pos = advance(self.pos, s.data.len())?; let contiguous = self.last_track == Some(track); self.last_track = Some(track); let tr = &mut self.tracks[track]; @@ -749,8 +785,8 @@ impl Mp4Writer { }; let d = rescale(vd, vts, tts).max(tc_dur as u64).min(u32::MAX as u64) as u32; self.w.write_all(&start.to_be_bytes())?; - let off = self.pos; - self.pos += 4; + let off = self.pos - self.start; + self.pos = advance(self.pos, 4)?; self.last_track = None; let t = &mut self.tracks[i].t; t.stts.push((1, d)); @@ -807,13 +843,13 @@ impl Mp4Writer { } let id = self.tracks.len() as u32 + 1; let mut t = Tables::new(); - let off = self.pos; + let off = self.pos - self.start; for (i, c) in self.chapters.iter().enumerate() { let end = self.chapters.get(i + 1).map_or(duration, |n| n.start); let d = u32::try_from(end.saturating_sub(c.start)).map_err(|_| Error::Mux("chapter longer than 2^32 movie ticks".into()))?; let data = text_sample(&c.title); self.w.write_all(&data)?; - self.pos += data.len() as u64; + self.pos = advance(self.pos, data.len())?; Tables::push_run(&mut t.stts, 1, d); Tables::push_run(&mut t.ctts, 1, 0); t.sizes.push(data.len() as u32); @@ -1341,4 +1377,105 @@ mod tests { } assert_eq!(t.sync_sample_before(5), 3); } + + /// `roll_distance` puts every sample in one `roll` group; without it, or without samples, + /// the track has no sample group. + #[test] + fn roll_distance_is_a_sample_group_over_every_sample() { + let file = |roll: Option, samples: u32| { + let mut w = Mp4Writer::new(Cursor::new(Vec::new()), WriterOptions::new(Brand::Mp4)).unwrap(); + let mut cfg = TrackConfig::new(SampleEntry::aac(vec![0x11, 0x90], 2, 48_000), 48_000); + cfg.roll_distance = roll; + let t = w.add_track(cfg).unwrap(); + for _ in 0..samples { + w.write_sample(t, WriteSample { data: &[0; 8], duration: 1024, composition_offset: 0, is_sync: true }).unwrap(); + } + w.finish().unwrap().into_inner() + }; + let out = file(Some(-1), 5); + let f = crate::isobmff::open(out.as_slice()).unwrap(); + let groups = &f.tracks[0].sample_groups; + assert_eq!(groups.len(), 1); + assert_eq!((groups[0].grouping_type, groups[0].runs.as_slice()), (crate::isobmff::FourCc(*b"roll"), [(5, 1)].as_slice())); + assert_eq!(groups[0].descriptions, [(-1i16).to_be_bytes().to_vec()]); + assert_eq!(f.tracks[0].samples.len(), 5); + for out in [file(None, 5), file(Some(-1), 0)] { + assert!(crate::isobmff::open(out.as_slice()).unwrap().tracks[0].sample_groups.is_empty()); + } + // The boxes as ISO/IEC 14496-12 §8.9 has them (and ffmpeg writes them): `sgpd` version 1 + // with a default description length of 2, one description; `sbgp` version 0, one run. + let sgpd = [&26u32.to_be_bytes()[..], b"sgpd", &[1, 0, 0, 0], b"roll", &[0, 0, 0, 2, 0, 0, 0, 1, 0xFF, 0xFF]].concat(); + let sbgp = [&28u32.to_be_bytes()[..], b"sbgp", &[0, 0, 0, 0], b"roll", &[0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0, 1]].concat(); + let both = [sgpd, sbgp].concat(); + assert!(out.windows(both.len()).any(|w| w == both)); + } + + /// A writer positioned where the header's end would pass `u64::MAX` is refused, not overflowed. + #[test] + fn a_writer_at_the_end_of_the_position_range_is_refused() { + for back in [0, 1, 10, 27] { + let mut bytes = [0u8; 8]; + let mut sink = Cursor::new(&mut bytes[..]); + sink.set_position(u64::MAX - back); + assert!(matches!(Mp4Writer::new(sink, WriterOptions::new(Brand::Mp4)), Err(Error::Mux(_))), "{back} before the end"); + } + } + + /// A writer that keeps taking bytes where positions end makes writing fail, not overflow. + #[test] + fn samples_past_the_last_position_are_refused() { + struct Endless(u64); + impl Write for Endless { + fn write(&mut self, data: &[u8]) -> std::io::Result { + self.0 = self.0.wrapping_add(data.len() as u64); + Ok(data.len()) + } + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } + } + impl Seek for Endless { + fn seek(&mut self, _: SeekFrom) -> std::io::Result { + Ok(self.0) + } + } + let mut w = Mp4Writer::new(Endless(u64::MAX - 100), WriterOptions::new(Brand::Mp4)).unwrap(); + let v = w.add_track(TrackConfig::new(avc_entry(), 1000)).unwrap(); + let sample = WriteSample { data: &[0; 40], duration: 40, composition_offset: 0, is_sync: true }; + w.write_sample(v, sample).unwrap(); + assert!(matches!(w.write_sample(v, sample), Err(Error::Mux(_)))); + } + + /// A file begun in the middle of a writer is a file from there: its chunk offsets count from + /// its own first byte, with `moov` at the end or in front, a timecode track and chapters. + #[test] + fn a_file_begun_mid_writer_has_offsets_from_its_start() { + let file = |prefix: usize, faststart: bool| { + let mut sink = Cursor::new(vec![0xAA; prefix]); + sink.set_position(prefix as u64); + let mut opts = WriterOptions::new(Brand::Mov); + opts.chapters = vec![Chapter { start: 0, title: "one".into() }]; + let mut w = Mp4Writer::new(sink, opts).unwrap(); + let v = w.add_track(TrackConfig::new(avc_entry(), 1000)).unwrap(); + let tc = TimecodeConfig { timescale: 1000, frame_duration: 40, frames_per_second: 25, ..Default::default() }; + w.add_timecode_track(v, tc, 7).unwrap(); + for i in 0..4u8 { + w.write_sample(v, WriteSample { data: &[i + 1; 10], duration: 40, composition_offset: 0, is_sync: true }).unwrap(); + } + let out = if faststart { w.finish_faststart() } else { w.finish() }.unwrap(); + assert_eq!(out.position(), out.get_ref().len() as u64); + out.into_inner() + }; + for faststart in [false, true] { + let (plain, offset) = (file(0, faststart), file(100, faststart)); + assert!(offset[..100].iter().all(|&b| b == 0xAA)); + assert!(offset[100..] == plain, "faststart {faststart}"); + let f = crate::isobmff::open(plain.as_slice()).unwrap(); + assert_eq!(f.tracks.len(), 3); + for i in 0..4 { + assert_eq!(f.read_sample(plain.as_slice(), 0, i).unwrap(), vec![i as u8 + 1; 10]); + } + assert_eq!(f.read_sample(plain.as_slice(), 1, 0).unwrap(), 7u32.to_be_bytes()); + } + } }