mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 22:42:46 +00:00
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:
16 files changed
+1440
-79
No files matched your search
+1
-1
@@ -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" }
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
@@ -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(_))));
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
@@ -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..]));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user