From eee76c3fc2034e91372407ca3858f32a660338ef Mon Sep 17 00:00:00 2001 From: alehel Date: Sat, 14 Nov 2026 01:28:23 +0100 Subject: [PATCH] craft-audio: AAC, ALAC, MP3, AC-3 and E-AC-3 as formats of encode and FileEncoder EncodeFormat gains Aac and Alac (M4A through craft-media's MP4 muxer) and Mp3, Ac3 and Eac3 (raw streams), so one call writes any of them from WAV-order f32 audio and EncodeOptions. - EncodeOptions::rate_control (with_bitrate, with_quality): a bit rate is moved to the nearest one the stream can carry; quality is AAC's VBR. - Channels are mapped onto the codec's order by speaker, from EncodeOptions::layout. A layout the format has not is Unsupported instead of reaching the wrong speakers. - M4A: the edit list skips the AAC priming, and the last sample's duration is cut so the track ends at the last frame written. - encode_format_for, ENCODE_EXTENSIONS and EncodeFormat::ALL list the new formats; aac-encode and alac-encode now turn on craft-media's isobmff. Refs #1230 --- Cargo.toml | 2 +- crates/audio/Cargo.toml | 9 +- crates/audio/README.md | 80 ++- crates/audio/examples/encode.rs | 8 +- crates/audio/src/aac/encoder/mod.rs | 10 + crates/audio/src/aac/mod.rs | 2 + crates/audio/src/ac3/mod.rs | 2 +- crates/audio/src/lib.rs | 132 +++- crates/audio/src/write/coded.rs | 581 ++++++++++++++++++ crates/audio/src/write/coded/tests.rs | 115 ++++ crates/audio/src/write/mod.rs | 138 +++-- crates/audio/src/write/pcm.rs | 3 + crates/audio/tests/audio/decode_extensions.rs | 14 +- crates/audio/tests/audio/encode_format.rs | 2 +- crates/audio/tests/codecs/file_formats.rs | 420 +++++++++++++ crates/audio/tests/codecs/main.rs | 1 + 16 files changed, 1440 insertions(+), 79 deletions(-) create mode 100644 crates/audio/src/write/coded.rs create mode 100644 crates/audio/src/write/coded/tests.rs create mode 100644 crates/audio/tests/codecs/file_formats.rs diff --git a/Cargo.toml b/Cargo.toml index 033e961b3..fc251abe0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,7 +85,7 @@ 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`. +# No default features: craft-audio turns on the craft-media modules each of its features needs. craft-latex = { path = "crates/latex" } craft-markdown = { path = "crates/markdown" } craft-math = { path = "crates/math" } 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..79f0efd46 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,6 +230,11 @@ 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 @@ -240,7 +245,43 @@ file gets and `mime_type()` its media type. `write` refuses a block without one 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. `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 Cs; L R C Ls Rs; 5.1; 6.1; and for 8 channels L R C LFE Ls Rs 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. + +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 +384,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 +451,17 @@ 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. +- `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. - 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 +529,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/lib.rs b/crates/audio/src/lib.rs index f8441a51d..f6be431dc 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. //! - [`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]; + 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,44 @@ 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, + } + } + + /// 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. + 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 +502,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 +515,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 +573,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..415d309fc --- /dev/null +++ b/crates/audio/src/write/coded.rs @@ -0,0 +1,581 @@ +//! 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, unused_mut) +)] + +use super::failed_write_error; +use super::pcm::validate_layout; +use crate::{AudioBuffer, AudioError, ChannelLayout, EncodeFormat, EncodeOptions, RateControl, Result, Speaker}; +use std::io::{Cursor, Seek, SeekFrom, 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 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 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, + }) +} + +/// 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(" ") +} + +/// For each channel the encoder takes, the channel of the caller's audio that feeds it. `mask` is +/// the layout the format has for the channel count, and `order` the encoder's channel order over +/// it (for each encoder channel, the WAV-order channel of `mask`; `None` when the encoder takes +/// WAV order). `layout` names the speakers of the caller's channels. `None` when `layout` is not +/// the format's layout: it is unordered, or has a speaker the format has not. +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(|| twin(speaker).and_then(|t| layout.index_of(t)))?; + if map.contains(&at) { + return None; + } + map.push(at); + } + Some(map) +} + +/// [`channel_map`] for a format with one layout per channel count, or why the audio does not fit. +fn fixed_map(name: &str, mask: u32, order: Option<&[usize]>, layout: &ChannelLayout) -> Result> { + if mask == 0 { + return Err(AudioError::Unsupported(format!("{name} cannot hold {} channels", layout.channels()))); + } + channel_map(mask, order, layout).ok_or_else(|| { + let given = match layout.mask() { + Some(m) => labels(m), + None => "no speaker assignment".into(), + }; + AudioError::Unsupported(format!("{name} holds {} channels as {}, not {given}; set `EncodeOptions::layout`", layout.channels(), labels(mask))) + }) +} + +/// The WAV mask of the speakers an AAC stream of `channels` channels has, as this crate's encoder +/// writes it and its decoder reports it: channel configurations 1–6, the 6.1 program config +/// element for 7, and configuration 7 (7.1 with a front pair left and right of centre) for 8. +#[cfg(feature = "aac-encode")] +fn aac_mask(channels: usize) -> u32 { + match channels { + 1 => 0x4, + 2 => 0x3, + 3 => 0x7, + 4 => 0x107, + 5 => 0x607, + 6 => 0x60F, + 7 => 0x70F, + 8 => 0xFF, + _ => 0, + } +} + +/// The encoder's channel order over [`aac_mask`]: [`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 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); + // 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 { + let given = match layout.mask() { + Some(m) => labels(m), + None => "no speaker assignment".into(), + }; + return Err(AudioError::Unsupported(format!("{name} has no layout of {} channels as {given}", layout.channels()))); + }; + 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}")), + } +} + +/// A sink seen from where the file starts: position 0 is the sink's position when the file was +/// begun. The MP4 muxer writes chunk offsets as stream positions, and they count from the first +/// byte of the file, which need not be the first byte of the sink. +#[cfg(any(feature = "aac-encode", feature = "alac-encode"))] +struct FromStart { + out: W, + start: u64, +} + +#[cfg(any(feature = "aac-encode", feature = "alac-encode"))] +impl Write for FromStart { + fn write(&mut self, data: &[u8]) -> std::io::Result { + self.out.write(data) + } + + fn write_all(&mut self, data: &[u8]) -> std::io::Result<()> { + self.out.write_all(data) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.out.flush() + } +} + +#[cfg(any(feature = "aac-encode", feature = "alac-encode"))] +impl Seek for FromStart { + fn seek(&mut self, to: SeekFrom) -> std::io::Result { + let to = match to { + SeekFrom::Start(at) => SeekFrom::Start( + self.start.checked_add(at).ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidInput, "seek past the end of the sink"))?, + ), + relative => relative, + }; + let at = self.out.seek(to)?; + at.checked_sub(self.start).ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidInput, "seek to before the start of the file")) + } +} + +/// 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(mut out: W, codec: &Codec, sample_rate: u32, channels: u32) -> Result { + use craft_media::isobmff::{Brand, Mp4Writer, WriterOptions}; + let start = out.stream_position()?; + let out = FromStart { out, start }; + 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)); + } + 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)?; + } + Ok(self.mux.finish().map_err(mux_error)?.out) + } +} + +/// 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, + 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) = match opts.format { + #[cfg(feature = "aac-encode")] + EncodeFormat::Aac => { + let map = fixed_map("AAC", aac_mask(channels), aac_order(channels), &layout)?; + let config = crate::aac::EncoderConfig { + mode: aac_mode(opts.rate_control, sample_rate, channels), + ..crate::aac::EncoderConfig::cbr(sample_rate, channels, 0) + }; + (Codec::Aac(Box::new(crate::aac::Encoder::new(config)?)), map) + } + #[cfg(feature = "alac-encode")] + EncodeFormat::Alac => { + let count = u8::try_from(channels).unwrap_or(u8::MAX); + let map = fixed_map("ALAC", crate::alac::channel_mask(count), 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::with_capacity(CHUNK); channels] }, map) + } + #[cfg(feature = "mp3-encode")] + EncodeFormat::Mp3 => { + let mask = match channels { + 1 => 0x4, + 2 => 0x3, + _ => 0, + }; + let map = fixed_map("MP3", mask, 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); + (Codec::Mp3(Box::new(crate::mp3::Encoder::new(crate::mp3::Config::cbr(sample_rate, channels, kbps))?)), map) + } + #[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)?; + (Codec::Ac3 { enc: Box::new(crate::ac3::encoder::Encoder::new(config)?), planes: vec![Vec::with_capacity(CHUNK); channels] }, map) + } + #[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, frames: 0, packets: Vec::new(), failed: false }) + } + + /// 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..4aeaac66e --- /dev/null +++ b/crates/audio/src/write/coded/tests.rs @@ -0,0 +1,115 @@ +use super::*; + +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 = aac_mask(channels); + 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_eq!(aac_mask(9), 0); + // 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(_)))); +} diff --git a/crates/audio/src/write/mod.rs b/crates/audio/src/write/mod.rs index 2355a7b05..c743965cb 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,13 @@ 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, whose edit list +/// makes the file decode to exactly `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 +39,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 +48,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 +57,57 @@ 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") }; + let (built, feature) = match format { + EncodeFormat::Wav | EncodeFormat::Aiff => (cfg!(feature = "pcm-encode"), "pcm-encode"), + EncodeFormat::Flac => (cfg!(feature = "flac-encode"), "flac-encode"), + EncodeFormat::Aac => (cfg!(feature = "aac-encode"), "aac-encode"), + EncodeFormat::Alac => (cfg!(feature = "alac-encode"), "alac-encode"), + EncodeFormat::Mp3 => (cfg!(feature = "mp3-encode"), "mp3-encode"), + EncodeFormat::Ac3 => (cfg!(feature = "ac3-encode"), "ac3-encode"), + EncodeFormat::Eac3 => (cfg!(feature = "eac3-encode"), "eac3-encode"), + }; if built { Ok(()) } else { Err(AudioError::Unsupported(format!("writing {format:?} needs the `{feature}` 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 +118,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,9 +150,8 @@ 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 +/// 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. /// @@ -125,30 +172,25 @@ 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 { +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 +198,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 +217,32 @@ 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 }) } /// 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 +265,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,6 +280,8 @@ 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(), } } } @@ -252,7 +310,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::*; 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..f6f021c65 100644 --- a/crates/audio/tests/audio/decode_extensions.rs +++ b/crates/audio/tests/audio/decode_extensions.rs @@ -62,11 +62,19 @@ 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) => { + assert!(named == format || (named, format) == (EncodeFormat::Aac, EncodeFormat::Alac), "{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..9affab350 --- /dev/null +++ b/crates/audio/tests/codecs/file_formats.rs @@ -0,0 +1,420 @@ +//! 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)); +} + +/// 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(1)).unwrap(); + encode(&buf, &EncodeOptions::new(EncodeFormat::Aac).with_bitrate(u32::MAX)).unwrap(); + // 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)); + } +} + +/// 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 at the length written (it applies the edit list), +/// 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); + assert_eq!(out[0].len(), 20_000, "AAC, {channels} channels"); + 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..])); + } +} 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;