Merge pull request #1242 from alehel/claude/issue-1230-c0c8eb

craft-audio: write AAC, ALAC, MP3, AC-3 and E-AC-3 files
This commit is contained in:
Dylan M. Taylor authored and GitHub committed 2026-11-14 11:46:38 -05:00
commit c9bf4a34a6
19 files changed
+1950 -99

No files matched your search

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