craft-audio: AAC, ALAC, MP3, AC-3 and E-AC-3 as formats of encode and FileEncoder

EncodeFormat gains Aac and Alac (M4A through craft-media's MP4 muxer) and
Mp3, Ac3 and Eac3 (raw streams), so one call writes any of them from
WAV-order f32 audio and EncodeOptions.

- EncodeOptions::rate_control (with_bitrate, with_quality): a bit rate is
  moved to the nearest one the stream can carry; quality is AAC's VBR.
- Channels are mapped onto the codec's order by speaker, from
  EncodeOptions::layout. A layout the format has not is Unsupported
  instead of reaching the wrong speakers.
- M4A: the edit list skips the AAC priming, and the last sample's duration
  is cut so the track ends at the last frame written.
- encode_format_for, ENCODE_EXTENSIONS and EncodeFormat::ALL list the new
  formats; aac-encode and alac-encode now turn on craft-media's isobmff.

Refs #1230
This commit is contained in:
alehel committed 2026-11-14 03:16:51 +01:00
1 parent de7640a006
commit eee76c3fc2
16 files changed
+1440 -79

No files matched your search

+1 -1
View File
@@ -85,7 +85,7 @@ craft-keymap = { path = "crates/keymap" }
craft-assuan = { path = "crates/assuan" }
craft-kra = { path = "crates/kra" }
craft-audio = { path = "crates/audio" }
# No default features, so craft-audio's `encode` builds craft-media without `isobmff`.
# No default features: craft-audio turns on the craft-media modules each of its features needs.
craft-latex = { path = "crates/latex" }
craft-markdown = { path = "crates/markdown" }
craft-math = { path = "crates/math" }
+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`).
+64 -16
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,6 +230,11 @@ let wav = encode(&surround, &EncodeOptions::default().with_layout(ChannelLayout:
let mut enc = FileEncoder::new(std::fs::File::create("take.wav")?, &EncodeOptions::default(), 48_000, 2)?;
enc.write(&buf.channels, buf.frames())?; // one slice (or Vec) per channel
enc.finish()?;
// The coded formats go the same way: AAC in M4A at 192 kbit/s, 5.1 as E-AC-3, 16-bit ALAC.
let m4a = encode(&buf, &EncodeOptions::new(EncodeFormat::Aac).with_bitrate(192_000))?;
let eac3 = encode(&surround, &EncodeOptions::new(EncodeFormat::Eac3).with_layout(ChannelLayout::SURROUND_5_1_SIDE))?;
let alac = encode(&buf, &EncodeOptions::new(EncodeFormat::Alac).with_bit_depth(BitDepth::Int16))?;
```
`EncodeOptions::default()` is 24-bit WAV. `with_bwf` adds a BWF `bext` chunk and
@@ -240,7 +245,43 @@ file gets and `mime_type()` its media type. `write` refuses a block without one
shorter than `frames`, and writes nothing. After a write fails for another reason, every later
`write` and `finish` returns an error.
### Encode AAC
| `EncodeFormat` | File | Audio | Options used |
|---|---|---|---|
| `Wav`, `Aiff`, `Flac` | WAV / BWF / RF64, AIFF / AIFF-C, FLAC | any rate; FLAC up to 8 channels | `bit_depth`, `dither`, `layout` (WAV, FLAC), `bwf`, `force_rf64` (WAV) |
| `Alac` | M4A | 1–8 channels, any rate | `bit_depth` (16, 24, 32), `dither`, `layout` |
| `Aac` | M4A | AAC-LC, 1–8 channels, the 13 AAC rates (7350–96000 Hz) | `rate_control`, `layout` |
| `Mp3` | `.mp3` stream | mono or stereo; 32, 44.1, 48 kHz or 16, 22.05, 24 kHz | `rate_control` (bit rate), `layout` |
| `Ac3`, `Eac3` | `.ac3` / `.eac3` stream | up to 5.1 (E-AC-3: 7.1); 32, 44.1, 48 kHz | `rate_control` (bit rate), `layout` |
`with_bitrate(bits_per_second)` sets a constant bit rate for all channels together. A rate the
stream cannot carry is moved to the nearest one it can: MP3 and AC-3 use the highest rate of their
tables not above it, AAC and E-AC-3 keep it inside their range. `with_quality(0.0..=1.0)` is AAC's
constant quality (VBR); the other lossy formats return `Unsupported` for it. Without either, AAC
is written at 64 kbit/s per channel, MP3 at 96 kbit/s per channel, and AC-3 and E-AC-3 at
96 kbit/s per full-bandwidth channel (the LFE not counted).
The audio is in WAV channel order for every format; `layout` (the usual layout for the channel
count when unset) names the speakers, and the channels are reordered into the codec's own order.
Each coded format has a fixed set of layouts, and audio with other speakers is `Unsupported`
rather than written to the wrong ones:
- AAC: C; L R; L R C; L R C Cs; L R C Ls Rs; 5.1; 6.1; and for 8 channels L R C LFE Ls Rs Lc Rc
(a front pair left and right of centre, mask `0xFF`). ALAC: the same, with 6.1's back centre last.
WAV 7.1 (backs and sides) is not that 7.1, and quad (the usual 4-channel layout) has no centre:
both need another format.
- AC-3 and E-AC-3: the layout picks the coding mode (1/0 to 3/2, with or without LFE; E-AC-3 also
7.1 with backs and sides).
- A surround pair is a surround pair: side and back surrounds are interchangeable where the format
has one pair.
An M4A's edit list skips the AAC encoder's priming (1024 frames) and ends at the last frame
written, so the file decodes to the length that went in. An MP3, AC-3 or E-AC-3 stream has no
place for that (see [Limits](#limits)). For a container of your own, use the codec modules below.
### Encode AAC packets
For an `.m4a` file, use `EncodeFormat::Aac` above. `aac::Encoder` is the packet encoder behind it,
for muxing into a container of your own:
```rust
use craft_audio::AudioError;
@@ -343,14 +384,14 @@ match craft_audio::decode(&bytes, None) {
| WAV, BWF, RF64/BW64 | native | native |
| AIFF, AIFF-C | native | native |
| FLAC | symphonia | native |
| AAC-LC | native | native |
| AAC-LC | native | native; M4A through `encode` / `FileEncoder`, packets through `aac::Encoder` |
| HE-AAC v1 | native (SBR) | — |
| HE-AAC v2 | native, without parametric stereo (see [Limits](#limits)) | — |
| ALAC | native | native, packets (`alac::Encoder`; no file writer) |
| ALAC | native | native; M4A through `encode` / `FileEncoder`, packets through `alac::Encoder` |
| MP1, MP2 (MPEG audio Layer I/II) | native | — |
| MP3 (Layer III) | native | native, frames (`mp3::Encoder`; the frames in a row are an `.mp3` file) |
| Opus | native | native |
| AC-3, E-AC-3 | native | native packet encoder |
| MP3 (Layer III) | native | native; `.mp3` through `encode` / `FileEncoder`, frames through `mp3::Encoder` |
| Opus | native | native, packets (`opus::Encoder`; no file writer) |
| AC-3, E-AC-3 | native | native; `.ac3` / `.eac3` through `encode` / `FileEncoder`, packets through `ac3::encoder::Encoder` |
| Vorbis, ADPCM, A-law, µ-law | symphonia | — |
| Container | Demuxer | Timing |
@@ -410,10 +451,17 @@ match craft_audio::decode(&bytes, None) {
- `TrackDecoder` returns `Unsupported` for FLAC and Vorbis; `decode` and `Decoder` read
those through symphonia. ALAC in CAF still goes through symphonia (craft-media has no CAF
demuxer).
- ALAC: `EncodeFormat` has no ALAC entry; `alac::Encoder` writes packets and the configuration,
and the caller muxes them (MP4 `alac` box, Matroska `A_ALAC`). craft-media's MP4 muxer writes no
`chan` box, so an MP4 of more than two channels carries no explicit layout; decoders then use the
standard ALAC layout for the count.
- ALAC: craft-media's MP4 muxer writes no `chan` box, so an M4A of more than two channels (from
`EncodeFormat::Alac` or muxed from `alac::Encoder` packets) carries no explicit layout; decoders
then use the standard ALAC layout for the count, which is the one written.
- `EncodeFormat::Mp3`, `Ac3` and `Eac3` write raw streams, which cannot signal the codec's delay or
the length: the file decodes with 1057 (MP3) or 256 (AC-3, E-AC-3) frames of delay in front and
padded to whole frames at the end. MP3 gets no Xing / Info frame. `EncodeFormat::Aac` and `Alac`
(M4A) decode to the length written.
- `EncodeFormat::Aac` and `Alac` write the `moov` box after the audio (no fast start), and no
metadata. Raw ADTS AAC is not a file format of `encode`; `aac::EncoderConfig::with_adts` gives
the frames.
- This crate's file reader plays a raw E-AC-3 7.1 stream as its 5.1 independent substream.
- ALAC 32-bit streams: ffmpeg does not decode their uncompressed (escaped) stereo frames, which
Apple's encoder and this one write for audio that does not compress; Apple's decoder and this
one do.
@@ -481,7 +529,7 @@ finite count as silence and the rest are clamped to ±4, so no input makes an in
decoder's 7680-bit input buffer.
- Not yet: block switching (short blocks; transients pre-echo), `scfsi` and `preflag`, intensity
stereo, a tonality-aware model, VBR/ABR, the Xing/Info and LAME gapless tags (the delay is not
signalled), CRC, free format, and an `EncodeFormat` entry for `encode`/`FileEncoder`.
signalled), CRC and free format.
### AC-3 and E-AC-3 encoding
+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];
+120 -12
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.
//! - [`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];
pub const ALL: &[EncodeFormat] = &[
EncodeFormat::Wav,
EncodeFormat::Aiff,
EncodeFormat::Flac,
EncodeFormat::Alac,
EncodeFormat::Aac,
EncodeFormat::Mp3,
EncodeFormat::Ac3,
EncodeFormat::Eac3,
];
/// The extension a new file of this format gets, lower case and without the dot: `"wav"`,
/// `"aif"`, `"flac"`. [`encode_format_for`] maps it back.
/// `"aif"`, `"flac"`, `"m4a"` (AAC and ALAC), `"mp3"`, `"ac3"`, `"eac3"`.
/// [`encode_format_for`] maps it back, `"m4a"` to AAC.
pub fn extension(self) -> &'static str {
match self {
EncodeFormat::Wav => "wav",
EncodeFormat::Aiff => "aif",
EncodeFormat::Flac => "flac",
EncodeFormat::Aac | EncodeFormat::Alac => "m4a",
EncodeFormat::Mp3 => "mp3",
EncodeFormat::Ac3 => "ac3",
EncodeFormat::Eac3 => "eac3",
}
}
@@ -249,6 +293,10 @@ impl EncodeFormat {
EncodeFormat::Wav => "audio/wav",
EncodeFormat::Aiff => "audio/aiff",
EncodeFormat::Flac => "audio/flac",
EncodeFormat::Aac | EncodeFormat::Alac => "audio/mp4",
EncodeFormat::Mp3 => "audio/mpeg",
EncodeFormat::Ac3 => "audio/ac3",
EncodeFormat::Eac3 => "audio/eac3",
}
}
@@ -258,8 +306,44 @@ impl EncodeFormat {
EncodeFormat::Wav => Container::Wav,
EncodeFormat::Aiff => Container::Aiff,
EncodeFormat::Flac => Container::Flac,
EncodeFormat::Aac | EncodeFormat::Alac => Container::Mp4,
EncodeFormat::Mp3 => Container::MpegAudio,
EncodeFormat::Ac3 | EncodeFormat::Eac3 => Container::Ac3,
}
}
/// The codec of the audio this format writes.
pub fn codec(self) -> CodecId {
match self {
EncodeFormat::Wav | EncodeFormat::Aiff => CodecId::Pcm,
EncodeFormat::Flac => CodecId::Flac,
EncodeFormat::Aac => CodecId::Aac,
EncodeFormat::Alac => CodecId::Alac,
EncodeFormat::Mp3 => CodecId::Mp3,
EncodeFormat::Ac3 => CodecId::Ac3,
EncodeFormat::Eac3 => CodecId::Eac3,
}
}
/// Whether the format loses audio: AAC, MP3, AC-3 and E-AC-3. These take
/// [`EncodeOptions::rate_control`] and ignore the bit depth and dither.
pub fn is_lossy(self) -> bool {
matches!(self, EncodeFormat::Aac | EncodeFormat::Mp3 | EncodeFormat::Ac3 | EncodeFormat::Eac3)
}
}
/// How a lossy format spends bits ([`EncodeOptions::rate_control`]).
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum RateControl {
/// A constant bit rate in bits per second, for all channels together. A format with a fixed
/// set of rates (MP3, AC-3) uses the highest one not above it; a rate outside what the
/// stream can carry is moved to the nearest one inside.
Bitrate(u32),
/// A constant quality from 0.0 (smallest) to 1.0 (best), at whatever bit rate each frame
/// needs. AAC only; values outside the range are clamped, and NaN is an error.
Quality(f32),
}
/// Stored sample representation.
@@ -418,9 +502,9 @@ pub enum BitDepth {
pub struct EncodeOptions {
/// Output file format.
pub format: EncodeFormat,
/// Sample depth to write.
/// Sample depth to write (ignored by the lossy formats).
pub bit_depth: BitDepth,
/// Add TPDF dither when quantizing to an integer depth.
/// Add TPDF dither when quantizing to an integer depth (ignored by the lossy formats).
pub dither: bool,
/// Write a BWF `bext` chunk (WAV only; ignored for other formats).
pub bwf: Option<BwfInfo>,
@@ -431,15 +515,27 @@ pub struct EncodeOptions {
/// the count (they read 5 and 6 channels with side surrounds and 7 as 6.1 with sides, so the
/// usual layouts for those counts get one; see the README's limits); AIFF has a fixed channel
/// order and ignores it. It must have as many channels as the audio.
///
/// AAC, ALAC, MP3, AC-3 and E-AC-3 store channels in their own order, so here the layout says
/// which speaker each channel of the audio (in WAV order) is, and the channels are reordered
/// to match. Each of these formats has a fixed set of layouts (AAC and ALAC one per channel
/// count, AC-3 one per coding mode); a layout outside it is [`AudioError::Unsupported`]. Side
/// and back surrounds are taken as the same pair where the format has only one.
pub layout: Option<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 +573,18 @@ impl EncodeOptions {
self.force_rf64 = force_rf64;
self
}
/// Sets `rate_control` to a constant bit rate in bits per second ([`RateControl::Bitrate`]).
pub fn with_bitrate(mut self, bits_per_second: u32) -> Self {
self.rate_control = Some(RateControl::Bitrate(bits_per_second));
self
}
/// Sets `rate_control` to a constant quality from 0.0 to 1.0 ([`RateControl::Quality`]).
pub fn with_quality(mut self, quality: f32) -> Self {
self.rate_control = Some(RateControl::Quality(quality));
self
}
}
impl Default for EncodeOptions {
+581
View File
@@ -0,0 +1,581 @@
//! The coded formats of [`FileEncoder`](crate::FileEncoder): AAC-LC and ALAC in M4A (through
//! craft-media's MP4 muxer) and MP3, AC-3 and E-AC-3 as raw streams. [`Writer`] maps the caller's
//! WAV-order channels onto the encoder's order, drives the packet encoder, and for M4A records the
//! encoder's priming and the exact length in the edit list.
// Each codec is its own feature, and so are the two sinks: with only some of them built, the
// other halves of the shared code are unused.
#![cfg_attr(
not(all(feature = "aac-encode", feature = "alac-encode", feature = "mp3-encode", feature = "ac3-encode", feature = "eac3-encode")),
allow(dead_code, unused_imports, unused_variables, unused_mut)
)]
use super::failed_write_error;
use super::pcm::validate_layout;
use crate::{AudioBuffer, AudioError, ChannelLayout, EncodeFormat, EncodeOptions, RateControl, Result, Speaker};
use std::io::{Cursor, Seek, SeekFrom, Write};
/// Frames handed to the encoder at a time, so its buffers and the packets waiting to be written
/// stay small however long a block is. Also the frames of an ALAC packet.
const CHUNK: usize = 4096;
/// Bits per second per channel when no rate is asked for: AAC.
const AAC_DEFAULT_BPS_PER_CHANNEL: u32 = 64_000;
/// Kilobits per second per channel when no rate is asked for: MP3, and AC-3 and E-AC-3 (per
/// full-bandwidth channel).
const DEFAULT_KBPS_PER_CHANNEL: u32 = 96;
/// The surround speaker a layout may name in place of `speaker`: formats with one surround pair
/// do not tell side from back, so a pair of either kind feeds it.
fn twin(speaker: Speaker) -> Option<Speaker> {
Some(match speaker {
Speaker::SideLeft => Speaker::BackLeft,
Speaker::SideRight => Speaker::BackRight,
Speaker::BackLeft => Speaker::SideLeft,
Speaker::BackRight => Speaker::SideRight,
_ => return None,
})
}
/// The speakers of `mask` as labels: `"L R C LFE Ls Rs"`.
fn labels(mask: u32) -> String {
ChannelLayout::from_mask(mask).speakers().iter().map(|s| s.label()).collect::<Vec<_>>().join(" ")
}
/// For each channel the encoder takes, the channel of the caller's audio that feeds it. `mask` is
/// the layout the format has for the channel count, and `order` the encoder's channel order over
/// it (for each encoder channel, the WAV-order channel of `mask`; `None` when the encoder takes
/// WAV order). `layout` names the speakers of the caller's channels. `None` when `layout` is not
/// the format's layout: it is unordered, or has a speaker the format has not.
fn channel_map(mask: u32, order: Option<&[usize]>, layout: &ChannelLayout) -> Option<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(|| twin(speaker).and_then(|t| layout.index_of(t)))?;
if map.contains(&at) {
return None;
}
map.push(at);
}
Some(map)
}
/// [`channel_map`] for a format with one layout per channel count, or why the audio does not fit.
fn fixed_map(name: &str, mask: u32, order: Option<&[usize]>, layout: &ChannelLayout) -> Result<Vec<usize>> {
if mask == 0 {
return Err(AudioError::Unsupported(format!("{name} cannot hold {} channels", layout.channels())));
}
channel_map(mask, order, layout).ok_or_else(|| {
let given = match layout.mask() {
Some(m) => labels(m),
None => "no speaker assignment".into(),
};
AudioError::Unsupported(format!("{name} holds {} channels as {}, not {given}; set `EncodeOptions::layout`", layout.channels(), labels(mask)))
})
}
/// The WAV mask of the speakers an AAC stream of `channels` channels has, as this crate's encoder
/// writes it and its decoder reports it: channel configurations 1–6, the 6.1 program config
/// element for 7, and configuration 7 (7.1 with a front pair left and right of centre) for 8.
#[cfg(feature = "aac-encode")]
fn aac_mask(channels: usize) -> u32 {
match channels {
1 => 0x4,
2 => 0x3,
3 => 0x7,
4 => 0x107,
5 => 0x607,
6 => 0x60F,
7 => 0x70F,
8 => 0xFF,
_ => 0,
}
}
/// The encoder's channel order over [`aac_mask`]: [`crate::aac::aac_order`], and for 8 channels
/// configuration 7's C, Lc Rc, L R, Ls Rs, LFE over the WAV order L R C LFE Ls Rs Lc Rc.
#[cfg(feature = "aac-encode")]
fn aac_order(channels: usize) -> Option<&'static [usize]> {
if channels == 8 { Some(&[2, 6, 7, 0, 1, 4, 5, 3]) } else { crate::aac::aac_order(channels) }
}
/// The AAC rate control for `rate`: a bit rate is kept within what the stream can carry, a
/// quality of 0–1 spans the encoder's VBR range.
#[cfg(feature = "aac-encode")]
fn aac_mode(rate: Option<RateControl>, sample_rate: u32, channels: usize) -> crate::aac::BitrateMode {
use crate::aac::BitrateMode;
let (lo, hi) = crate::aac::cbr_range(sample_rate, channels);
// An empty range (a rate or channel count the encoder refuses anyway) leaves the rate alone.
let fit = |bps: u32| if lo <= hi { bps.clamp(lo, hi) } else { bps };
match rate {
None => BitrateMode::Cbr(fit(AAC_DEFAULT_BPS_PER_CHANNEL.saturating_mul(channels as u32))),
Some(RateControl::Bitrate(bps)) => BitrateMode::Cbr(fit(bps)),
// NaN stays NaN, which the encoder refuses.
Some(RateControl::Quality(q)) => BitrateMode::Vbr(1.0 + 4.0 * q.clamp(0.0, 1.0)),
}
}
/// The bit rate in kbit/s for a codec with constant bit rates only.
fn constant_kbps(name: &str, rate: Option<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 {
let given = match layout.mask() {
Some(m) => labels(m),
None => "no speaker assignment".into(),
};
return Err(AudioError::Unsupported(format!("{name} has no layout of {} channels as {given}", layout.channels())));
};
let full = config.mode.channels() as u32;
let want = constant_kbps(name, rate, DEFAULT_KBPS_PER_CHANNEL * full)?;
// What `Config::validate` asks of the rate: enough for the channels, and for AC-3 an entry of
// the frame size table, for E-AC-3 a syncframe its 11-bit word count can hold.
let least = if config.mode == Surround71 { 384 } else { 32 * full };
config.bitrate_kbps = match codec {
Codec::Ac3 => {
let floor = crate::ac3::BITRATES.iter().copied().find(|&r| r >= least).unwrap_or(least);
crate::ac3::BITRATES.iter().rev().copied().find(|&r| r <= want).filter(|&r| r >= floor).unwrap_or(floor)
}
Codec::Eac3 => {
let most = if config.mode == Surround71 { 1024 } else { (2048 * u64::from(sample_rate) / 96_000).min(1024) as u32 };
want.clamp(least, most.max(least))
}
};
Ok((config, map))
}
/// One encoded packet and the frames it decodes to.
struct Packet {
data: Vec<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}")),
}
}
/// A sink seen from where the file starts: position 0 is the sink's position when the file was
/// begun. The MP4 muxer writes chunk offsets as stream positions, and they count from the first
/// byte of the file, which need not be the first byte of the sink.
#[cfg(any(feature = "aac-encode", feature = "alac-encode"))]
struct FromStart<W> {
out: W,
start: u64,
}
#[cfg(any(feature = "aac-encode", feature = "alac-encode"))]
impl<W: Write> Write for FromStart<W> {
fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
self.out.write(data)
}
fn write_all(&mut self, data: &[u8]) -> std::io::Result<()> {
self.out.write_all(data)
}
fn flush(&mut self) -> std::io::Result<()> {
self.out.flush()
}
}
#[cfg(any(feature = "aac-encode", feature = "alac-encode"))]
impl<W: Seek> Seek for FromStart<W> {
fn seek(&mut self, to: SeekFrom) -> std::io::Result<u64> {
let to = match to {
SeekFrom::Start(at) => SeekFrom::Start(
self.start.checked_add(at).ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidInput, "seek past the end of the sink"))?,
),
relative => relative,
};
let at = self.out.seek(to)?;
at.checked_sub(self.start).ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidInput, "seek to before the start of the file"))
}
}
/// An M4A file being written: one audio track whose samples are the packets.
///
/// The last packet is held back until the length is known: its duration is cut to the audio's
/// last frame, so the sample durations add up to the priming plus the audio, and the edit that
/// skips the priming ends where the audio does. The movie timescale is the sample rate, which
/// keeps that edit exact.
#[cfg(any(feature = "aac-encode", feature = "alac-encode"))]
struct M4a<W: Write + Seek> {
mux: craft_media::isobmff::Mp4Writer<FromStart<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(mut out: W, codec: &Codec, sample_rate: u32, channels: u32) -> Result<Self> {
use craft_media::isobmff::{Brand, Mp4Writer, WriterOptions};
let start = out.stream_position()?;
let out = FromStart { out, start };
let entry = codec.sample_entry(sample_rate, channels, 0, 0).ok_or_else(|| AudioError::Unsupported("M4A holds AAC and ALAC".into()))?;
let mut options = WriterOptions::new(Brand::Mp4);
options.movie_timescale = sample_rate;
let mux = Mp4Writer::new(out, options).map_err(mux_error)?;
Ok(M4a { mux, track: None, entry, sample_rate, channels, priming: codec.priming(), held: None, frames: 0, bytes: 0 })
}
fn track(&mut self, has_audio: bool) -> Result<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));
}
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)?;
}
Ok(self.mux.finish().map_err(mux_error)?.out)
}
}
/// 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>,
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) = match opts.format {
#[cfg(feature = "aac-encode")]
EncodeFormat::Aac => {
let map = fixed_map("AAC", aac_mask(channels), aac_order(channels), &layout)?;
let config = crate::aac::EncoderConfig {
mode: aac_mode(opts.rate_control, sample_rate, channels),
..crate::aac::EncoderConfig::cbr(sample_rate, channels, 0)
};
(Codec::Aac(Box::new(crate::aac::Encoder::new(config)?)), map)
}
#[cfg(feature = "alac-encode")]
EncodeFormat::Alac => {
let count = u8::try_from(channels).unwrap_or(u8::MAX);
let map = fixed_map("ALAC", crate::alac::channel_mask(count), crate::alac::alac_order(count), &layout)?;
let bits = match opts.bit_depth {
crate::BitDepth::Int16 => 16,
crate::BitDepth::Int24 => 24,
crate::BitDepth::Int32 => 32,
crate::BitDepth::Float32 => return Err(AudioError::Unsupported("ALAC cannot store 32-bit float samples".into())),
};
let enc = crate::alac::Encoder::new(crate::alac::EncoderConfig::new(sample_rate, count, bits).with_frame_length(CHUNK as u32))?;
let quantizers = super::pcm::Quantizer::per_channel(u32::from(bits), opts.dither, channels);
(Codec::Alac { enc: Box::new(enc), quantizers, pending: vec![Vec::with_capacity(CHUNK); channels] }, map)
}
#[cfg(feature = "mp3-encode")]
EncodeFormat::Mp3 => {
let mask = match channels {
1 => 0x4,
2 => 0x3,
_ => 0,
};
let map = fixed_map("MP3", mask, None, &layout)?;
let want = constant_kbps("MP3", opts.rate_control, DEFAULT_KBPS_PER_CHANNEL * channels as u32)?;
// A sample rate MP3 has not is the encoder's to refuse.
let kbps = crate::mp3::nearest_bitrate(sample_rate, want).unwrap_or(want);
(Codec::Mp3(Box::new(crate::mp3::Encoder::new(crate::mp3::Config::cbr(sample_rate, channels, kbps))?)), map)
}
#[cfg(any(feature = "ac3-encode", feature = "eac3-encode"))]
EncodeFormat::Ac3 | EncodeFormat::Eac3 => {
let codec = if opts.format == EncodeFormat::Ac3 { crate::ac3::encoder::Codec::Ac3 } else { crate::ac3::encoder::Codec::Eac3 };
let (config, map) = ac3_config(codec, sample_rate, &layout, opts.rate_control)?;
(Codec::Ac3 { enc: Box::new(crate::ac3::encoder::Encoder::new(config)?), planes: vec![Vec::with_capacity(CHUNK); channels] }, map)
}
#[allow(unreachable_patterns)]
f => return Err(AudioError::Unsupported(format!("writing {f:?}"))),
};
let sink = match opts.format {
#[cfg(any(feature = "aac-encode", feature = "alac-encode"))]
EncodeFormat::Aac | EncodeFormat::Alac => Sink::M4a(Box::new(M4a::new(out, &codec, sample_rate, channels as u32)?)),
_ => Sink::Raw(out),
};
Ok(Writer { codec, sink, map, frames: 0, packets: Vec::new(), failed: false })
}
/// Hands the packets waiting to the sink.
fn drain(&mut self) -> Result<()> {
for packet in self.packets.drain(..) {
match &mut self.sink {
#[cfg(any(feature = "aac-encode", feature = "alac-encode"))]
Sink::M4a(m4a) => m4a.write(packet)?,
Sink::Raw(out) => out.write_all(&packet.data)?,
}
}
Ok(())
}
/// Appends `frames` frames of `block`, which holds every channel at least that long.
pub(crate) fn write<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;
+115
View File
@@ -0,0 +1,115 @@
use super::*;
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 = aac_mask(channels);
assert_eq!(mask.count_ones() as usize, channels);
let mut order = map(mask, aac_order(channels), mask).unwrap();
order.sort_unstable();
assert_eq!(order, (0..channels).collect::<Vec<_>>());
}
assert_eq!(aac_mask(9), 0);
// 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(_))));
}
+98 -40
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,13 @@ use crate::{AudioBuffer, AudioError, ChannelLayout, EncodeFormat, EncodeOptions,
pub use raw::{pcm_aiff_file, pcm_aiff_header, pcm_wav_file, pcm_wav_header};
use std::io::{Seek, Write};
/// Encode `buf` as WAV/BWF, AIFF/AIFF-C or FLAC. Integer depths clamp samples to [-1, 1]; float
/// Encode `buf` as the file `opts.format` names. Integer depths clamp samples to [-1, 1]; float
/// keeps values above full scale. `opts.layout`, when set, must have `buf`'s channel count.
///
/// The lossy formats take `opts.rate_control`. AAC and ALAC are written as M4A, whose edit list
/// makes the file decode to exactly `buf`'s frames. MP3, AC-3 and E-AC-3 are raw streams, which
/// have no place for that: they decode with the codec's delay in front and padded to whole frames.
///
/// ```
/// # #[cfg(feature = "pcm-encode")] {
/// use craft_audio::{AudioBuffer, BitDepth, EncodeFormat, EncodeOptions, encode};
@@ -32,6 +39,7 @@ use std::io::{Seek, Write};
pub fn encode(buf: &AudioBuffer, opts: &EncodeOptions) -> Result<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 +48,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 +57,57 @@ 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") };
let (built, feature) = match format {
EncodeFormat::Wav | EncodeFormat::Aiff => (cfg!(feature = "pcm-encode"), "pcm-encode"),
EncodeFormat::Flac => (cfg!(feature = "flac-encode"), "flac-encode"),
EncodeFormat::Aac => (cfg!(feature = "aac-encode"), "aac-encode"),
EncodeFormat::Alac => (cfg!(feature = "alac-encode"), "alac-encode"),
EncodeFormat::Mp3 => (cfg!(feature = "mp3-encode"), "mp3-encode"),
EncodeFormat::Ac3 => (cfg!(feature = "ac3-encode"), "ac3-encode"),
EncodeFormat::Eac3 => (cfg!(feature = "eac3-encode"), "eac3-encode"),
};
if built { Ok(()) } else { Err(AudioError::Unsupported(format!("writing {format:?} needs the `{feature}` feature"))) }
}
/// The file extensions of the formats [`encode`] can write (those whose features are on).
#[cfg(all(feature = "pcm-encode", feature = "flac-encode"))]
pub const ENCODE_EXTENSIONS: &[&str] = &["wav", "wave", "bwf", "aif", "aiff", "aifc", "flac"];
/// The file extensions of the formats [`encode`] can write (those whose features are on).
#[cfg(all(feature = "pcm-encode", not(feature = "flac-encode")))]
pub const ENCODE_EXTENSIONS: &[&str] = &["wav", "wave", "bwf", "aif", "aiff", "aifc"];
/// The file extensions of the formats [`encode`] can write (those whose features are on).
#[cfg(all(feature = "flac-encode", not(feature = "pcm-encode")))]
pub const ENCODE_EXTENSIONS: &[&str] = &["flac"];
/// Every extension [`ENCODE_EXTENSIONS`] can list, and how many of them the features built give.
const fn extensions() -> ([&'static str; 12], usize) {
let mut all = [""; 12];
let mut n = 0;
macro_rules! add {
($built:expr, $($extension:literal),+) => {
if $built {
$(
all[n] = $extension;
n += 1;
)+
}
};
}
add!(cfg!(feature = "pcm-encode"), "wav", "wave", "bwf", "aif", "aiff", "aifc");
add!(cfg!(feature = "flac-encode"), "flac");
add!(cfg!(any(feature = "aac-encode", feature = "alac-encode")), "m4a");
add!(cfg!(feature = "mp3-encode"), "mp3");
add!(cfg!(feature = "ac3-encode"), "ac3");
add!(cfg!(feature = "eac3-encode"), "eac3", "ec3");
(all, n)
}
/// The format [`encode`] writes for a file extension or format name (`wav`, `aiff`, `flac`,
/// case-insensitive, with or without the dot), or `None` when it cannot write it. `aifc` is
/// AIFF: float AIFF is written as AIFF-C.
const EXTENSIONS: ([&str; 12], usize) = extensions();
/// The file extensions of the formats [`encode`] can write (those whose features are on).
pub const ENCODE_EXTENSIONS: &[&str] = EXTENSIONS.0.split_at(EXTENSIONS.1).0;
/// The format [`encode`] writes for a file extension or format name (`wav`, `aiff`, `flac`, `m4a`,
/// `mp3`, `ac3`, `eac3`, case-insensitive, with or without the dot), or `None` when it cannot
/// write it. `aifc` is AIFF: float AIFF is written as AIFF-C. `m4a` is AAC when the AAC encoder
/// is built and ALAC otherwise; `alac` always names ALAC.
///
/// ```
/// # #[cfg(feature = "pcm-encode")] {
/// use craft_audio::{EncodeFormat, encode_format_for};
///
/// assert_eq!(encode_format_for(".AIFF"), Some(EncodeFormat::Aiff));
/// assert_eq!(encode_format_for("mp3"), None);
/// assert_eq!(encode_format_for("ogg"), None);
/// # }
/// ```
pub fn encode_format_for(name: &str) -> Option<EncodeFormat> {
@@ -82,6 +118,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,9 +150,8 @@ fn failed_write_error() -> AudioError {
AudioError::Encode("the stream encoder already failed on an earlier write; its output is incomplete".into())
}
/// Writes WAV/BWF (RF64 above 4 GiB), AIFF / AIFF-C `fl32` or FLAC block by block, producing
/// the same bytes [`encode`] would for the whole buffer: a file of any length never
/// has to fit in memory. The file starts at the sink's position, and nothing may follow that
/// Writes a file of any [`EncodeFormat`] block by block, producing the same bytes [`encode`]
/// would for the whole buffer: a file of any length never has to fit in memory. The file starts at the sink's position, and nothing may follow that
/// position (a new file, or one truncated there): the encoder cannot truncate a `Write + Seek`,
/// so the bytes of a longer file already there would stay after the new one.
///
@@ -125,30 +172,25 @@ fn failed_write_error() -> AudioError {
/// ```
pub struct FileEncoder<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> {
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 +198,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 +217,32 @@ 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 })
}
/// 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 +265,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,6 +280,8 @@ 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(),
}
}
}
@@ -252,7 +310,7 @@ pub fn wav_float_header(sample_rate: u32, channels: u16, frames: u64) -> Result<
// With only one of `pcm-encode` and `flac-encode`, the other's formats are refused, not silently
// written as something else.
#[cfg(all(test, not(all(feature = "pcm-encode", feature = "flac-encode"))))]
#[cfg(all(test, any(feature = "pcm-encode", feature = "flac-encode"), not(all(feature = "pcm-encode", feature = "flac-encode"))))]
mod tests {
use super::*;
+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};
+11 -3
View File
@@ -62,11 +62,19 @@ fn every_decode_extension_probes_and_decodes() {
fn every_encode_format_has_an_extension_it_is_written_and_read_under() {
for &format in EncodeFormat::ALL {
let ext = format.extension();
assert_eq!(craft_audio::encode_format_for(ext), Some(format), "{ext}");
assert!(craft_audio::ENCODE_EXTENSIONS.contains(&ext), "{ext}");
// The extension names the format, or for `m4a`, which two formats share, AAC before ALAC.
// A format whose feature is off has no name.
match craft_audio::encode_format_for(ext) {
Some(named) => {
assert!(named == format || (named, format) == (EncodeFormat::Aac, EncodeFormat::Alac), "{ext}");
assert!(craft_audio::ENCODE_EXTENSIONS.contains(&ext), "{ext}");
}
None => assert!(!matches!(format, EncodeFormat::Wav | EncodeFormat::Aiff | EncodeFormat::Flac), "{ext}"),
}
assert!(DECODE_EXTENSIONS.contains(&ext), "{ext}: what is written reads back");
assert!(format.mime_type().starts_with("audio/"), "{format:?}");
}
assert_eq!(EncodeFormat::ALL.iter().map(|f| f.extension()).collect::<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}");
}
}
+420
View File
@@ -0,0 +1,420 @@
//! The coded formats of `encode` and `FileEncoder` (AAC and ALAC in M4A; MP3, AC-3 and E-AC-3
//! streams), read back with this crate's decoder and, where ffmpeg is installed, with ffmpeg.
use std::io::{Cursor, Seek, SeekFrom, Write};
use std::process::Command;
use craft_audio::{
AudioBuffer, AudioError, BitDepth, ChannelLayout, CodecId, Container, ENCODE_EXTENSIONS, EncodeFormat, EncodeOptions, FileEncoder, RateControl,
decode, encode, encode_format_for,
};
const CODED: [EncodeFormat; 5] = [EncodeFormat::Aac, EncodeFormat::Alac, EncodeFormat::Mp3, EncodeFormat::Ac3, EncodeFormat::Eac3];
/// `channels` channels of `frames` frames, each channel a tone of its own frequency, on the
/// 16-bit grid (so a lossless 16-bit format returns it exactly).
fn tones(sample_rate: u32, channels: usize, frames: usize) -> AudioBuffer {
let channel = |c: usize| {
let step = (60.0 + 17.0 * c as f32) * std::f32::consts::TAU / sample_rate as f32;
(0..frames).map(|i| ((i as f32 * step).sin() * 0.4 * 32768.0).round() / 32768.0).collect()
};
AudioBuffer { sample_rate, channels: (0..channels).map(channel).collect() }
}
/// Signal-to-noise ratio in dB of `got` against `want`, over the frames both have.
fn snr(want: &[f32], got: &[f32]) -> f64 {
let (signal, noise) =
want.iter().zip(got).fold((0f64, 0f64), |(s, n), (&w, &g)| (s + f64::from(w).powi(2), n + (f64::from(w) - f64::from(g)).powi(2)));
10.0 * (signal / noise.max(1e-30)).log10()
}
fn is_unsupported<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));
}
/// 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(1)).unwrap();
encode(&buf, &EncodeOptions::new(EncodeFormat::Aac).with_bitrate(u32::MAX)).unwrap();
// Quality is AAC's; ALAC has no rate to control.
let (small, large) =
(size(&EncodeOptions::new(EncodeFormat::Aac).with_quality(0.0)), size(&EncodeOptions::new(EncodeFormat::Aac).with_quality(1.0)));
assert!(small < large, "{small} {large}");
assert!(matches!(encode(&buf, &EncodeOptions::new(EncodeFormat::Aac).with_quality(f32::NAN)), Err(AudioError::Encode(_))));
for format in [EncodeFormat::Mp3, EncodeFormat::Ac3, EncodeFormat::Eac3] {
assert!(is_unsupported(encode(&buf, &EncodeOptions::new(format).with_quality(0.5))), "{format:?}");
}
let alac = EncodeOptions::new(EncodeFormat::Alac);
assert_eq!(size(&alac), size(&alac.clone().with_bitrate(64_000)));
assert_eq!(alac.rate_control, None);
assert_eq!(alac.with_quality(0.5).rate_control, Some(RateControl::Quality(0.5)));
}
/// What a format cannot hold is refused before anything is written.
#[test]
fn what_a_format_cannot_hold_is_refused() {
let refused = |format, sample_rate, channels: u16, layout: Option<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));
}
}
/// 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 at the length written (it applies the edit list),
/// ALAC exactly, the raw streams behind their delay.
#[test]
fn ffmpeg_reads_the_files() {
let ff = require_ffmpeg!();
for (channels, layout) in [(1, None), (2, None), (6, None), (8, Some(ChannelLayout::from_mask(0xFF)))] {
let buf = tones(48_000, channels, 20_000);
let mut opts = EncodeOptions::new(EncodeFormat::Aac);
opts.layout = layout;
let out = ffmpeg_decode(&ff, &encode(&buf, &opts).unwrap(), &format!("aac_{channels}.m4a"), channels);
assert_eq!(out[0].len(), 20_000, "AAC, {channels} channels");
if channels <= 6 {
// ffmpeg also returns 5.1 in WAV order.
for (c, (want, got)) in buf.channels.iter().zip(&out).enumerate() {
assert!(snr(want, got) > 20.0, "AAC, {channels} channels, channel {c}: {} dB", snr(want, got));
}
}
let opts = opts.with_format(EncodeFormat::Alac).with_bit_depth(BitDepth::Int16);
let opts = if channels == 6 { opts.with_layout(ChannelLayout::SURROUND_5_1) } else { opts };
let out = ffmpeg_decode(&ff, &encode(&buf, &opts).unwrap(), &format!("alac_{channels}.m4a"), channels);
assert!(out == buf.channels, "ALAC, {channels} channels");
}
let buf = tones(44_100, 2, 20_000);
for (format, delay, floor) in
[(EncodeFormat::Mp3, craft_audio::mp3::DELAY as usize, 25.0), (EncodeFormat::Ac3, 256, 15.0), (EncodeFormat::Eac3, 256, 15.0)]
{
let name = format!("stream.{}", format.extension());
let out = ffmpeg_decode(&ff, &encode(&buf, &EncodeOptions::new(format)).unwrap(), &name, 2);
for (want, got) in buf.channels.iter().zip(&out) {
assert!(
got.len() >= 20_000 + delay && snr(want, &got[delay..]) > floor,
"{format:?}: {} frames, {} dB",
got.len(),
snr(want, &got[delay..])
);
}
}
// E-AC-3 7.1: the front channels, which the 5.1 downmix leaves as they are.
let buf = tones(48_000, 8, 20_000);
let out = ffmpeg_decode(&ff, &encode(&buf, &EncodeOptions::new(EncodeFormat::Eac3)).unwrap(), "stream_7_1.eac3", 8);
assert!(out[7].len() >= 20_256);
for c in 0..3 {
assert!(snr(&buf.channels[c], &out[c][256..]) > 12.0, "E-AC-3 7.1, channel {c}: {} dB", snr(&buf.channels[c], &out[c][256..]));
}
}
+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;