mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-13 22:12:12 +00:00
craft-audio: AC-3 and E-AC-3 packet encoders (#806)
* feat(audio): add AC-3 and E-AC-3 packet encoders * craft-audio: AC-3 encoder errors convert to AudioError::Encode --------- Co-authored-by: whatever-industries <77seventy77@users.noreply.github.com>
This commit is contained in:
1 parent
9f10d434d9
commit
2765c84bda
12 files changed
+2078
-22
No files matched your search
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "craft-audio"
|
||||
description = "Audio file I/O and codecs: WAV/AIFF read and write, FLAC encoder, AAC and ALAC decoders and encoders, Opus, AC-3/E-AC-3 and MP1/MP2 decoders."
|
||||
description = "Audio file I/O and codecs: WAV/AIFF read and write, FLAC encoder, AAC and ALAC decoders and encoders, AC-3/E-AC-3 decoder and encoder, Opus and MP1/MP2 decoders."
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
@@ -22,8 +22,8 @@ decode = ["decode-native", "symphonia"]
|
||||
decode-native = ["aac-decode", "alac-decode", "opus-decode", "ac3-decode", "mpa-decode", "craft-media/isobmff", "craft-media/matroska", "craft-media/ogg", "dep:log"]
|
||||
# The symphonia fallback for the formats with no native decoder yet.
|
||||
symphonia = ["decode-native", "dep:symphonia"]
|
||||
# Every encoder: the lossless file writers, the AAC encoder and the ALAC packet encoder.
|
||||
encode = ["lossless-encode", "aac-encode", "alac-encode"]
|
||||
# Every encoder: the lossless file writers and the AAC, ALAC and AC-3/E-AC-3 packet encoders.
|
||||
encode = ["lossless-encode", "aac-encode", "alac-encode", "ac3-encode", "eac3-encode"]
|
||||
# Encode WAV/BWF/RF64, AIFF/AIFF-C and FLAC files.
|
||||
lossless-encode = ["pcm-encode", "flac-encode"]
|
||||
# Encode WAV/BWF/RF64 and AIFF/AIFF-C files.
|
||||
@@ -39,6 +39,8 @@ mpa-decode = ["craft-media/bitstream"]
|
||||
# ALAC packet decoder and encoder (`alac`), ported from Apple's reference implementation.
|
||||
alac-decode = []
|
||||
alac-encode = []
|
||||
ac3-encode = []
|
||||
eac3-encode = []
|
||||
# Multi-resolution waveform overviews (`peaks`).
|
||||
peaks = []
|
||||
# Encode AAC channels in parallel (with `aac-encode`).
|
||||
|
||||
+79
-9
@@ -1,7 +1,7 @@
|
||||
# craft-audio
|
||||
|
||||
Audio file I/O and codecs for the Craft apps. Native implementations: WAV and AIFF read/write,
|
||||
FLAC encoder, AAC and ALAC decoders/encoders, Opus, AC-3/E-AC-3 and MP1/MP2 decoders.
|
||||
FLAC encoder, AAC and ALAC decoders/encoders, AC-3/E-AC-3 decoder/encoder, Opus and MP1/MP2 decoders.
|
||||
Formats without a native decoder are decoded by [symphonia](#symphonia), which is to be replaced
|
||||
by native decoders.
|
||||
|
||||
@@ -28,7 +28,7 @@ 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`) | the five 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 and ALAC packet encoders | `lossless-encode`, `aac-encode`, `alac-encode` |
|
||||
| `encode` | every encoder: the WAV, AIFF and FLAC file writers, and the AAC, ALAC and AC-3/E-AC-3 packet encoders | `lossless-encode`, `aac-encode`, `alac-encode`, `ac3-encode`, `eac3-encode` |
|
||||
| `lossless-encode` | WAV, AIFF and FLAC writing | `pcm-encode`, `flac-encode` |
|
||||
| `pcm-encode` | `encode`, `FileEncoder` for WAV and AIFF; `wav_float_header` | none |
|
||||
| `flac-encode` | `encode`, `FileEncoder` for FLAC | craft-media `bitstream` |
|
||||
@@ -36,7 +36,8 @@ No default features. The shared types (`AudioBuffer`, `AudioInfo`, `ChannelLayou
|
||||
| `alac-decode` | `alac::Decoder` | none |
|
||||
| `alac-encode` | `alac::Encoder` | none |
|
||||
| `opus-decode` | `opus::Decoder`/`StreamDecoder` | none |
|
||||
| `ac3-decode` | `ac3::Decoder` | none |
|
||||
| `ac3-decode` | `ac3::Decoder`, `ac3::ProgrammeDecoder` | none |
|
||||
| `ac3-encode`, `eac3-encode` | `ac3::encoder::Encoder` | none |
|
||||
| `mpa-decode` | `mpa::Decoder` | craft-media `bitstream` |
|
||||
| `peaks` | `peaks::Peaks` | none |
|
||||
| `rayon` | parallel AAC encoding (with `aac-encode`) | `rayon` |
|
||||
@@ -302,7 +303,7 @@ match craft_audio::decode(&bytes, None) {
|
||||
| MP1, MP2 (MPEG audio Layer I/II) | native | — |
|
||||
| MP3 (Layer III) | symphonia | — |
|
||||
| Opus | native | — |
|
||||
| AC-3, E-AC-3 | native | — |
|
||||
| AC-3, E-AC-3 | native | native packet encoder |
|
||||
| Vorbis, ADPCM, A-law, µ-law | symphonia | — |
|
||||
|
||||
| Container | Demuxer | Timing |
|
||||
@@ -335,8 +336,10 @@ match craft_audio::decode(&bytes, None) {
|
||||
the end a file declares is not presented.
|
||||
- A Matroska track's end comes from its timestamps, so it is known to one timestamp tick (usually
|
||||
1 ms) unless the last block has `DiscardPadding`.
|
||||
- E-AC-3: 7.1 programs decode as their 5.1 core. Adaptive hybrid transform and enhanced coupling
|
||||
return `Unsupported`.
|
||||
- E-AC-3: `ac3::Decoder` and `TrackDecoder` retain independent-substream decoding, so a
|
||||
7.1 programme is presented as its 5.1 compatibility downmix. Use `ac3::ProgrammeDecoder`
|
||||
with an explicit expected layout for complete six-block 7.1 access units; see below.
|
||||
Adaptive hybrid transform and enhanced coupling return `Unsupported`.
|
||||
- E-AC-3 with syncframes of fewer than six blocks: a seek decodes from the start of the stream,
|
||||
because such syncframes may reuse exponents from any syncframe before them. A seek in Matroska
|
||||
E-AC-3 reads the syncframe headers of the packets before its target to find the packets' places.
|
||||
@@ -401,10 +404,77 @@ Each codec module's rustdoc lists the coding tools it implements.
|
||||
| FLAC encoder | RFC 9639 | 16, 24, 32 bits; 1–8 channels, layouts decoders would not assume for the count as a `WAVEFORMATEXTENSIBLE_CHANNEL_MASK` comment; up to 655,350 Hz; LPC, stereo decorrelation, wasted bits, STREAMINFO MD5 | bit-exact through ffmpeg, STREAMINFO MD5 checked against ffmpeg |
|
||||
| AAC (`aac`) | ISO/IEC 14496-3 | AAC-LC decode and encode (CBR, VBR, ADTS), 1–8 channels; HE-AAC: see [Limits](#limits) | ffmpeg: ~135 dB SNR; PNS noise differs per sample at equal band energy |
|
||||
| Opus (`opus`) | RFC 6716, 8251, 7845 | decoder: SILK, hybrid, CELT, all frame sizes, PLC and FEC, multistream (`Decoder`) and single stream (`StreamDecoder`), 8–48 kHz output | all 36 RFC 8251 conformance cases |
|
||||
| AC-3/E-AC-3 (`ac3`) | ATSC A/52:2012 | all AC-3 channel modes and frame sizes; E-AC-3 independent substream 0 with spectral extension; output in WAV order, no downmix | ffmpeg: 65–94 dB SNR on tones, 27–48 dB on white noise (see [Limits](#limits)) |
|
||||
| AC-3/E-AC-3 (`ac3`) | decoder: ATSC A/52:2012; encoder/programme API: ETSI TS 102 366 V1.4.1 | single-syncframe and complete-programme decoding; packet encoding through discrete 7.1 (see below) | decoder comparisons and encoder output through ffmpeg, CRC/channel/timing/bit-budget regressions; synthetic quality limitations below |
|
||||
| MP1/MP2 (`mpa`) | ISO/IEC 11172-3, 13818-3 | Layer I and II, MPEG-1/2/2.5, 8–48 kHz; also the Layer II rate/mode pairs the spec disallows (ffmpeg's default mono 384 kbit/s); no free format | ffmpeg-encoded streams, 73–84 dB SNR |
|
||||
| ALAC (`alac`) | Apple's reference implementation | decode and encode, 1–8 channels, 16/20/24/32 bits (decode: any depth 1–32), fast mode, magic cookie in MP4/Matroska and CAF form | Apple's reference: same packets from the encoder, same samples from both decoders on 766 configurations; ffmpeg: bit-exact both ways |
|
||||
|
||||
### AC-3 and E-AC-3 encoding
|
||||
|
||||
`ac3::encoder` is implemented from **ETSI TS 102 366 V1.4.1 (September 2017)**,
|
||||
with the existing first-party decoder's normative allocation tables/math shared internally.
|
||||
No third-party codec implementation source is used. Decoder executables are interoperability
|
||||
oracles, not linked dependencies. This is a basic encoder implementation, not a Dolby-certified
|
||||
product or a claim of perceptual parity with mature commercial encoders.
|
||||
|
||||
The public `Config` chooses `Codec`, sample rate, programme bitrate, `CodingMode` and LFE.
|
||||
`Encoder::new` validates these before taking PCM. Both codecs accept 32, 44.1 and 48 kHz,
|
||||
all eight AC-3 coding modes (including distinct dual mono), and optional LFE. AC-3 uses
|
||||
its standardized 32–640 kb/s frame-size table; this implementation requires at least
|
||||
32 kb/s per full-bandwidth channel. E-AC-3 accepts 32–1024 kb/s programme rates, subject
|
||||
to that same minimum and the per-substream 2048-word limit. E-AC-3 discrete 7.1 requires
|
||||
LFE and at least 384 kb/s. Illegal rate/layout combinations return errors rather than
|
||||
changing the requested codec or silently reducing channel count.
|
||||
|
||||
PCM planes have equal lengths, finite values in ±1, and the explicit speaker order returned
|
||||
by `channel_layout()`. Values outside this input contract are rejected. Ordinary 5.1 uses
|
||||
**L, R, C, LFE, Ls, Rs**; discrete 7.1 uses WAVE order **L, R, C, LFE, Lrs, Rrs, Ls, Rs**.
|
||||
Dual mono is an unordered layout of programme 1, programme 2 and optional LFE. Mono surround
|
||||
and other intermediate modes retain their distinct speaker masks rather than using only a
|
||||
channel count.
|
||||
|
||||
The encoder performs windowed long/short transforms with transient switching, adaptive
|
||||
D15/D25/D45 exponent grouping and reuse, standardized masking/bit allocation, grouped and
|
||||
asymmetric mantissa quantization, and CRC generation. Rate control enforces both AC-3
|
||||
regional bit budgets as well as the total frame size; sparse frames use bounded skip fields
|
||||
so auxiliary padding cannot violate the final-three-eighths constraint. LFE receives a
|
||||
stateful six-pole 120 Hz low-pass before analysis. The initial implementation does not use
|
||||
channel coupling, stereo rematrixing, AHT, spectral extension, enhanced coupling or VBR;
|
||||
these are optional coding tools whose absence limits efficiency, particularly at low rates.
|
||||
The emitted dialogue normalization is 31 and no dynamic-range gain processing is requested.
|
||||
|
||||
A 7.1 E-AC-3 packet contains an independent 5.1 compatibility downmix followed by a dependent
|
||||
substream carrying the original side and rear pairs (`chanmap = 0x1a00`). The compatibility
|
||||
surrounds average side and rear inputs, **−6 dB per input**, so correlated full-scale signals
|
||||
retain headroom. This differs from the optional Annex H `dmixtyp=1` −3 dB mix; that metadata
|
||||
is not signalled. Full 7.1 decoding restores the original surrounds. The `dec3` descriptor
|
||||
reports one dependent substream and the added rear pair, not the replacement side pair.
|
||||
|
||||
`push` accepts arbitrary chunk sizes and returns complete 1536-sample access units.
|
||||
`finish` drains overlap and returns final packets plus **256 priming samples** and exact
|
||||
trailing padding; an empty stream returns no packets and zero trimming. A muxer should use
|
||||
these counts once in its presentation timing. Raw elementary streams do not carry the
|
||||
original arbitrary sample length, so raw-file consumers cannot recover this trimming by
|
||||
inspecting the bitstream alone. `codec_configuration()` returns the `dac3` or `dec3` payload,
|
||||
without its box header, for `ac-3`/`ec-3` sample entries. Dependent syncframes are part of the
|
||||
same timed packet, not separately timestamped samples. Input validation errors consume no
|
||||
PCM; an internal encoding error terminates that encoder, and its partial stream must be
|
||||
discarded rather than retrying input with ambiguous consumption.
|
||||
|
||||
`ac3::ProgrammeDecoder::new(expected_layout)` provides a separate, strict complete-access-unit
|
||||
path for the encoder's six-block E-AC-3 topology. It checks CRCs, bounds, substream order,
|
||||
layout, block counts and dependent channel maps before publishing PCM. Its explicit
|
||||
full-range policy does not apply metadata DRC independently to component substreams.
|
||||
Unsupported topology is rejected rather than reported as a successful reduced-channel decode.
|
||||
The older single-syncframe decoder's contract is preserved.
|
||||
|
||||
Validation includes all sample rates/coding modes, channel isolation, CRC and actual decoder
|
||||
bit-position checks, short/long transition reconstruction, LFE response, chunk invariance,
|
||||
invalid input and exact trim accounting. Independent executable checks cover original
|
||||
impulse/tone/noise boundary stimuli and 7.1 channel identities, plus externally
|
||||
produced 7.1 programme metadata. Synthetic SNR is a diagnostic, not a perceptual quality
|
||||
certificate: broad dialogue/music listening, platform player behaviour and peak-memory
|
||||
qualification remain separate checks. Lossy ringing can exceed the source PCM peak.
|
||||
|
||||
## symphonia
|
||||
|
||||
symphonia 0.6 (MPL-2.0, used unmodified as the Craft licensing rules require) decodes the formats
|
||||
@@ -508,8 +578,8 @@ The codec modules' public items keep their names, so apps can re-export them
|
||||
`Speaker::ALL` is a slice.
|
||||
- `TrackInfo::sample_rate` and `channels` are `Option`s.
|
||||
- Features are per codec. SoundCraft's `encode` (WAV, AIFF, FLAC) is now `lossless-encode`, made
|
||||
of `pcm-encode` (WAV, AIFF) and `flac-encode`. `encode` turns on every encoder, AAC
|
||||
included. Writing a format whose feature is off returns `AudioError::Unsupported`.
|
||||
of `pcm-encode` (WAV, AIFF) and `flac-encode`. `encode` turns on every encoder, including AAC,
|
||||
ALAC and AC-3/E-AC-3. Writing a format whose feature is off returns `AudioError::Unsupported`.
|
||||
- AAC encoder settings it cannot use are `aac::Error::InvalidSettings` (`AudioError::Encode`).
|
||||
- `TrackDecoder::decode` returns an error for a corrupt Opus packet, as for the other codecs;
|
||||
call `conceal`.
|
||||
|
||||
@@ -116,6 +116,10 @@ pub(crate) struct Bsi {
|
||||
/// reduced rates.
|
||||
pub fscod: usize,
|
||||
pub numblkscod: usize,
|
||||
/// Explicit dependent-substream channel assignments (Table E.1.4).
|
||||
pub chanmap: Option<u16>,
|
||||
/// Presence of compression metadata also marks the last dependent substream.
|
||||
pub compre: bool,
|
||||
}
|
||||
|
||||
/// Read syncinfo and bsi (§E2.2.1-2), leaving `r` at the audio frame.
|
||||
@@ -138,7 +142,8 @@ pub(crate) fn read_bsi(r: &mut Bits) -> Result<Bsi> {
|
||||
let acmod = r.read(3) as usize;
|
||||
let lfeon = r.bit();
|
||||
r.skip(5 + 5); // bsid, dialnorm
|
||||
if r.bit() {
|
||||
let compre = r.bit();
|
||||
if compre {
|
||||
r.skip(8); // compr
|
||||
}
|
||||
if acmod == 0 {
|
||||
@@ -147,9 +152,7 @@ pub(crate) fn read_bsi(r: &mut Bits) -> Result<Bsi> {
|
||||
r.skip(8); // compr2
|
||||
}
|
||||
}
|
||||
if strmtyp == 1 && r.bit() {
|
||||
r.skip(16); // chanmap
|
||||
}
|
||||
let chanmap = if strmtyp == 1 && r.bit() { Some(r.read(16) as u16) } else { None };
|
||||
if r.bit() {
|
||||
// mixing metadata
|
||||
if acmod > 2 {
|
||||
@@ -235,7 +238,7 @@ pub(crate) fn read_bsi(r: &mut Bits) -> Result<Bsi> {
|
||||
let l = r.read(6) as usize;
|
||||
r.skip((l + 1) * 8); // addbsi
|
||||
}
|
||||
Ok(Bsi { strmtyp, fscod: rate_code, numblkscod })
|
||||
Ok(Bsi { strmtyp, fscod: rate_code, numblkscod, chanmap, compre })
|
||||
}
|
||||
|
||||
/// The audio frame (§E2.2.3) and the state that runs from block to block within a syncframe.
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
//! Standards-derived AC-3 and E-AC-3 encoding (ETSI TS 102 366 V1.4.1).
|
||||
//!
|
||||
//! Input uses explicit WAVE speaker order. Each packet represents 1536 samples,
|
||||
//! including all dependent substreams for a 7.1 programme. The encoder uses the
|
||||
//! conventional transform/quantization tools; AHT and spectral extension are not used.
|
||||
//! Dual mono uses programme 1, programme 2, then optional LFE rather than a
|
||||
//! speaker mask. The 7.1 compatibility downmix averages each side/rear pair
|
||||
//! (−6 dB per input) to retain headroom; dependent data restores the originals.
|
||||
mod bitstream;
|
||||
mod transform;
|
||||
|
||||
use crate::ChannelLayout;
|
||||
|
||||
/// The output bitstream family.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Codec {
|
||||
/// AC-3 (`bsid = 8`).
|
||||
Ac3,
|
||||
/// Enhanced AC-3 (`bsid = 16`).
|
||||
Eac3,
|
||||
}
|
||||
|
||||
/// Explicit speaker topology. Dual mono is not stereo.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum CodingMode {
|
||||
/// Two independently controlled mono programmes.
|
||||
DualMono = 0,
|
||||
/// Centre.
|
||||
Mono = 1,
|
||||
/// Left and right.
|
||||
Stereo = 2,
|
||||
/// Left, centre and right.
|
||||
ThreeFront = 3,
|
||||
/// Left, right and mono surround.
|
||||
StereoSurround = 4,
|
||||
/// Left, centre, right and mono surround.
|
||||
ThreeFrontSurround = 5,
|
||||
/// Left, right and a surround pair.
|
||||
StereoTwoSurround = 6,
|
||||
/// Left, centre, right and a surround pair.
|
||||
ThreeFrontTwoSurround = 7,
|
||||
/// E-AC-3: independent 5.1 downmix plus dependent original side/rear channels.
|
||||
Surround71 = 8,
|
||||
}
|
||||
|
||||
impl CodingMode {
|
||||
/// Full-bandwidth channels, excluding LFE.
|
||||
pub fn channels(self) -> usize {
|
||||
if self == Self::Surround71 { 7 } else { super::NFCHANS[self as usize] }
|
||||
}
|
||||
}
|
||||
|
||||
/// Constant-rate encoding settings.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Config {
|
||||
/// AC-3 or E-AC-3.
|
||||
pub codec: Codec,
|
||||
/// PCM sampling frequency: 32000, 44100 or 48000 Hz.
|
||||
pub sample_rate: u32,
|
||||
/// Programme data rate, including any dependent substream, in kilobits/second.
|
||||
pub bitrate_kbps: u32,
|
||||
/// Speaker topology.
|
||||
pub mode: CodingMode,
|
||||
/// LFE channel present. Required for `Surround71`.
|
||||
pub lfe: bool,
|
||||
}
|
||||
|
||||
/// Invalid configuration, PCM or lifecycle use.
|
||||
#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
/// The requested configuration cannot be encoded.
|
||||
#[error("invalid AC-3 encoder settings: {0}")]
|
||||
InvalidSettings(&'static str),
|
||||
/// PCM channel lengths/counts or values are invalid.
|
||||
#[error("invalid AC-3 encoder input: {0}")]
|
||||
InvalidInput(&'static str),
|
||||
/// No more samples can be submitted after finalization.
|
||||
#[error("AC-3 encoder is already finished")]
|
||||
Finished,
|
||||
/// The configured frame cannot hold mandatory syntax.
|
||||
#[error("AC-3 frame bit budget exhausted")]
|
||||
BitBudget,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Check format limits and supported configurations before accepting PCM.
|
||||
pub fn validate(&self) -> Result<(), Error> {
|
||||
if !matches!(self.sample_rate, 32_000 | 44_100 | 48_000) {
|
||||
return Err(Error::InvalidSettings("sample rate must be 32000, 44100 or 48000 Hz"));
|
||||
}
|
||||
if self.mode == CodingMode::Surround71 && (self.codec != Codec::Eac3 || !self.lfe) {
|
||||
return Err(Error::InvalidSettings("7.1 requires E-AC-3 and LFE"));
|
||||
}
|
||||
match self.codec {
|
||||
Codec::Ac3 if !super::BITRATES.contains(&self.bitrate_kbps) => {
|
||||
return Err(Error::InvalidSettings("bit rate is not an AC-3 frame-size table entry"));
|
||||
}
|
||||
Codec::Eac3 if !(32..=1024).contains(&self.bitrate_kbps) => {
|
||||
return Err(Error::InvalidSettings("E-AC-3 programme bit rate must be 32..1024 kb/s"));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if self.codec == Codec::Eac3 {
|
||||
let words = (u64::from(self.bitrate_kbps) * 96_000).div_ceil(u64::from(self.sample_rate));
|
||||
let largest = if self.mode == CodingMode::Surround71 { words * 3 / 5 } else { words };
|
||||
if largest > 2048 {
|
||||
return Err(Error::InvalidSettings("E-AC-3 syncframe exceeds its 11-bit word count"));
|
||||
}
|
||||
}
|
||||
let minimum = if self.mode == CodingMode::Surround71 { 384 } else { 32 * self.mode.channels() as u32 };
|
||||
if self.bitrate_kbps < minimum {
|
||||
return Err(Error::InvalidSettings("bit rate is too low for the selected encoder channel topology"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Number of PCM planes.
|
||||
pub fn channels(&self) -> usize {
|
||||
self.mode.channels() + usize::from(self.lfe)
|
||||
}
|
||||
|
||||
/// Required planar PCM speaker order (ascending WAVE mask bits).
|
||||
pub fn channel_layout(&self) -> ChannelLayout {
|
||||
if self.mode == CodingMode::DualMono {
|
||||
return ChannelLayout::unordered(self.channels() as u16);
|
||||
}
|
||||
let mask = match self.mode {
|
||||
CodingMode::Mono => 0x4,
|
||||
CodingMode::Stereo => 0x3,
|
||||
CodingMode::ThreeFront => 0x7,
|
||||
CodingMode::StereoSurround => 0x103,
|
||||
CodingMode::ThreeFrontSurround => 0x107,
|
||||
CodingMode::StereoTwoSurround => 0x603,
|
||||
CodingMode::ThreeFrontTwoSurround => 0x607,
|
||||
CodingMode::Surround71 => 0x637,
|
||||
CodingMode::DualMono => 0,
|
||||
};
|
||||
ChannelLayout::from_mask(mask | if self.lfe { 8 } else { 0 })
|
||||
}
|
||||
}
|
||||
|
||||
/// One complete timed access unit, not an individual dependent syncframe.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Packet {
|
||||
/// Raw syncframes in independent/dependent order.
|
||||
pub data: Vec<u8>,
|
||||
/// Decoded samples per channel, including any encoder priming or padding.
|
||||
pub samples: u32,
|
||||
}
|
||||
|
||||
/// Final packets and exact container trimming information.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Finish {
|
||||
/// Access units emitted while draining transform overlap.
|
||||
pub packets: Vec<Packet>,
|
||||
/// Samples to trim at the beginning of the decoded stream.
|
||||
pub priming_samples: u32,
|
||||
/// Samples to trim at the end of the decoded stream.
|
||||
pub padding_samples: u32,
|
||||
}
|
||||
|
||||
/// Streaming encoder with bounded transform history.
|
||||
pub struct Encoder {
|
||||
config: Config,
|
||||
pending: Vec<Vec<f32>>,
|
||||
history: Vec<[f32; 256]>,
|
||||
frame_index: u64,
|
||||
input_samples: u64,
|
||||
finished: bool,
|
||||
lfe_filter: Option<(usize, transform::LowPass)>,
|
||||
}
|
||||
|
||||
impl Encoder {
|
||||
/// Construct a validated encoder; no PCM is consumed on error.
|
||||
pub fn new(config: Config) -> Result<Self, Error> {
|
||||
config.validate()?;
|
||||
Ok(Self {
|
||||
pending: vec![Vec::new(); config.channels()],
|
||||
history: vec![[0.0; 256]; config.channels()],
|
||||
config,
|
||||
frame_index: 0,
|
||||
input_samples: 0,
|
||||
finished: false,
|
||||
lfe_filter: config.lfe.then(|| {
|
||||
let index = if config.mode == CodingMode::Mono {
|
||||
1
|
||||
} else if matches!(
|
||||
config.mode,
|
||||
CodingMode::ThreeFront | CodingMode::ThreeFrontSurround | CodingMode::ThreeFrontTwoSurround | CodingMode::Surround71
|
||||
) {
|
||||
3
|
||||
} else {
|
||||
2
|
||||
};
|
||||
(index, transform::LowPass::new(config.sample_rate))
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
/// Required input speaker assignments.
|
||||
pub fn channel_layout(&self) -> ChannelLayout {
|
||||
self.config.channel_layout()
|
||||
}
|
||||
|
||||
/// The analysis window introduces 256 decoded priming samples.
|
||||
pub fn priming_samples(&self) -> u32 {
|
||||
256
|
||||
}
|
||||
|
||||
/// Payload of an ISO BMFF `dac3` or `dec3` configuration box, without its box header.
|
||||
pub fn codec_configuration(&self) -> Vec<u8> {
|
||||
bitstream::configuration(self.config)
|
||||
}
|
||||
|
||||
/// Accept equally sized finite PCM planes in the documented WAVE order.
|
||||
/// Values outside ±1 are rejected rather than silently clipped. Input errors
|
||||
/// leave the encoder untouched. A frame-encoding error is terminal; discard
|
||||
/// the stream rather than retrying input whose consumption is then unspecified.
|
||||
pub fn push(&mut self, planes: &[&[f32]]) -> Result<Vec<Packet>, Error> {
|
||||
if self.finished {
|
||||
return Err(Error::Finished);
|
||||
}
|
||||
if planes.len() != self.config.channels() {
|
||||
return Err(Error::InvalidInput("channel count differs from configuration"));
|
||||
}
|
||||
let count = planes.first().map_or(0, |p| p.len());
|
||||
if planes.iter().any(|p| p.len() != count || p.iter().any(|v| !v.is_finite() || v.abs() > 1.0)) {
|
||||
return Err(Error::InvalidInput("PCM must be finite, normalized and have equal plane lengths"));
|
||||
}
|
||||
self.input_samples =
|
||||
self.input_samples.checked_add(count as u64).filter(|&n| n <= u64::MAX - 1791).ok_or(Error::InvalidInput("sample count overflow"))?;
|
||||
let mut packets = Vec::new();
|
||||
let mut offset = 0;
|
||||
while offset < count {
|
||||
let take = (1536 - self.pending[0].len()).min(count - offset);
|
||||
for (ch, (dst, src)) in self.pending.iter_mut().zip(planes).enumerate() {
|
||||
if let Some((lfe, filter)) = &mut self.lfe_filter
|
||||
&& ch == *lfe
|
||||
{
|
||||
dst.extend(src[offset..offset + take].iter().map(|&value| filter.process(value)));
|
||||
} else {
|
||||
dst.extend_from_slice(&src[offset..offset + take]);
|
||||
}
|
||||
}
|
||||
offset += take;
|
||||
if self.pending[0].len() == 1536 {
|
||||
packets.push(self.emit()?);
|
||||
}
|
||||
}
|
||||
Ok(packets)
|
||||
}
|
||||
|
||||
/// Drain the last overlap and return exact priming/padding counts. Empty input emits no packets.
|
||||
pub fn finish(&mut self) -> Result<Finish, Error> {
|
||||
if self.finished {
|
||||
return Err(Error::Finished);
|
||||
}
|
||||
if self.input_samples == 0 {
|
||||
self.finished = true;
|
||||
return Ok(Finish { packets: Vec::new(), priming_samples: 0, padding_samples: 0 });
|
||||
}
|
||||
let padded = self.input_samples.checked_add(256).ok_or(Error::InvalidInput("sample count overflow"))?;
|
||||
let required = padded.div_ceil(1536);
|
||||
let decoded_samples = required.checked_mul(1536).ok_or(Error::InvalidInput("sample count overflow"))?;
|
||||
let mut packets = Vec::new();
|
||||
while self.frame_index < required {
|
||||
for (ch, p) in self.pending.iter_mut().enumerate() {
|
||||
if let Some((lfe, filter)) = &mut self.lfe_filter
|
||||
&& ch == *lfe
|
||||
{
|
||||
while p.len() < 1536 {
|
||||
p.push(filter.process(0.0));
|
||||
}
|
||||
} else {
|
||||
p.resize(1536, 0.0);
|
||||
}
|
||||
}
|
||||
packets.push(self.emit()?);
|
||||
}
|
||||
self.finished = true;
|
||||
Ok(Finish { packets, priming_samples: 256, padding_samples: (decoded_samples - padded) as u32 })
|
||||
}
|
||||
|
||||
fn emit(&mut self) -> Result<Packet, Error> {
|
||||
let mut spectra = vec![Vec::with_capacity(6); self.config.channels()];
|
||||
let mut short = [false; 6];
|
||||
for (block, short_block) in short.iter_mut().enumerate() {
|
||||
let windows: Vec<[f32; 512]> = (0..self.config.channels())
|
||||
.map(|ch| {
|
||||
let mut window = [0.0; 512];
|
||||
if block == 0 {
|
||||
window[..256].copy_from_slice(&self.history[ch]);
|
||||
} else {
|
||||
window[..256].copy_from_slice(&self.pending[ch][(block - 1) * 256..block * 256]);
|
||||
}
|
||||
window[256..].copy_from_slice(&self.pending[ch][block * 256..(block + 1) * 256]);
|
||||
window
|
||||
})
|
||||
.collect();
|
||||
*short_block =
|
||||
windows.iter().enumerate().any(|(ch, w)| self.lfe_filter.as_ref().is_none_or(|(lfe, _)| ch != *lfe) && transform::has_transient(w));
|
||||
for (ch, window) in windows.iter().enumerate() {
|
||||
let full_bandwidth = self.lfe_filter.as_ref().is_none_or(|(lfe, _)| ch != *lfe);
|
||||
spectra[ch].push(transform::analysis(window, *short_block && full_bandwidth));
|
||||
}
|
||||
}
|
||||
let data = match bitstream::encode(self.config, self.frame_index, &spectra, &short) {
|
||||
Ok(data) => data,
|
||||
Err(error) => {
|
||||
self.finished = true;
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
for (history, pending) in self.history.iter_mut().zip(&mut self.pending) {
|
||||
history.copy_from_slice(&pending[1280..1536]);
|
||||
pending.clear();
|
||||
}
|
||||
self.frame_index += 1;
|
||||
Ok(Packet { data, samples: 1536 })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,631 @@
|
||||
//! Syntax writer and bounded rate control; ETSI TS 102 366 §§4, 6, 7 and Annex E/F.
|
||||
use super::super::{AllocParams, Globals, bit_allocation};
|
||||
use super::{Codec, CodingMode, Config, Error};
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct Writer {
|
||||
bytes: Vec<u8>,
|
||||
bits: usize,
|
||||
}
|
||||
impl Writer {
|
||||
fn put(&mut self, value: u32, count: u32) {
|
||||
for bit in (0..count).rev() {
|
||||
if self.bits.is_multiple_of(8) {
|
||||
self.bytes.push(0);
|
||||
}
|
||||
if value & (1 << bit) != 0 {
|
||||
self.bytes[self.bits / 8] |= 1 << (7 - self.bits % 8);
|
||||
}
|
||||
self.bits += 1;
|
||||
}
|
||||
}
|
||||
fn append(&mut self, other: &Self) {
|
||||
for bit in 0..other.bits {
|
||||
self.put(u32::from((other.bytes[bit / 8] >> (7 - bit % 8)) & 1), 1);
|
||||
}
|
||||
}
|
||||
fn flag(&mut self, value: bool) {
|
||||
self.put(u32::from(value), 1);
|
||||
}
|
||||
}
|
||||
|
||||
fn rate_code(rate: u32) -> usize {
|
||||
match rate {
|
||||
48_000 => 0,
|
||||
44_100 => 1,
|
||||
_ => 2,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn configuration(config: Config) -> Vec<u8> {
|
||||
let mut w = Writer::default();
|
||||
let acmod = if config.mode == CodingMode::Surround71 { 7 } else { config.mode as u32 };
|
||||
match config.codec {
|
||||
Codec::Ac3 => {
|
||||
w.put(rate_code(config.sample_rate) as u32, 2);
|
||||
w.put(8, 5);
|
||||
w.put(0, 3);
|
||||
w.put(acmod, 3);
|
||||
w.flag(config.lfe);
|
||||
w.put(super::super::BITRATES.iter().position(|&b| b == config.bitrate_kbps).unwrap_or(0) as u32, 5);
|
||||
w.put(0, 5);
|
||||
}
|
||||
Codec::Eac3 => {
|
||||
// Ceiling frame rate describes the maximum rate when 44.1 kHz needs alternating sizes.
|
||||
let words = (u64::from(config.bitrate_kbps) * 96_000).div_ceil(u64::from(config.sample_rate));
|
||||
let max_rate = (words * 16 * u64::from(config.sample_rate)).div_ceil(1_536_000);
|
||||
w.put(max_rate as u32, 13);
|
||||
w.put(0, 3); // one independent substream
|
||||
w.put(rate_code(config.sample_rate) as u32, 2);
|
||||
w.put(16, 5);
|
||||
w.put(0, 2);
|
||||
w.put(0, 3);
|
||||
w.put(acmod, 3);
|
||||
w.flag(config.lfe);
|
||||
w.put(0, 3);
|
||||
let dependent = config.mode == CodingMode::Surround71;
|
||||
w.put(u32::from(dependent), 4);
|
||||
if dependent {
|
||||
w.put(2, 9);
|
||||
} else {
|
||||
w.put(0, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
w.bytes
|
||||
}
|
||||
|
||||
pub(super) fn encode(config: Config, frame_index: u64, spectra: &[Vec<[f32; 256]>], short: &[bool; 6]) -> Result<Vec<u8>, Error> {
|
||||
if config.mode == CodingMode::Surround71 {
|
||||
// Input WAVE order: L R C LFE Lrs Rrs Ls Rs. Independent stream is a
|
||||
// compatible 5.1 downmix; the dependent stream restores original surrounds.
|
||||
// Averaging (−6 dB per input) keeps correlated full-scale sides/rears
|
||||
// bounded. This intentionally is not Annex H dmixtyp=1's −3 dB mix;
|
||||
// that optional metadata is not emitted.
|
||||
let mut primary =
|
||||
vec![spectra[0].clone(), spectra[2].clone(), spectra[1].clone(), spectra[6].clone(), spectra[7].clone(), spectra[3].clone()];
|
||||
for block in 0..6 {
|
||||
for bin in 0..256 {
|
||||
primary[3][block][bin] = (spectra[6][block][bin] + spectra[4][block][bin]) * 0.5;
|
||||
primary[4][block][bin] = (spectra[7][block][bin] + spectra[5][block][bin]) * 0.5;
|
||||
}
|
||||
}
|
||||
let dependent = vec![spectra[6].clone(), spectra[7].clone(), spectra[4].clone(), spectra[5].clone()];
|
||||
let total_words = frame_words(config, frame_index);
|
||||
let primary_words = total_words * 3 / 5;
|
||||
let mut output = syncframe(config, 7, true, 0, primary_words, &primary, short)?;
|
||||
output.extend(syncframe(config, 6, false, 1, total_words - primary_words, &dependent, short)?);
|
||||
return Ok(output);
|
||||
}
|
||||
let nf = config.mode.channels();
|
||||
let fronts = match config.mode {
|
||||
CodingMode::Mono => 1,
|
||||
CodingMode::ThreeFront | CodingMode::ThreeFrontSurround | CodingMode::ThreeFrontTwoSurround => 3,
|
||||
_ => 2,
|
||||
};
|
||||
let mut order: Vec<usize> = (0..nf).map(|ch| if config.lfe && ch >= fronts { ch + 1 } else { ch }).collect();
|
||||
if fronts == 3 {
|
||||
order.swap(1, 2);
|
||||
}
|
||||
if config.lfe {
|
||||
order.push(fronts);
|
||||
}
|
||||
let coded: Vec<_> = order.iter().map(|&ch| spectra[ch].clone()).collect();
|
||||
syncframe(config, config.mode as u8, config.lfe, 0, frame_words(config, frame_index), &coded, short)
|
||||
}
|
||||
|
||||
fn frame_words(config: Config, index: u64) -> usize {
|
||||
let numerator = u64::from(config.bitrate_kbps) * 96_000;
|
||||
let denominator = u64::from(config.sample_rate);
|
||||
// An accumulator without unbounded state drift or long-stream multiplication overflow.
|
||||
let floor = numerator / denominator;
|
||||
let remainder = numerator % denominator;
|
||||
let phase = (index % denominator) * remainder % denominator;
|
||||
(floor + u64::from(phase + remainder >= denominator)) as usize
|
||||
}
|
||||
|
||||
struct Channel {
|
||||
coefficients: Vec<[f32; 256]>,
|
||||
exponents: [[u8; 256]; 6],
|
||||
codes: [Vec<u8>; 6],
|
||||
strategies: [u8; 6],
|
||||
end: usize,
|
||||
}
|
||||
|
||||
fn channels(spectra: &[Vec<[f32; 256]>], nf: usize, bandwidth: usize, short: &[bool; 6]) -> Vec<Channel> {
|
||||
spectra
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, blocks)| {
|
||||
let end = if index == nf { 7 } else { 73 + 3 * bandwidth };
|
||||
let raw: Vec<[u8; 256]> = blocks
|
||||
.iter()
|
||||
.map(|block| {
|
||||
std::array::from_fn(|bin| {
|
||||
let peak = block[bin].abs();
|
||||
if peak > 0.0 { (-peak.log2()).floor().clamp(0.0, 24.0) as u8 } else { 24 }
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let mut starts = vec![0];
|
||||
let mut start = 0;
|
||||
for block in 1..6 {
|
||||
let difference: u32 = (0..end).map(|bin| u32::from(raw[block][bin].abs_diff(raw[start][bin]))).sum();
|
||||
if (index < nf && short[block] != short[block - 1]) || difference > end as u32 * 2 {
|
||||
starts.push(block);
|
||||
start = block;
|
||||
}
|
||||
}
|
||||
starts.push(6);
|
||||
let mut channel = Channel {
|
||||
coefficients: blocks.clone(),
|
||||
exponents: [[24; 256]; 6],
|
||||
codes: std::array::from_fn(|_| Vec::new()),
|
||||
strategies: [0; 6],
|
||||
end,
|
||||
};
|
||||
for range in starts.windows(2) {
|
||||
let (first, last) = (range[0], range[1]);
|
||||
let strategy = if index == nf || last - first >= 4 {
|
||||
1
|
||||
} else if last - first >= 2 {
|
||||
2
|
||||
} else {
|
||||
3
|
||||
};
|
||||
let size = 1usize << (strategy - 1);
|
||||
let groups = (end - 1 + 3 * size - 3) / (3 * size);
|
||||
let mut envelope = [24u8; 256];
|
||||
for bin in 0..end {
|
||||
envelope[bin] = (first..last).map(|block| raw[block][bin]).min().unwrap_or(24);
|
||||
}
|
||||
let mut codes = Vec::with_capacity(1 + groups * 3);
|
||||
codes.push(envelope[0].min(15));
|
||||
for group in 0..groups * 3 {
|
||||
codes.push((0..size).map(|i| envelope[(1 + group * size + i).min(end - 1)]).min().unwrap_or(24));
|
||||
}
|
||||
for i in 1..codes.len() {
|
||||
codes[i] = codes[i].min(codes[i - 1] + 2);
|
||||
}
|
||||
for i in (0..codes.len() - 1).rev() {
|
||||
codes[i] = codes[i].min(codes[i + 1] + 2);
|
||||
}
|
||||
let reconstructed: [u8; 256] =
|
||||
std::array::from_fn(|bin| if bin == 0 { codes[0] } else { codes.get(1 + (bin - 1) / size).copied().unwrap_or(24) });
|
||||
for block in first..last {
|
||||
channel.exponents[block] = reconstructed;
|
||||
}
|
||||
channel.strategies[first] = strategy;
|
||||
channel.codes[first] = codes;
|
||||
}
|
||||
channel
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn syncframe(
|
||||
config: Config,
|
||||
acmod: u8,
|
||||
lfe: bool,
|
||||
stream_type: u32,
|
||||
words: usize,
|
||||
spectra: &[Vec<[f32; 256]>],
|
||||
short: &[bool; 6],
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
if !(64..=2048).contains(&words) && config.codec == Codec::Eac3 {
|
||||
return Err(Error::BitBudget);
|
||||
}
|
||||
let nf = super::super::NFCHANS[acmod as usize];
|
||||
let per_channel = config.bitrate_kbps / config.mode.channels() as u32;
|
||||
let cutoff = if per_channel < 48 {
|
||||
12_000
|
||||
} else if per_channel < 64 {
|
||||
16_000
|
||||
} else {
|
||||
20_000
|
||||
};
|
||||
let target_bin = (cutoff * 512 / config.sample_rate).min(253) as usize;
|
||||
let max_bw = target_bin.saturating_sub(73) / 3;
|
||||
for bandwidth in (0..=max_bw.min(60)).rev() {
|
||||
let channels = channels(spectra, nf, bandwidth, short);
|
||||
// Rate control uses exactly the reconstructed exponent envelope and the
|
||||
// decoder's standardized allocation math, not the preprocessed spectrum.
|
||||
let mut low = 0;
|
||||
let mut high = 1023;
|
||||
let mut best = None;
|
||||
while low <= high {
|
||||
let snr = (low + high) / 2;
|
||||
match serialize(config, acmod, lfe, stream_type, words, bandwidth, &channels, snr, short) {
|
||||
Some(frame) => {
|
||||
best = Some(frame);
|
||||
low = snr + 1;
|
||||
}
|
||||
None => {
|
||||
if snr == 0 {
|
||||
break;
|
||||
}
|
||||
high = snr - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(frame) = best {
|
||||
return Ok(frame);
|
||||
}
|
||||
}
|
||||
Err(Error::BitBudget)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn serialize(
|
||||
config: Config,
|
||||
acmod: u8,
|
||||
lfe: bool,
|
||||
stream_type: u32,
|
||||
words: usize,
|
||||
bandwidth: usize,
|
||||
channels: &[Channel],
|
||||
snr: u32,
|
||||
short: &[bool; 6],
|
||||
) -> Option<Vec<u8>> {
|
||||
let enhanced = config.codec == Codec::Eac3;
|
||||
let mut w = Writer::default();
|
||||
w.put(0x0b77, 16);
|
||||
if enhanced {
|
||||
eac3_header(&mut w, config, acmod, lfe, stream_type, words, channels, snr);
|
||||
} else {
|
||||
ac3_header(&mut w, config, acmod, lfe, words);
|
||||
}
|
||||
let globals = Globals { fscod: rate_code(config.sample_rate), sdcycod: 2, fdcycod: 1, sgaincod: 1, dbpbcod: 2, floorcod: 7 };
|
||||
let baps: Vec<Vec<[u8; 256]>> = (0..6)
|
||||
.map(|block| {
|
||||
channels
|
||||
.iter()
|
||||
.map(|channel| {
|
||||
let mut bap = [0; 256];
|
||||
if snr != 0 {
|
||||
let params = AllocParams {
|
||||
start: 0,
|
||||
end: channel.end,
|
||||
fgain: super::super::FASTGAIN[4],
|
||||
snroffset: (snr as i32 - 240) * 4,
|
||||
leak: None,
|
||||
dba: None,
|
||||
};
|
||||
bit_allocation(&channel.exponents[block], ¶ms, &globals, &mut bap);
|
||||
}
|
||||
bap
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
let split = (words / 2 + words / 8) * 16;
|
||||
let mut headers: Vec<Writer> = Vec::with_capacity(6);
|
||||
let mut mantissas: Vec<Writer> = Vec::with_capacity(6);
|
||||
for block in 0..6 {
|
||||
let mut header = Writer::default();
|
||||
block_header(&mut header, enhanced, acmod, lfe, stream_type, bandwidth, channels, snr, block, short[block]);
|
||||
let mut mantissa = Writer::default();
|
||||
write_mantissas(&mut mantissa, channels, &baps[block], block);
|
||||
headers.push(header);
|
||||
mantissas.push(mantissa);
|
||||
}
|
||||
let skips = if enhanced { [None; 6] } else { plan_skips(w.bits, &headers, &mantissas, words)? };
|
||||
for block in 0..6 {
|
||||
w.append(&headers[block]);
|
||||
if !enhanced {
|
||||
w.flag(skips[block].is_some());
|
||||
if let Some(bytes) = skips[block] {
|
||||
w.put(bytes as u32, 9);
|
||||
for _ in 0..bytes {
|
||||
w.put(0, 8);
|
||||
}
|
||||
}
|
||||
if block == 5 && w.bits < split {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
w.append(&mantissas[block]);
|
||||
if !enhanced && block == 1 && w.bits > split {
|
||||
return None;
|
||||
}
|
||||
if w.bits + 18 > words * 16 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
w.bytes.resize(words * 2, 0); // auxiliary padding, auxdatae and crcrsv/encinfo are zero
|
||||
let size = w.bytes.len();
|
||||
if !enhanced {
|
||||
let split_bytes = split / 8;
|
||||
let residue = crc(&w.bytes[2..split_bytes]);
|
||||
let mut prefix = residue;
|
||||
for _ in 0..(split_bytes - 2) * 8 {
|
||||
prefix = reverse_zero_bit(prefix);
|
||||
}
|
||||
w.bytes[2..4].copy_from_slice(&prefix.to_be_bytes());
|
||||
}
|
||||
let mut tail = crc(&w.bytes[2..size - 2]);
|
||||
if !enhanced && tail == 0x0b77 {
|
||||
w.bytes[size - 3] ^= 1; // reserved bit prevents a false syncword in CRC2
|
||||
tail = crc(&w.bytes[2..size - 2]);
|
||||
}
|
||||
w.bytes[size - 2..].copy_from_slice(&tail.to_be_bytes());
|
||||
Some(w.bytes)
|
||||
}
|
||||
|
||||
/// §4.5 limits the complete final region, including unused auxiliary padding.
|
||||
/// Move excess padding into bounded skip fields before block 5's mantissas.
|
||||
fn plan_skips(prefix: usize, headers: &[Writer], mantissas: &[Writer], words: usize) -> Option<[Option<usize>; 6]> {
|
||||
let total = words * 16;
|
||||
let split = (words / 2 + words / 8) * 16;
|
||||
let original = prefix + headers.iter().map(|h| h.bits + 1).sum::<usize>() + mantissas.iter().map(|m| m.bits).sum::<usize>() + 18;
|
||||
if original > total || mantissas[5].bits + 18 > total - split {
|
||||
return None;
|
||||
}
|
||||
let before_last = original - mantissas[5].bits - 18;
|
||||
let mut missing = split.saturating_sub(before_last);
|
||||
let mut skips = [None; 6];
|
||||
let mut added = 0;
|
||||
for block in (0..6).rev() {
|
||||
if missing == 0 {
|
||||
break;
|
||||
}
|
||||
let bytes = missing.saturating_sub(9).div_ceil(8).min(511);
|
||||
let bits = 9 + bytes * 8;
|
||||
skips[block] = Some(bytes);
|
||||
added += bits;
|
||||
missing = missing.saturating_sub(bits);
|
||||
}
|
||||
if missing != 0 || original + added > total {
|
||||
return None;
|
||||
}
|
||||
Some(skips)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn block_header(
|
||||
w: &mut Writer,
|
||||
enhanced: bool,
|
||||
acmod: u8,
|
||||
lfe: bool,
|
||||
stream_type: u32,
|
||||
bandwidth: usize,
|
||||
channels: &[Channel],
|
||||
snr: u32,
|
||||
block: usize,
|
||||
short: bool,
|
||||
) {
|
||||
let nf = super::super::NFCHANS[acmod as usize];
|
||||
// Block switches are common to full-bandwidth channels; LFE always uses long blocks.
|
||||
for _ in 0..nf {
|
||||
w.flag(short);
|
||||
}
|
||||
for _ in 0..nf {
|
||||
w.flag(false);
|
||||
}
|
||||
for _ in 0..if acmod == 0 { 2 } else { 1 } {
|
||||
w.flag(false);
|
||||
} // dynrng
|
||||
if enhanced {
|
||||
if block == 0 {
|
||||
w.flag(false);
|
||||
}
|
||||
}
|
||||
// spxinu, later spxstre
|
||||
else {
|
||||
w.flag(block == 0);
|
||||
if block == 0 {
|
||||
w.flag(false);
|
||||
}
|
||||
}
|
||||
if enhanced && block > 0 {
|
||||
w.flag(false);
|
||||
} // spxstre
|
||||
if acmod == 2 {
|
||||
if !enhanced || block > 0 {
|
||||
w.flag(block == 0);
|
||||
}
|
||||
if block == 0 {
|
||||
w.put(0, 4);
|
||||
}
|
||||
}
|
||||
if !enhanced {
|
||||
for channel in &channels[..nf] {
|
||||
w.put(u32::from(channel.strategies[block]), 2);
|
||||
}
|
||||
if lfe {
|
||||
w.flag(channels[nf].strategies[block] != 0);
|
||||
}
|
||||
}
|
||||
for channel in &channels[..nf] {
|
||||
if channel.strategies[block] != 0 {
|
||||
w.put(bandwidth as u32, 6);
|
||||
}
|
||||
}
|
||||
for (ch, channel) in channels.iter().enumerate() {
|
||||
if channel.strategies[block] == 0 {
|
||||
continue;
|
||||
}
|
||||
let codes = &channel.codes[block];
|
||||
w.put(u32::from(codes[0]), 4);
|
||||
for first in (1..codes.len()).step_by(3) {
|
||||
let mut previous = i32::from(codes[first - 1]);
|
||||
let mut packed = 0;
|
||||
for &exponent in &codes[first..first + 3] {
|
||||
packed = packed * 5 + (i32::from(exponent) - previous + 2) as u32;
|
||||
previous = i32::from(exponent);
|
||||
}
|
||||
w.put(packed, 7);
|
||||
}
|
||||
if ch < nf {
|
||||
w.put(0, 2);
|
||||
}
|
||||
}
|
||||
if !enhanced {
|
||||
w.flag(block == 0);
|
||||
if block == 0 {
|
||||
w.put(2, 2);
|
||||
w.put(1, 2);
|
||||
w.put(1, 2);
|
||||
w.put(2, 2);
|
||||
w.put(7, 3);
|
||||
}
|
||||
w.flag(block == 0);
|
||||
if block == 0 {
|
||||
w.put(snr / 16, 6);
|
||||
for _ in channels {
|
||||
w.put(snr % 16, 4);
|
||||
w.put(4, 3);
|
||||
}
|
||||
}
|
||||
w.flag(false); // delta allocation
|
||||
} else if stream_type == 0 {
|
||||
w.flag(false);
|
||||
} // convsnroffste
|
||||
}
|
||||
|
||||
fn ac3_header(w: &mut Writer, config: Config, acmod: u8, lfe: bool, words: usize) {
|
||||
w.put(0, 16); // CRC1, patched after allocation
|
||||
w.put(rate_code(config.sample_rate) as u32, 2);
|
||||
let index = super::super::BITRATES.iter().position(|&b| b == config.bitrate_kbps).unwrap_or(0);
|
||||
let base = (u64::from(config.bitrate_kbps) * 96_000 / u64::from(config.sample_rate)) as usize;
|
||||
w.put((index * 2 + usize::from(words > base)) as u32, 6);
|
||||
w.put(8, 5);
|
||||
w.put(0, 3);
|
||||
w.put(u32::from(acmod), 3);
|
||||
if acmod & 1 != 0 && acmod != 1 {
|
||||
w.put(1, 2);
|
||||
}
|
||||
if acmod & 4 != 0 {
|
||||
w.put(1, 2);
|
||||
}
|
||||
if acmod == 2 {
|
||||
w.put(0, 2);
|
||||
}
|
||||
w.flag(lfe);
|
||||
for _ in 0..if acmod == 0 { 2 } else { 1 } {
|
||||
w.put(31, 5);
|
||||
w.put(0, 3);
|
||||
}
|
||||
w.put(0, 1);
|
||||
w.put(1, 1);
|
||||
w.put(0, 3);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn eac3_header(w: &mut Writer, config: Config, acmod: u8, lfe: bool, stream_type: u32, words: usize, channels: &[Channel], snr: u32) {
|
||||
let nf = super::super::NFCHANS[acmod as usize];
|
||||
w.put(stream_type, 2);
|
||||
w.put(0, 3);
|
||||
w.put((words - 1) as u32, 11);
|
||||
w.put(rate_code(config.sample_rate) as u32, 2);
|
||||
w.put(3, 2);
|
||||
w.put(u32::from(acmod), 3);
|
||||
w.flag(lfe);
|
||||
w.put(16, 5);
|
||||
w.put(31, 5);
|
||||
w.flag(stream_type == 1);
|
||||
if stream_type == 1 {
|
||||
w.put(0, 8);
|
||||
}
|
||||
if acmod == 0 {
|
||||
w.put(31, 5);
|
||||
w.flag(false);
|
||||
}
|
||||
if stream_type == 1 {
|
||||
w.flag(true);
|
||||
w.put(0x1a00, 16);
|
||||
}
|
||||
w.flag(false);
|
||||
w.flag(false);
|
||||
w.flag(false); // mixmdate, infomdate, addbsie
|
||||
w.flag(true);
|
||||
w.flag(false); // per-block exponents, AHT disabled
|
||||
w.put(0, 2); // frame-level SNR
|
||||
w.flag(false);
|
||||
w.flag(true);
|
||||
w.flag(true); // transproce, blkswe, dithflage
|
||||
w.put(0, 5); // bamode, frmfgaincode, dbaflde, skipflde, spxattene
|
||||
if acmod > 1 {
|
||||
w.flag(false);
|
||||
for _ in 1..6 {
|
||||
w.flag(false);
|
||||
}
|
||||
}
|
||||
for block in 0..6 {
|
||||
for channel in &channels[..nf] {
|
||||
w.put(u32::from(channel.strategies[block]), 2);
|
||||
}
|
||||
}
|
||||
if lfe {
|
||||
for block in 0..6 {
|
||||
w.flag(channels[nf].strategies[block] != 0);
|
||||
}
|
||||
}
|
||||
if stream_type == 0 {
|
||||
for _ in 0..nf {
|
||||
w.put(0, 5);
|
||||
}
|
||||
} // converter exponents
|
||||
w.put(snr / 16, 6);
|
||||
w.put(snr % 16, 4);
|
||||
w.flag(false); // block-start information omitted
|
||||
}
|
||||
|
||||
fn write_mantissas(w: &mut Writer, channels: &[Channel], baps: &[[u8; 256]], block: usize) {
|
||||
let values: Vec<(u8, f32)> = channels
|
||||
.iter()
|
||||
.zip(baps)
|
||||
.flat_map(|(channel, bap)| {
|
||||
(0..channel.end).map(move |bin| (bap[bin], channel.coefficients[block][bin] * 2f32.powi(i32::from(channel.exponents[block][bin]))))
|
||||
})
|
||||
.collect();
|
||||
let mut grouped = vec![None; values.len()];
|
||||
for (bap, levels, group_size, bits) in [(1, 3, 3, 5), (2, 5, 3, 7), (4, 11, 2, 7)] {
|
||||
let indexes: Vec<_> = values.iter().enumerate().filter_map(|(i, &(b, _))| (b == bap).then_some(i)).collect();
|
||||
for group in indexes.chunks(group_size) {
|
||||
let mut code = 0;
|
||||
for slot in 0..group_size {
|
||||
let value = group.get(slot).map_or(0.0, |&i| values[i].1);
|
||||
code = code * levels + quantize_symmetric(value, levels);
|
||||
}
|
||||
grouped[group[0]] = Some((code, bits));
|
||||
}
|
||||
}
|
||||
for (index, (bap, value)) in values.into_iter().enumerate() {
|
||||
if let Some((code, bits)) = grouped[index] {
|
||||
w.put(code, bits);
|
||||
continue;
|
||||
}
|
||||
match bap {
|
||||
0 | 1 | 2 | 4 => {}
|
||||
3 => w.put(quantize_symmetric(value, 7), 3),
|
||||
5 => w.put(quantize_symmetric(value, 15), 4),
|
||||
_ => {
|
||||
let bits = super::super::QNTZTAB[usize::from(bap)];
|
||||
let scale = 1i32 << (bits - 1);
|
||||
let code = (value * scale as f32).round().clamp(-scale as f32, (scale - 1) as f32) as i32;
|
||||
w.put(code as u32 & ((1 << bits) - 1), bits);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn quantize_symmetric(value: f32, levels: u32) -> u32 {
|
||||
((value * levels as f32 + (levels - 1) as f32) * 0.5).round().clamp(0.0, (levels - 1) as f32) as u32
|
||||
}
|
||||
|
||||
pub(super) fn crc(bytes: &[u8]) -> u16 {
|
||||
let mut value = 0u16;
|
||||
for &byte in bytes {
|
||||
value ^= u16::from(byte) << 8;
|
||||
for _ in 0..8 {
|
||||
value = if value & 0x8000 != 0 { (value << 1) ^ 0x8005 } else { value << 1 };
|
||||
}
|
||||
}
|
||||
value
|
||||
}
|
||||
fn reverse_zero_bit(value: u16) -> u16 {
|
||||
let high = value & 1;
|
||||
((value ^ if high != 0 { 0x8005 } else { 0 }) >> 1) | (high << 15)
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn analysis_synthesis_reconstructs_overlap() {
|
||||
let input: Vec<f32> = (0..1024).map(|n| (n as f32 * 0.071).sin() * 0.4).collect();
|
||||
let mut overlap = [0.0; 256];
|
||||
for offset in (0..768).step_by(256) {
|
||||
let mut block = [0.0; 512];
|
||||
block.copy_from_slice(&input[offset..offset + 512]);
|
||||
let coeffs = transform::analysis(&block, false);
|
||||
let mut decoded = [0.0; 512];
|
||||
super::super::imdct(&coeffs, false, &mut decoded);
|
||||
if offset > 0 {
|
||||
for i in 0..256 {
|
||||
assert!((2.0 * (decoded[i] + overlap[i]) - input[offset + i]).abs() < 1e-5);
|
||||
}
|
||||
}
|
||||
overlap.copy_from_slice(&decoded[256..]);
|
||||
}
|
||||
}
|
||||
|
||||
fn config(codec: Codec, mode: CodingMode, lfe: bool) -> Config {
|
||||
Config { codec, sample_rate: 48_000, bitrate_kbps: 448, mode, lfe }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_tone_headers_crc_and_level() -> Result<(), Box<dyn std::error::Error>> {
|
||||
for codec in [Codec::Ac3, Codec::Eac3] {
|
||||
for (mode, lfe) in [(CodingMode::Mono, false), (CodingMode::Stereo, false), (CodingMode::ThreeFrontTwoSurround, true)] {
|
||||
let cfg = config(codec, mode, lfe);
|
||||
let input: Vec<Vec<f32>> = (0..cfg.channels())
|
||||
.map(|ch| (0..3072).map(|n| (n as f32 * (if lfe && ch == 3 { 0.01 } else { 0.04 + ch as f32 * 0.015 })).sin() * 0.2).collect())
|
||||
.collect();
|
||||
let mut encoder = Encoder::new(cfg)?;
|
||||
let mut packets = encoder.push(&input.iter().map(Vec::as_slice).collect::<Vec<_>>())?;
|
||||
let finish = encoder.finish()?;
|
||||
packets.extend(finish.packets);
|
||||
let mut decoder = super::super::Decoder::new();
|
||||
let mut decoded = vec![Vec::new(); cfg.channels()];
|
||||
for packet in &packets {
|
||||
let header = super::super::parse_header(&packet.data)?;
|
||||
assert_eq!(header.channels(), cfg.channels());
|
||||
assert_eq!(header.samples(), 1536);
|
||||
assert_eq!(bitstream::crc(&packet.data[2..]), 0);
|
||||
if codec == Codec::Ac3 {
|
||||
let words = packet.data.len() / 2;
|
||||
let split = (words / 2 + words / 8) * 2;
|
||||
assert_eq!(bitstream::crc(&packet.data[2..split]), 0);
|
||||
}
|
||||
let frame = decoder.decode(&packet.data)?;
|
||||
for (dst, src) in decoded.iter_mut().zip(frame.channels) {
|
||||
dst.extend(src);
|
||||
}
|
||||
}
|
||||
for ch in 0..cfg.channels() {
|
||||
if lfe && ch == 3 {
|
||||
continue;
|
||||
} // The causal LFE low-pass intentionally changes phase.
|
||||
let mse = input[ch].iter().zip(&decoded[ch][256..]).map(|(&a, &b)| (a - b).powi(2)).sum::<f32>() / input[ch].len() as f32;
|
||||
assert!(mse < 0.0001, "{codec:?} {mode:?} ch{ch}: mse={mse}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_short_transitions_reconstruct_without_gain_or_phase_steps() {
|
||||
let mut seed = 23u32;
|
||||
let input: Vec<f32> = (0..2048)
|
||||
.map(|_| {
|
||||
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
|
||||
(seed as f32 / u32::MAX as f32 - 0.5) * 0.8
|
||||
})
|
||||
.collect();
|
||||
for switches in [[false; 6], [true; 6], [false, true, false, true, true, false]] {
|
||||
let mut overlap = [0.0; 256];
|
||||
for (block, short) in switches.into_iter().enumerate() {
|
||||
let offset = block * 256;
|
||||
let mut samples = [0.0; 512];
|
||||
samples.copy_from_slice(&input[offset..offset + 512]);
|
||||
let coefficients = transform::analysis(&samples, short);
|
||||
let mut decoded = [0.0; 512];
|
||||
super::super::imdct(&coefficients, short, &mut decoded);
|
||||
if block > 0 {
|
||||
for i in 0..256 {
|
||||
assert!((2.0 * (decoded[i] + overlap[i]) - input[offset + i]).abs() < 1e-5);
|
||||
}
|
||||
}
|
||||
overlap.copy_from_slice(&decoded[256..]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lfe_lowpass_preserves_bass_and_rejects_full_bandwidth_content() {
|
||||
for rate in [32000, 44100, 48000] {
|
||||
for (frequency, minimum, maximum) in [(60.0, 0.98, 1.01), (120.0, 0.69, 0.72), (1000.0, 0.0, 0.00001)] {
|
||||
let mut filter = transform::LowPass::new(rate);
|
||||
let mut power = 0.0;
|
||||
let mut source_power = 0.0;
|
||||
for n in 0..rate {
|
||||
let input = (n as f32 * std::f32::consts::TAU * frequency / rate as f32).sin() * 0.5;
|
||||
let output = filter.process(input);
|
||||
if n > rate / 2 {
|
||||
power += f64::from(output).powi(2);
|
||||
source_power += f64::from(input).powi(2);
|
||||
}
|
||||
}
|
||||
let gain = (power / source_power).sqrt();
|
||||
assert!((minimum..=maximum).contains(&gain), "rate={rate} frequency={frequency} gain={gain}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rates_modes_and_crc_boundaries() -> Result<(), Box<dyn std::error::Error>> {
|
||||
for codec in [Codec::Ac3, Codec::Eac3] {
|
||||
for rate in [32000, 44100, 48000] {
|
||||
for mode in [
|
||||
CodingMode::DualMono,
|
||||
CodingMode::Mono,
|
||||
CodingMode::Stereo,
|
||||
CodingMode::ThreeFront,
|
||||
CodingMode::StereoSurround,
|
||||
CodingMode::ThreeFrontSurround,
|
||||
CodingMode::StereoTwoSurround,
|
||||
CodingMode::ThreeFrontTwoSurround,
|
||||
] {
|
||||
for lfe in [false, true] {
|
||||
let cfg = Config { sample_rate: rate, ..config(codec, mode, lfe) };
|
||||
let mut encoder = Encoder::new(cfg)?;
|
||||
let planes = vec![vec![0.0; 1537]; cfg.channels()];
|
||||
let mut packets = encoder.push(&planes.iter().map(Vec::as_slice).collect::<Vec<_>>())?;
|
||||
let finish = encoder.finish()?;
|
||||
packets.extend(finish.packets);
|
||||
assert_eq!(packets.len(), 2);
|
||||
assert_eq!(finish.padding_samples, 1279);
|
||||
for packet in packets {
|
||||
let h = super::super::parse_header(&packet.data)?;
|
||||
assert_eq!(h.sample_rate, rate);
|
||||
assert_eq!(h.acmod, mode as u8);
|
||||
assert_eq!(h.lfeon, lfe);
|
||||
assert_eq!(h.frame_bytes, packet.data.len());
|
||||
assert_eq!(bitstream::crc(&packet.data[2..]), 0);
|
||||
let decoded = super::super::Decoder::new().decode(&packet.data)?;
|
||||
assert!(decoded.channels.iter().flatten().all(|&v| v == 0.0));
|
||||
if codec == Codec::Ac3 {
|
||||
let words = packet.data.len() / 2;
|
||||
assert_eq!(bitstream::crc(&packet.data[2..(words / 2 + words / 8) * 2]), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_validation_and_terminal_lifecycle() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cfg = config(Codec::Ac3, CodingMode::Stereo, false);
|
||||
let mut encoder = Encoder::new(cfg)?;
|
||||
assert!(encoder.push(&[&[0.0]]).is_err());
|
||||
assert!(encoder.push(&[&[0.0], &[]]).is_err());
|
||||
for value in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, 1.01, -1.01] {
|
||||
assert!(encoder.push(&[&[value], &[0.0]]).is_err());
|
||||
}
|
||||
assert_eq!(encoder.input_samples, 0);
|
||||
assert!(encoder.push(&[&[], &[]])?.is_empty());
|
||||
let finish = encoder.finish()?;
|
||||
assert!(finish.packets.is_empty());
|
||||
assert_eq!(finish.priming_samples, 0);
|
||||
assert_eq!(finish.padding_samples, 0);
|
||||
assert_eq!(encoder.push(&[&[], &[]]).err(), Some(Error::Finished));
|
||||
assert_eq!(encoder.finish().err(), Some(Error::Finished));
|
||||
// Force an internal impossible budget to exercise terminal handling without
|
||||
// relying on a public configuration that new() should have rejected.
|
||||
let mut failing = Encoder::new(cfg)?;
|
||||
failing.config.bitrate_kbps = 1;
|
||||
assert_eq!(failing.push(&[&[0.0; 1536], &[0.0; 1536]]).err(), Some(Error::BitBudget));
|
||||
assert_eq!(failing.push(&[&[], &[]]).err(), Some(Error::Finished));
|
||||
assert_eq!(failing.finish().err(), Some(Error::Finished));
|
||||
let mut overflow = Encoder::new(cfg)?;
|
||||
overflow.input_samples = u64::MAX - 2000;
|
||||
assert!(overflow.push(&[&[0.0; 1000], &[0.0; 1000]]).is_err());
|
||||
assert_eq!(overflow.input_samples, u64::MAX - 2000);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chunk_boundaries_and_exact_trim_counts() -> Result<(), Box<dyn std::error::Error>> {
|
||||
for codec in [Codec::Ac3, Codec::Eac3] {
|
||||
for length in [1, 255, 256, 1280, 1535, 1536, 1537, 3071] {
|
||||
let cfg = config(codec, CodingMode::Stereo, false);
|
||||
let input: Vec<f32> = (0..length).map(|n| (n as f32 * 0.071).sin() * 0.2).collect();
|
||||
let mut contiguous = Encoder::new(cfg)?;
|
||||
let mut whole = contiguous.push(&[&input, &input])?;
|
||||
let finish = contiguous.finish()?;
|
||||
whole.extend(finish.packets);
|
||||
assert_eq!(whole.len() as u64 * 1536, length as u64 + u64::from(finish.priming_samples) + u64::from(finish.padding_samples));
|
||||
let mut chunked = Encoder::new(cfg)?;
|
||||
let mut pieces = Vec::new();
|
||||
for chunk in input.chunks(37) {
|
||||
pieces.extend(chunked.push(&[chunk, chunk])?);
|
||||
}
|
||||
let end = chunked.finish()?;
|
||||
pieces.extend(end.packets);
|
||||
assert_eq!(whole.iter().map(|p| &p.data).collect::<Vec<_>>(), pieces.iter().map(|p| &p.data).collect::<Vec<_>>());
|
||||
assert_eq!(finish.padding_samples, end.padding_samples);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configuration_validates_real_frame_bounds_and_explicit_layout() {
|
||||
for rate in [32000, 44100] {
|
||||
assert!(Encoder::new(Config { codec: Codec::Eac3, sample_rate: rate, bitrate_kbps: 1024, mode: CodingMode::Stereo, lfe: false }).is_err());
|
||||
}
|
||||
assert!(Encoder::new(Config { codec: Codec::Eac3, sample_rate: 48000, bitrate_kbps: 1024, mode: CodingMode::Stereo, lfe: false }).is_ok());
|
||||
assert!(Encoder::new(config(Codec::Ac3, CodingMode::Surround71, true)).is_err());
|
||||
assert!(Encoder::new(config(Codec::Eac3, CodingMode::Surround71, false)).is_err());
|
||||
assert_eq!(config(Codec::Eac3, CodingMode::Surround71, true).channel_layout(), ChannelLayout::SURROUND_7_1);
|
||||
assert!(!config(Codec::Ac3, CodingMode::DualMono, false).channel_layout().is_ordered());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dec3_describes_dependent_rear_pair_and_aggregate_rate() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Independently verified against a Dolby-hosted 48k/768k 7.1 sample's
|
||||
// six-byte dec3 descriptor and ETSI Annex F; media is not redistributed.
|
||||
let encoder = Encoder::new(Config { codec: Codec::Eac3, sample_rate: 48000, bitrate_kbps: 768, mode: CodingMode::Surround71, lfe: true })?;
|
||||
assert_eq!(encoder.codec_configuration(), [0x18, 0x00, 0x20, 0x0f, 0x02, 0x02]);
|
||||
for rate in [32000, 44100, 48000] {
|
||||
let cfg = Config { codec: Codec::Eac3, sample_rate: rate, bitrate_kbps: 640, mode: CodingMode::Surround71, lfe: true };
|
||||
let mut encoder = Encoder::new(cfg)?;
|
||||
let meta = encoder.codec_configuration();
|
||||
let declared = (u32::from(meta[0]) << 5) | (u32::from(meta[1]) >> 3);
|
||||
let pcm = vec![vec![0.0; 3072]; 8];
|
||||
for packet in encoder.push(&pcm.iter().map(Vec::as_slice).collect::<Vec<_>>())? {
|
||||
let actual = (packet.data.len() as u64 * 8 * u64::from(rate)).div_ceil(1_536_000);
|
||||
assert!(u64::from(declared) >= actual);
|
||||
let first = super::super::parse_header(&packet.data)?;
|
||||
let second = super::super::parse_header(&packet.data[first.frame_bytes..])?;
|
||||
assert_eq!((first.strmtyp, first.acmod, first.lfeon), (0, 7, true));
|
||||
assert_eq!((second.strmtyp, second.acmod, second.lfeon), (1, 6, false));
|
||||
assert_eq!(first.frame_bytes + second.frame_bytes, packet.data.len());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn actual_decoder_positions_satisfy_all_three_ac3_bit_budgets() -> Result<(), Box<dyn std::error::Error>> {
|
||||
for rate in [32000, 44100, 48000] {
|
||||
for bitrate in super::super::BITRATES {
|
||||
for mode in [CodingMode::Mono, CodingMode::ThreeFrontTwoSurround] {
|
||||
let cfg =
|
||||
Config { codec: Codec::Ac3, sample_rate: rate, bitrate_kbps: bitrate, mode, lfe: mode == CodingMode::ThreeFrontTwoSurround };
|
||||
if cfg.validate().is_err() {
|
||||
continue;
|
||||
}
|
||||
for stimulus in 0..3 {
|
||||
let mut seed = 17u32;
|
||||
let input: Vec<Vec<f32>> = (0..cfg.channels())
|
||||
.map(|_| {
|
||||
(0..1536)
|
||||
.map(|n| {
|
||||
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
|
||||
match stimulus {
|
||||
0 => 0.0,
|
||||
1 => {
|
||||
if n == 513 {
|
||||
0.8
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
_ => ((seed >> 8) as f32 / 16777216.0 - 0.5) * 0.9,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
let mut encoder = Encoder::new(cfg)?;
|
||||
for packet in encoder.push(&input.iter().map(Vec::as_slice).collect::<Vec<_>>())? {
|
||||
let mut decoder = super::super::Decoder::new();
|
||||
decoder.decode(&packet.data)?;
|
||||
assert_eq!(decoder.block_bit_positions.len(), 6);
|
||||
let words = packet.data.len() / 2;
|
||||
let split = (words / 2 + words / 8) * 16;
|
||||
assert!(decoder.block_bit_positions[1].1 <= split, "first5/8: {cfg:?} stimulus{stimulus}");
|
||||
assert!(decoder.block_bit_positions[5].0 >= split, "final3/8: {cfg:?} stimulus{stimulus}");
|
||||
assert!(decoder.block_bit_positions[5].1 + 18 <= words * 16, "wholeframe: {cfg:?} stimulus{stimulus}");
|
||||
assert_eq!(bitstream::crc(&packet.data[2..split / 8]), 0);
|
||||
assert_eq!(bitstream::crc(&packet.data[2..]), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lfe_final_nonzero_input_is_chunk_invariant() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cfg = config(Codec::Eac3, CodingMode::ThreeFrontTwoSurround, true);
|
||||
let mut input = vec![vec![0.0; 1537]; 6];
|
||||
input[3][1536] = 0.75;
|
||||
let mut one = Encoder::new(cfg)?;
|
||||
let mut a = one.push(&input.iter().map(Vec::as_slice).collect::<Vec<_>>())?;
|
||||
a.extend(one.finish()?.packets);
|
||||
let mut two = Encoder::new(cfg)?;
|
||||
let mut b = Vec::new();
|
||||
for range in [0..1, 1..255, 255..1536, 1536..1537] {
|
||||
b.extend(two.push(&input.iter().map(|p| &p[range.clone()]).collect::<Vec<_>>())?);
|
||||
}
|
||||
b.extend(two.finish()?.packets);
|
||||
assert_eq!(a.iter().map(|p| &p.data).collect::<Vec<_>>(), b.iter().map(|p| &p.data).collect::<Vec<_>>());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transient_adjacent_tone_refreshes_exponents_within_the_frame() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Independent QC localized a previous shared-six-block exponent envelope's
|
||||
// degradation to the first 32 ms after this onset. Preserve that regression.
|
||||
let rate = 32000u32;
|
||||
let input: Vec<f32> = (0..rate)
|
||||
.map(|n| {
|
||||
if n == 3200 || n == 1535 {
|
||||
0.85
|
||||
} else if (4800..11200).contains(&n) {
|
||||
(std::f32::consts::TAU * 301.0 * n as f32 / rate as f32).sin() * 0.4
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let mut encoder = Encoder::new(Config { codec: Codec::Ac3, sample_rate: rate, bitrate_kbps: 96, mode: CodingMode::Mono, lfe: false })?;
|
||||
let mut packets = encoder.push(&[&input])?;
|
||||
packets.extend(encoder.finish()?.packets);
|
||||
let mut decoder = super::super::Decoder::new();
|
||||
let mut decoded = Vec::new();
|
||||
for packet in packets {
|
||||
decoded.extend(decoder.decode(&packet.data)?.channels.remove(0));
|
||||
}
|
||||
let mut signal = 0.0f64;
|
||||
let mut error = 0.0f64;
|
||||
for n in 5440..10560 {
|
||||
signal += f64::from(input[n]).powi(2);
|
||||
error += f64::from(input[n] - decoded[n + 256]).powi(2);
|
||||
}
|
||||
let snr = 10.0 * (signal / error).log10();
|
||||
assert!(snr > 55.0, "transient-adjacent tone SNR {snr} dB");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn surround_compatibility_downmix_retains_correlated_signal_headroom() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cfg = Config { codec: Codec::Eac3, sample_rate: 48000, bitrate_kbps: 640, mode: CodingMode::Surround71, lfe: true };
|
||||
let mut pcm = vec![vec![0.0; 3072]; 8];
|
||||
for ch in [4, 5, 6, 7] {
|
||||
for n in 0..3072 {
|
||||
pcm[ch][n] = (n as f32 * std::f32::consts::TAU * 440.0 / 48000.0).sin() * 0.99;
|
||||
}
|
||||
}
|
||||
let mut encoder = Encoder::new(cfg)?;
|
||||
let mut packets = encoder.push(&pcm.iter().map(Vec::as_slice).collect::<Vec<_>>())?;
|
||||
packets.extend(encoder.finish()?.packets);
|
||||
let mut decoder = super::super::Decoder::new();
|
||||
let mut left = Vec::new();
|
||||
for packet in packets {
|
||||
let h = super::super::parse_header(&packet.data)?;
|
||||
let frame = decoder.decode(&packet.data[..h.frame_bytes])?;
|
||||
left.extend_from_slice(&frame.channels[4]);
|
||||
}
|
||||
let mse = (256..3072).map(|n| (left[n + 256] - pcm[6][n]).powi(2)).sum::<f32>() / (3072 - 256) as f32;
|
||||
assert!(mse < 0.0002, "compatibility downmix mse={mse}");
|
||||
assert!(left.iter().all(|v| v.abs() < 1.05));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encoder_errors_convert_to_audio_errors() {
|
||||
let bad = Config { codec: Codec::Ac3, sample_rate: 22_050, bitrate_kbps: 192, mode: CodingMode::Stereo, lfe: false };
|
||||
let made: crate::Result<Encoder> = Encoder::new(bad).map_err(Into::into);
|
||||
assert!(matches!(made, Err(crate::AudioError::Encode(m)) if m.contains("sample rate")));
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
//! Long-block analysis transform, ETSI TS 102 366 §7.2.3.
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// Analysis matrix includes the window and −2/N normalization (N=512).
|
||||
/// Cached rather than evaluating trigonometric functions for each audio block.
|
||||
fn matrix() -> &'static Vec<[f32; 512]> {
|
||||
static MATRIX: OnceLock<Vec<[f32; 512]>> = OnceLock::new();
|
||||
MATRIX.get_or_init(|| {
|
||||
let window = super::super::window();
|
||||
(0..256)
|
||||
.map(|k| {
|
||||
std::array::from_fn(|n| {
|
||||
let w = if n < 256 { window[n] } else { window[511 - n] };
|
||||
let phase = std::f64::consts::PI / 1024.0 * ((2 * n + 1) * (2 * k + 1)) as f64 + std::f64::consts::PI / 4.0 * (2 * k + 1) as f64;
|
||||
(-phase.cos() * f64::from(w) / 256.0) as f32
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn analysis_long(input: &[f32; 512]) -> [f32; 256] {
|
||||
std::array::from_fn(|k| matrix()[k].iter().zip(input).map(|(&a, &b)| f64::from(a) * f64::from(b)).sum::<f64>() as f32)
|
||||
}
|
||||
|
||||
/// Six-pole Butterworth low-pass at 120 Hz, before LFE analysis/downsampling.
|
||||
/// Cascaded second-order sections retain stability and streaming continuity.
|
||||
pub(super) struct LowPass {
|
||||
sections: [Biquad; 3],
|
||||
}
|
||||
struct Biquad {
|
||||
b: [f64; 3],
|
||||
a: [f64; 2],
|
||||
z: [f64; 2],
|
||||
}
|
||||
impl LowPass {
|
||||
pub(super) fn new(rate: u32) -> Self {
|
||||
let omega = std::f64::consts::TAU * 120.0 / f64::from(rate);
|
||||
Self {
|
||||
sections: [0.5176380902050415, std::f64::consts::FRAC_1_SQRT_2, 1.9318516525781366].map(|q| {
|
||||
let alpha = omega.sin() / (2.0 * q);
|
||||
let a0 = 1.0 + alpha;
|
||||
let b0 = (1.0 - omega.cos()) * 0.5 / a0;
|
||||
Biquad { b: [b0, 2.0 * b0, b0], a: [-2.0 * omega.cos() / a0, (1.0 - alpha) / a0], z: [0.0; 2] }
|
||||
}),
|
||||
}
|
||||
}
|
||||
pub(super) fn process(&mut self, input: f32) -> f32 {
|
||||
let mut value = f64::from(input);
|
||||
for section in &mut self.sections {
|
||||
let output = section.b[0] * value + section.z[0];
|
||||
section.z[0] = section.b[1] * value - section.a[0] * output + section.z[1];
|
||||
section.z[1] = section.b[2] * value - section.a[1] * output;
|
||||
value = output;
|
||||
}
|
||||
value as f32
|
||||
}
|
||||
}
|
||||
|
||||
/// Short transforms use the same 512-sample window but two 256-point
|
||||
/// cosine transforms with alpha = -1, +1, interleaved in the bitstream.
|
||||
fn short_matrix() -> &'static Vec<[f32; 256]> {
|
||||
static MATRIX: OnceLock<Vec<[f32; 256]>> = OnceLock::new();
|
||||
MATRIX.get_or_init(|| {
|
||||
let window = super::super::window();
|
||||
(0..256)
|
||||
.map(|index| {
|
||||
let half = index % 2;
|
||||
let k = index / 2;
|
||||
std::array::from_fn(|n| {
|
||||
let w = if half == 0 { window[n] } else { window[255 - n] };
|
||||
let phase = std::f64::consts::PI / 512.0 * ((2 * n + 1) * (2 * k + 1)) as f64
|
||||
+ std::f64::consts::PI / 2.0 * (2 * k + 1) as f64 * half as f64;
|
||||
(-phase.cos() * f64::from(w) / 128.0) as f32
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn analysis(input: &[f32; 512], short: bool) -> [f32; 256] {
|
||||
if !short {
|
||||
return analysis_long(input);
|
||||
}
|
||||
std::array::from_fn(|k| {
|
||||
let offset = (k % 2) * 256;
|
||||
short_matrix()[k].iter().zip(&input[offset..offset + 256]).map(|(&a, &b)| f64::from(a) * f64::from(b)).sum::<f64>() as f32
|
||||
})
|
||||
}
|
||||
|
||||
/// Detect a substantial onset within the analysis window. Decisions are shared
|
||||
/// by full-bandwidth channels so a compatible surround downmix can be formed in
|
||||
/// the transform domain without mixing coefficients from different transforms.
|
||||
pub(super) fn has_transient(input: &[f32; 512]) -> bool {
|
||||
let energy: Vec<f64> = input.as_chunks::<32>().0.iter().map(|chunk| chunk.iter().map(|&v| f64::from(v).powi(2)).sum::<f64>()).collect();
|
||||
energy.windows(2).any(|e| e[1] > 0.001 && e[1] > (e[0] + 1e-8) * 8.0)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Clean-room AC-3 (Dolby Digital) and E-AC-3 (Dolby Digital Plus) audio decoder, written from
|
||||
//! Specification-derived AC-3 (Dolby Digital) and E-AC-3 (Dolby Digital Plus) codec tools.
|
||||
//! The syncframe decoder is written from
|
||||
//! ATSC A/52:2012 ("Digital Audio Compression Standard", §5 bit stream syntax, §6-7 decoding,
|
||||
//! Annex E for E-AC-3).
|
||||
//!
|
||||
@@ -12,12 +13,18 @@
|
||||
//! - Output: planar f32 at full scale ±1, channels in WAV / SMPTE order (L R C LFE Ls Rs); no
|
||||
//! downmix.
|
||||
//!
|
||||
//! E-AC-3 (bsid 11-16, Annex E): independent substream 0 (dependent substreams and further
|
||||
//! [`Decoder`] handles E-AC-3 (bsid 11-16, Annex E): independent substream 0 (dependent substreams and further
|
||||
//! programs are skipped), 1 / 2 / 3 / 6 blocks per syncframe, the reduced sample rates, frame-based
|
||||
//! exponent strategies and the other frame-level syntax, and spectral extension (§E3.6). The
|
||||
//! adaptive hybrid transform and enhanced coupling are refused with [`Error::Unsupported`];
|
||||
//! transient pre-noise processing data is read and not applied.
|
||||
//!
|
||||
//! [`ProgrammeDecoder`] separately assembles complete six-block E-AC-3 access units, including
|
||||
//! a supported discrete 7.1 dependent-stream topology, with an explicit expected speaker layout
|
||||
//! and full-range output. It retains the syncframe decoder's unsupported-tool limits.
|
||||
//! The optional encoder follows ETSI TS 102 366 V1.4.1; see the crate README for supported
|
||||
//! encoding tools and the distinction between raw-stream padding and container presentation.
|
||||
//!
|
||||
//! From FilmCraft's `filmcraft-ac3` (storytold/filmcraft@8fda2f2). Public items keep their names, so
|
||||
//! FilmCraft can re-export this module.
|
||||
|
||||
@@ -25,6 +32,10 @@
|
||||
#![allow(clippy::needless_range_loop)]
|
||||
|
||||
mod eac3;
|
||||
mod program;
|
||||
pub use program::{ProgrammeDecoder, ProgrammeFrame};
|
||||
#[cfg(any(feature = "ac3-encode", feature = "eac3-encode"))]
|
||||
pub mod encoder;
|
||||
mod tables;
|
||||
|
||||
use std::sync::OnceLock;
|
||||
@@ -685,6 +696,11 @@ pub struct Decoder {
|
||||
delay: [[f32; 256]; 6],
|
||||
dither: u32,
|
||||
seed: u32,
|
||||
/// Apply the legacy per-syncframe dynamic-range metadata policy.
|
||||
apply_dynamic_range: bool,
|
||||
/// Actual parser positions, used to validate encoder allocation-region constraints.
|
||||
#[cfg(test)]
|
||||
block_bit_positions: Vec<(usize, usize)>,
|
||||
/// E-AC-3: the block state of the last syncframe (acmod, lfeon, sample rate code), whose
|
||||
/// exponents a syncframe of fewer than six blocks may reuse.
|
||||
carry: Option<(usize, bool, usize, Box<FrameState>)>,
|
||||
@@ -714,7 +730,15 @@ pub struct Frame {
|
||||
impl Decoder {
|
||||
/// A decoder with empty overlap state.
|
||||
pub fn new() -> Decoder {
|
||||
Decoder { delay: [[0.0; 256]; 6], dither: 0, seed: 0x1234_5678, carry: None }
|
||||
Decoder {
|
||||
delay: [[0.0; 256]; 6],
|
||||
dither: 0,
|
||||
seed: 0x1234_5678,
|
||||
apply_dynamic_range: true,
|
||||
carry: None,
|
||||
#[cfg(test)]
|
||||
block_bit_positions: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Seed the dither generator (dither is decoder-specific noise; tests compare two seeds).
|
||||
@@ -726,6 +750,8 @@ impl Decoder {
|
||||
pub fn reset(&mut self) {
|
||||
self.delay = [[0.0; 256]; 6];
|
||||
self.carry = None;
|
||||
#[cfg(test)]
|
||||
self.block_bit_positions.clear();
|
||||
}
|
||||
|
||||
fn dither(&mut self) -> f32 {
|
||||
@@ -802,6 +828,11 @@ impl Decoder {
|
||||
if !h.is_primary() {
|
||||
return Ok(Frame { header: h, channels: Vec::new() });
|
||||
}
|
||||
self.decode_eac3_substream(frame, h)
|
||||
}
|
||||
|
||||
// The programme assembler validates the dependent topology before calling this helper.
|
||||
fn decode_eac3_substream(&mut self, frame: &[u8], h: Header) -> Result<Frame> {
|
||||
// E-AC-3 has a single CRC, at the end (§E2.2.6): reseed the dither from the last words
|
||||
let n = frame.len();
|
||||
let tail = frame.get(n.saturating_sub(4)..).and_then(|t| <[u8; 4]>::try_from(t).ok()).unwrap_or_default();
|
||||
@@ -817,7 +848,10 @@ impl Decoder {
|
||||
};
|
||||
let pcm = self.blocks(&mut r, &mut f, acmod, lfeon, nblocks, Some(&mut e))?;
|
||||
self.carry = Some((acmod, lfeon, bsi.fscod, f));
|
||||
Ok(Frame { header: h, channels: wav_order(pcm, acmod, lfeon) })
|
||||
// A dependent channel map assigns positions in coded order, overriding acmod's
|
||||
// conventional speaker order. The programme assembler maps those channels once.
|
||||
let channels = if h.strmtyp == 1 { pcm } else { wav_order(pcm, acmod, lfeon) };
|
||||
Ok(Frame { header: h, channels })
|
||||
}
|
||||
|
||||
/// Decode `nblocks` audio blocks and synthesize them (inverse transform, overlap-add), in coded
|
||||
@@ -1241,6 +1275,8 @@ impl Decoder {
|
||||
}
|
||||
}
|
||||
// mantissas
|
||||
#[cfg(test)]
|
||||
let mantissa_start = r.pos;
|
||||
let mut gr = Groups::default();
|
||||
let mut cpl = [0f32; 256];
|
||||
let mut got_cpl = false;
|
||||
@@ -1266,6 +1302,8 @@ impl Decoder {
|
||||
coefs[LFE][bin] = m * 2f32.powi(-(f.exps[LFE][bin] as i32));
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
self.block_bit_positions.push((mantissa_start, r.pos));
|
||||
// decoupling
|
||||
if f.cplinu {
|
||||
for ch in 0..nf {
|
||||
@@ -1307,6 +1345,11 @@ impl Decoder {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Programme assembly requests full-range PCM rather than applying the independent
|
||||
// downmix's DRC to channels that will subsequently be replaced by a dependent stream.
|
||||
if !self.apply_dynamic_range {
|
||||
return Ok(());
|
||||
}
|
||||
// dynamic range
|
||||
for ch in 0..nf {
|
||||
let g = if acmod == 0 && ch == 1 { f.dynrng[1] } else { f.dynrng[0] };
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
//! Complete six-block E-AC-3 programme access units, including the conventional 7.1 topology.
|
||||
//!
|
||||
//! The existing syncframe decoder deliberately ignores dependent substreams. This API instead
|
||||
//! accepts one complete 1536-sample access unit: independent programme zero, optionally followed
|
||||
//! by its four-channel dependent replacement/extension. It returns full-range PCM (no dialnorm,
|
||||
//! RF compression or dynamic-range metadata gain), so an independent downmix's gain is never
|
||||
//! incorrectly applied to the assembled programme. It does not claim support for every E-AC-3
|
||||
//! dependent channel map or shorter-frame grouping.
|
||||
|
||||
use super::{Bits, Decoder, Error, Header, Result, eac3, parse_header};
|
||||
use crate::ChannelLayout;
|
||||
|
||||
/// Decoded complete programme, in WAVE speaker-mask order.
|
||||
#[derive(Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub struct ProgrammeFrame {
|
||||
/// Samples per second, shared by every substream.
|
||||
pub sample_rate: u32,
|
||||
/// Speaker assignments; dual mono remains unordered.
|
||||
pub layout: ChannelLayout,
|
||||
/// Exactly 1536 samples in each channel.
|
||||
pub channels: Vec<Vec<f32>>,
|
||||
}
|
||||
|
||||
/// Stateful decoder for complete six-block E-AC-3 programme-zero access units.
|
||||
///
|
||||
/// For 7.1 the independent 3/2+LFE downmix is followed by one four-channel dependent stream whose
|
||||
/// explicit map is Ls, Rs, Lrs, Rrs (0x1A00). The first pair replaces the independent surrounds;
|
||||
/// the second pair adds rear speakers. Output order is L, R, C, LFE, Lrs, Rrs, Ls, Rs.
|
||||
/// Unsupported topologies fail explicitly. After any error all overlap state is reset.
|
||||
pub struct ProgrammeDecoder {
|
||||
independent: Decoder,
|
||||
dependent: Decoder,
|
||||
topology: Option<(u32, u8, bool, bool)>,
|
||||
expected_layout: ChannelLayout,
|
||||
}
|
||||
|
||||
impl ProgrammeDecoder {
|
||||
/// Start with empty overlap state and full-range output.
|
||||
///
|
||||
/// The expected layout comes from the container or the caller's programme description.
|
||||
/// Requiring it prevents a missing dependent frame from silently downgrading 7.1 to 5.1.
|
||||
pub fn new(expected_layout: ChannelLayout) -> Self {
|
||||
let mut independent = Decoder::new();
|
||||
independent.apply_dynamic_range = false;
|
||||
let mut dependent = Decoder::new();
|
||||
dependent.apply_dynamic_range = false;
|
||||
Self { independent, dependent, topology: None, expected_layout }
|
||||
}
|
||||
|
||||
/// Clear the programme's overlap state before seeking or switching streams.
|
||||
pub fn reset(&mut self) {
|
||||
self.independent.reset();
|
||||
self.dependent.reset();
|
||||
self.topology = None;
|
||||
}
|
||||
|
||||
/// Decode one complete access unit, never publishing a partial programme.
|
||||
///
|
||||
/// Input is bounded to two syncframes of at most 4096 bytes each. Both CRCs, frame bounds,
|
||||
/// programme IDs, rates, block counts and the dependent channel map are checked first.
|
||||
pub fn decode(&mut self, access_unit: &[u8]) -> Result<ProgrammeFrame> {
|
||||
let result = self.decode_inner(access_unit);
|
||||
if result.is_err() {
|
||||
self.reset();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn decode_inner(&mut self, au: &[u8]) -> Result<ProgrammeFrame> {
|
||||
let (primary, dependent) = validate(au)?;
|
||||
let layout = if dependent.is_some() { ChannelLayout::SURROUND_7_1 } else { primary_layout(primary) };
|
||||
if layout != self.expected_layout {
|
||||
return Err(Error::Invalid("programme does not match the expected speaker layout"));
|
||||
}
|
||||
let topology = (primary.sample_rate, primary.acmod, primary.lfeon, dependent.is_some());
|
||||
if self.topology != Some(topology) {
|
||||
self.reset();
|
||||
}
|
||||
let mut channels = self.independent.decode_eac3_substream(&au[..primary.frame_bytes], primary)?.channels;
|
||||
if let Some(header) = dependent {
|
||||
let more = self.dependent.decode_eac3_substream(&au[primary.frame_bytes..], header)?.channels;
|
||||
channels = assemble_7_1(channels, more)?;
|
||||
}
|
||||
self.topology = Some(topology);
|
||||
Ok(ProgrammeFrame { sample_rate: primary.sample_rate, layout, channels })
|
||||
}
|
||||
}
|
||||
|
||||
fn primary_layout(h: Header) -> ChannelLayout {
|
||||
let mask = match h.acmod {
|
||||
0 => return ChannelLayout::unordered(if h.lfeon { 3 } else { 2 }),
|
||||
1 => 0x4,
|
||||
2 => 0x3,
|
||||
3 => 0x7,
|
||||
4 => 0x103,
|
||||
5 => 0x107,
|
||||
6 => 0x603,
|
||||
_ => 0x607,
|
||||
};
|
||||
ChannelLayout::from_mask(mask | if h.lfeon { 8 } else { 0 })
|
||||
}
|
||||
|
||||
fn validate(au: &[u8]) -> Result<(Header, Option<Header>)> {
|
||||
if au.len() > 8192 {
|
||||
return Err(Error::Invalid("programme access unit exceeds two syncframes"));
|
||||
}
|
||||
let first = checked_frame(au)?;
|
||||
if first.strmtyp != 0 || first.substreamid != 0 {
|
||||
return Err(Error::Unsupported("programme must start with independent substream zero".into()));
|
||||
}
|
||||
if first.frame_bytes == au.len() {
|
||||
return Ok((first, None));
|
||||
}
|
||||
let remainder = &au[first.frame_bytes..];
|
||||
let second = checked_frame(remainder)?;
|
||||
if second.frame_bytes != remainder.len() {
|
||||
return Err(Error::Unsupported("more than one dependent substream".into()));
|
||||
}
|
||||
if second.strmtyp != 1 || second.substreamid != 0 {
|
||||
return Err(Error::Invalid("expected dependent substream zero"));
|
||||
}
|
||||
if second.sample_rate != first.sample_rate || second.blocks != first.blocks {
|
||||
return Err(Error::Invalid("dependent rate or block count differs from independent"));
|
||||
}
|
||||
if first.acmod != 7 || !first.lfeon || !matches!(second.acmod, 5 | 6) || second.lfeon {
|
||||
return Err(Error::Unsupported("dependent topology is not the supported 7.1 programme".into()));
|
||||
}
|
||||
let mut bits = Bits::new(remainder);
|
||||
let bsi = eac3::read_bsi(&mut bits)?;
|
||||
if bits.overrun() {
|
||||
return Err(Error::Invalid("truncated dependent bit stream information"));
|
||||
}
|
||||
if !bsi.compre {
|
||||
return Err(Error::Invalid("last dependent substream must signal compre"));
|
||||
}
|
||||
if bsi.chanmap != Some(0x1A00) {
|
||||
return Err(Error::Unsupported("dependent channel map must be Ls Rs Lrs Rrs".into()));
|
||||
}
|
||||
Ok((first, Some(second)))
|
||||
}
|
||||
|
||||
fn checked_frame(bytes: &[u8]) -> Result<Header> {
|
||||
let h = parse_header(bytes)?;
|
||||
if !h.is_eac3() || h.bsid != 16 || h.blocks != 6 || !matches!(h.sample_rate, 32_000 | 44_100 | 48_000) {
|
||||
return Err(Error::Unsupported("programme decoder requires full-rate six-block E-AC-3 bsid 16".into()));
|
||||
}
|
||||
let frame = bytes.get(..h.frame_bytes).ok_or(Error::Invalid("truncated programme syncframe"))?;
|
||||
if crc16(&frame[2..]) != 0 {
|
||||
return Err(Error::Invalid("programme syncframe CRC mismatch"));
|
||||
}
|
||||
Ok(h)
|
||||
}
|
||||
|
||||
// ETSI TS 102 366 E.1.6: polynomial x^16+x^15+x^2+1; callers exclude only the syncword.
|
||||
fn crc16(bytes: &[u8]) -> u16 {
|
||||
let mut crc = 0u16;
|
||||
for &byte in bytes {
|
||||
crc ^= u16::from(byte) << 8;
|
||||
for _ in 0..8 {
|
||||
crc = if crc & 0x8000 != 0 { (crc << 1) ^ 0x8005 } else { crc << 1 };
|
||||
}
|
||||
}
|
||||
crc
|
||||
}
|
||||
|
||||
fn assemble_7_1(mut primary: Vec<Vec<f32>>, mut dependent: Vec<Vec<f32>>) -> Result<Vec<Vec<f32>>> {
|
||||
if primary.len() != 6 || dependent.len() != 4 || primary.iter().chain(&dependent).any(|c| c.len() != 1536) {
|
||||
return Err(Error::Invalid("decoded programme dimensions differ from signalled channels"));
|
||||
}
|
||||
// Keep L R C LFE; discard downmixed Ls/Rs. Dependent coded order is Ls Rs Lrs Rrs.
|
||||
primary.truncate(4);
|
||||
let rear = dependent.split_off(2);
|
||||
primary.extend(rear);
|
||||
primary.extend(dependent);
|
||||
Ok(primary)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Original independent-spec oracle envelopes: valid BSI/CRC, deliberately no audio blocks.
|
||||
const PRIMARY: [u8; 32] = [
|
||||
0x0b, 0x77, 0x00, 0x0f, 0x3f, 0x87, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x71, 0x87,
|
||||
];
|
||||
const DEPENDENT: [u8; 32] = [
|
||||
0x0b, 0x77, 0x40, 0x0f, 0x3c, 0x87, 0xe0, 0x11, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, 0x93,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn seven_one_rejects_valid_independent_prefix_before_audio_decode() -> Result<()> {
|
||||
assert!(validate(&PRIMARY)?.1.is_none());
|
||||
let mut decoder = ProgrammeDecoder::new(ChannelLayout::SURROUND_7_1);
|
||||
assert_eq!(decoder.decode(&PRIMARY).err(), Some(Error::Invalid("programme does not match the expected speaker layout")));
|
||||
assert!(decoder.topology.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_dependency_order_frame_bounds_and_crc() -> Result<()> {
|
||||
let mut au = PRIMARY.to_vec();
|
||||
au.extend_from_slice(&DEPENDENT);
|
||||
assert!(validate(&au)?.1.is_some());
|
||||
assert!(validate(&DEPENDENT).is_err());
|
||||
assert!(validate(&au[..63]).is_err());
|
||||
au.push(0);
|
||||
assert!(validate(&au).is_err());
|
||||
au.pop();
|
||||
au[55] ^= 1;
|
||||
assert!(validate(&au).is_err());
|
||||
let mut duplicate = PRIMARY.to_vec();
|
||||
duplicate.extend_from_slice(&PRIMARY);
|
||||
assert!(validate(&duplicate).is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn changed_dependent(bit: usize, width: usize, value: u16) -> Vec<u8> {
|
||||
let mut frame = DEPENDENT;
|
||||
for i in 0..width {
|
||||
let at = bit + i;
|
||||
let mask = 1 << (7 - at % 8);
|
||||
frame[at / 8] = (frame[at / 8] & !mask) | if value & (1 << (width - 1 - i)) != 0 { mask } else { 0 };
|
||||
}
|
||||
let crc = crc16(&frame[2..30]);
|
||||
frame[30..].copy_from_slice(&crc.to_be_bytes());
|
||||
let mut au = PRIMARY.to_vec();
|
||||
au.extend_from_slice(&frame);
|
||||
au
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_crc_valid_but_inconsistent_programme_descriptions() {
|
||||
// Fields are normative bit offsets in the independent structural BSI fixture.
|
||||
let wrong_map = changed_dependent(60, 16, 0x1800);
|
||||
assert_eq!(validate(&wrong_map).err(), Some(Error::Unsupported("dependent channel map must be Ls Rs Lrs Rrs".into())));
|
||||
let missing_last_marker = changed_dependent(50, 1, 0);
|
||||
assert_eq!(validate(&missing_last_marker).err(), Some(Error::Invalid("last dependent substream must signal compre")));
|
||||
let wrong_rate = changed_dependent(32, 2, 2);
|
||||
assert_eq!(validate(&wrong_rate).err(), Some(Error::Invalid("dependent rate or block count differs from independent")));
|
||||
let short_frame = changed_dependent(34, 2, 0);
|
||||
assert_eq!(validate(&short_frame).err(), Some(Error::Unsupported("programme decoder requires full-rate six-block E-AC-3 bsid 16".into())));
|
||||
let mut decoder = ProgrammeDecoder::new(ChannelLayout::SURROUND_7_1);
|
||||
decoder.reset();
|
||||
assert_eq!(decoder.expected_layout, ChannelLayout::SURROUND_7_1);
|
||||
}
|
||||
|
||||
#[cfg(feature = "eac3-encode")]
|
||||
#[test]
|
||||
fn explicit_map_overrides_dependent_acmod_speaker_order() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||
use super::super::encoder::{Codec, CodingMode, Config, Encoder};
|
||||
let config = Config { codec: Codec::Eac3, mode: CodingMode::Surround71, lfe: true, sample_rate: 48_000, bitrate_kbps: 640 };
|
||||
let mut encoder = Encoder::new(config)?;
|
||||
let source: Vec<Vec<f32>> = (0..8).map(|c| (0..1536).map(|n| ((n as f32) * (0.01 + c as f32 * 0.017)).sin() * 0.2).collect()).collect();
|
||||
let packets = encoder.push(&source.iter().map(Vec::as_slice).collect::<Vec<_>>())?;
|
||||
let packet = packets.first().ok_or("encoder did not emit the complete frame")?;
|
||||
let expected = ProgrammeDecoder::new(ChannelLayout::SURROUND_7_1).decode(&packet.data)?;
|
||||
let mut alternate = packet.data.clone();
|
||||
let offset = parse_header(&alternate)?.frame_bytes;
|
||||
let dependent = &mut alternate[offset..];
|
||||
// Both acmod5 and6 code four FBW channels. The explicit map overrides their default
|
||||
// positions, so changing only acmod (with no coupling/rematrix/mix metadata) is valid.
|
||||
dependent[4] = (dependent[4] & 0xf1) | (5 << 1);
|
||||
let end = dependent.len() - 2;
|
||||
let crc = crc16(&dependent[2..end]);
|
||||
dependent[end..].copy_from_slice(&crc.to_be_bytes());
|
||||
let actual = ProgrammeDecoder::new(ChannelLayout::SURROUND_7_1).decode(&alternate)?;
|
||||
assert_eq!(expected.layout, actual.layout);
|
||||
assert_eq!(expected.channels, actual.channels);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrupt_envelopes_and_truncations_never_panic_or_publish_partial_pcm() {
|
||||
let mut original = PRIMARY.to_vec();
|
||||
original.extend_from_slice(&DEPENDENT);
|
||||
for length in 0..original.len() {
|
||||
let result = std::panic::catch_unwind(|| ProgrammeDecoder::new(ChannelLayout::SURROUND_7_1).decode(&original[..length]));
|
||||
assert!(matches!(result, Ok(Err(_))), "truncated length {length}");
|
||||
}
|
||||
for bit in 0..original.len() * 8 {
|
||||
let mut corrupt = original.clone();
|
||||
corrupt[bit / 8] ^= 1 << (bit % 8);
|
||||
let result = std::panic::catch_unwind(|| ProgrammeDecoder::new(ChannelLayout::SURROUND_7_1).decode(&corrupt));
|
||||
assert!(matches!(result, Ok(Err(_))), "corrupt bit {bit}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dependent_replaces_downmix_and_labels_rears() -> Result<()> {
|
||||
let primary = (0..6).map(|c| vec![c as f32; 1536]).collect();
|
||||
let dependent = (10..14).map(|c| vec![c as f32; 1536]).collect();
|
||||
let result = assemble_7_1(primary, dependent)?;
|
||||
assert_eq!(result.iter().map(|c| c[0]).collect::<Vec<_>>(), [0., 1., 2., 3., 12., 13., 10., 11.]);
|
||||
assert_eq!(ChannelLayout::SURROUND_7_1.mask(), Some(0x63f));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_programme_never_produces_partial_channels() {
|
||||
assert!(assemble_7_1(vec![vec![0.; 1536]; 6], vec![vec![0.; 1535]; 4]).is_err());
|
||||
let mut decoder = ProgrammeDecoder::new(ChannelLayout::SURROUND_7_1);
|
||||
decoder.topology = Some((48_000, 7, true, true));
|
||||
assert!(decoder.decode(&[0; 8193]).is_err());
|
||||
assert!(decoder.topology.is_none());
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
#[cfg(any(feature = "aac-decode", feature = "aac-encode"))]
|
||||
pub mod aac;
|
||||
#[cfg(feature = "ac3-decode")]
|
||||
#[cfg(any(feature = "ac3-decode", feature = "ac3-encode", feature = "eac3-encode"))]
|
||||
pub mod ac3;
|
||||
#[cfg(any(feature = "alac-decode", feature = "alac-encode"))]
|
||||
pub mod alac;
|
||||
@@ -128,6 +128,13 @@ impl From<ac3::Error> for AudioError {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "ac3-encode", feature = "eac3-encode"))]
|
||||
impl From<ac3::encoder::Error> for AudioError {
|
||||
fn from(e: ac3::encoder::Error) -> Self {
|
||||
AudioError::Encode(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "mpa-decode")]
|
||||
impl From<mpa::Error> for AudioError {
|
||||
fn from(e: mpa::Error) -> Self {
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
//! Original synthetic PCM encoded here and decoded by an external FFmpeg executable only.
|
||||
//! Covers every public acmod/LFE combination at all three rates, plus genuine dependent-stream
|
||||
//! 7.1. These are framing/routing/level regressions, not a perceptual quality certification.
|
||||
//! Uses the shared oracle discovery rules; set CRAFT_MEDIA_REQUIRE_ORACLES=1 to require FFmpeg.
|
||||
|
||||
use std::f64::consts::TAU;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use craft_audio::ac3::encoder::{Codec, CodingMode, Config, Encoder, Packet};
|
||||
|
||||
const INPUT_SAMPLES: usize = 4097;
|
||||
const ANALYSIS_START: usize = 1024;
|
||||
const ANALYSIS_SAMPLES: usize = 2048;
|
||||
const AMPLITUDE: f64 = 0.2;
|
||||
|
||||
/// Bit-by-bit polynomial division, independent of the encoder's CRC implementation.
|
||||
fn crc16(bytes: &[u8]) -> u16 {
|
||||
bytes.iter().fold(0u16, |mut remainder, &byte| {
|
||||
for bit in (0..8).rev() {
|
||||
let feedback = ((remainder >> 15) ^ u16::from((byte >> bit) & 1)) != 0;
|
||||
remainder <<= 1;
|
||||
if feedback {
|
||||
remainder ^= 0x8005;
|
||||
}
|
||||
}
|
||||
remainder
|
||||
})
|
||||
}
|
||||
|
||||
fn lfe_index(config: Config) -> Option<usize> {
|
||||
config.lfe.then_some(match config.mode {
|
||||
CodingMode::Mono => 1,
|
||||
CodingMode::ThreeFront | CodingMode::ThreeFrontSurround | CodingMode::ThreeFrontTwoSurround | CodingMode::Surround71 => 3,
|
||||
_ => 2,
|
||||
})
|
||||
}
|
||||
|
||||
fn run_case(ffmpeg: &Path, config: Config) {
|
||||
let label = format!("{:?}/{:?}/lfe={}/{}Hz", config.codec, config.mode, config.lfe, config.sample_rate);
|
||||
let channels = config.channels();
|
||||
// Integer cycles in the analysis window make the channel probes mutually orthogonal.
|
||||
// LFE stays below 47 Hz at all rates, avoiding the encoder's intentional 120 Hz roll-off.
|
||||
let bins: Vec<usize> = (0..channels).map(|ch| if Some(ch) == lfe_index(config) { 2 } else { [29, 43, 61, 79, 101, 127, 151, 173][ch] }).collect();
|
||||
let source: Vec<Vec<f32>> = bins
|
||||
.iter()
|
||||
.map(|&bin| {
|
||||
(0..INPUT_SAMPLES)
|
||||
.map(|i| (AMPLITUDE * (i as f64 / 256.0).min(1.0) * (TAU * bin as f64 * i as f64 / ANALYSIS_SAMPLES as f64).sin()) as f32)
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
let mut encoder = Encoder::new(config).unwrap_or_else(|e| panic!("{label}: construct: {e}"));
|
||||
let mut packets = encoder.push(&source.iter().map(Vec::as_slice).collect::<Vec<_>>()).unwrap_or_else(|e| panic!("{label}: encode: {e}"));
|
||||
let finish = encoder.finish().unwrap_or_else(|e| panic!("{label}: finish: {e}"));
|
||||
assert_eq!(finish.priming_samples, 256, "{label}: priming");
|
||||
packets.extend(finish.packets);
|
||||
let decoded_samples = packets.len() * 1536;
|
||||
assert_eq!(decoded_samples, INPUT_SAMPLES + 256 + finish.padding_samples as usize, "{label}: trim accounting");
|
||||
let stream = check_packets(&packets, config, &label);
|
||||
let pcm = external_decode(ffmpeg, config, &label, &stream, decoded_samples);
|
||||
check_tones(&pcm, &bins, config, &label);
|
||||
}
|
||||
|
||||
fn check_packets(packets: &[Packet], config: Config, label: &str) -> Vec<u8> {
|
||||
let mut stream = Vec::new();
|
||||
for (index, packet) in packets.iter().enumerate() {
|
||||
assert_eq!(packet.samples, 1536, "{label}: packet {index} duration");
|
||||
let mut remaining = packet.data.as_slice();
|
||||
let mut frames = 0;
|
||||
while !remaining.is_empty() {
|
||||
let header = craft_audio::ac3::parse_header(remaining).unwrap_or_else(|e| panic!("{label}: packet {index} header: {e}"));
|
||||
assert_eq!(header.sample_rate, config.sample_rate, "{label}: sample rate");
|
||||
assert_eq!(header.samples(), 1536, "{label}: block count");
|
||||
assert_eq!(header.is_eac3(), config.codec == Codec::Eac3, "{label}: family");
|
||||
assert_eq!(header.substreamid, 0, "{label}: programme id");
|
||||
assert_eq!(header.strmtyp, u8::from(frames != 0), "{label}: independent/dependent ordering");
|
||||
assert!(header.frame_bytes <= remaining.len(), "{label}: truncated frame");
|
||||
let frame = &remaining[..header.frame_bytes];
|
||||
assert_eq!(crc16(&frame[2..]), 0, "{label}: packet {index} frame {frames} CRC");
|
||||
if config.codec == Codec::Ac3 {
|
||||
let boundary = (frame.len() / 2 / 2 + frame.len() / 2 / 8) * 2;
|
||||
assert_eq!(crc16(&frame[2..boundary]), 0, "{label}: packet {index} CRC1");
|
||||
}
|
||||
if config.mode == CodingMode::Surround71 {
|
||||
assert_eq!(header.channels(), if frames == 0 { 6 } else { 4 }, "{label}: substream channel count");
|
||||
} else {
|
||||
assert_eq!(header.acmod, config.mode as u8, "{label}: acmod");
|
||||
assert_eq!(header.lfeon, config.lfe, "{label}: LFE flag");
|
||||
}
|
||||
remaining = &remaining[header.frame_bytes..];
|
||||
frames += 1;
|
||||
}
|
||||
assert_eq!(frames, if config.mode == CodingMode::Surround71 { 2 } else { 1 }, "{label}: complete access unit");
|
||||
stream.extend_from_slice(&packet.data);
|
||||
}
|
||||
|
||||
stream
|
||||
}
|
||||
|
||||
fn external_decode(ffmpeg: &Path, config: Config, label: &str, stream: &[u8], decoded_samples: usize) -> Vec<f32> {
|
||||
let channels = config.channels();
|
||||
let format = if config.codec == Codec::Ac3 { "ac3" } else { "eac3" };
|
||||
let path = crate::testkit::fixtures_dir("ac3-encoder-oracle").join(format!(
|
||||
"{}-{:?}-lfe{}-{}-{}.{}",
|
||||
std::process::id(),
|
||||
config.mode,
|
||||
config.lfe,
|
||||
config.sample_rate,
|
||||
config.bitrate_kbps,
|
||||
format
|
||||
));
|
||||
std::fs::write(&path, stream).unwrap_or_else(|e| panic!("{label}: write fixture: {e}"));
|
||||
// Explicit format matters for three-frame files, which can be too short for auto-probing.
|
||||
// Do not request -ac or -ar: output must retain the bitstream's actual channel count/rate.
|
||||
let result = Command::new(ffmpeg)
|
||||
.args(["-nostdin", "-v", "error", "-xerror", "-err_detect", "explode", "-f", format, "-i"])
|
||||
.arg(&path)
|
||||
.args(["-map", "0:a:0", "-c:a", "pcm_f32le", "-f", "f32le", "pipe:1"])
|
||||
.output()
|
||||
.unwrap_or_else(|e| panic!("{label}: launch FFmpeg: {e}"));
|
||||
assert!(result.status.success(), "{label}: FFmpeg failed: {}", String::from_utf8_lossy(&result.stderr));
|
||||
assert!(result.stderr.is_empty(), "{label}: FFmpeg reported: {}", String::from_utf8_lossy(&result.stderr));
|
||||
assert_eq!(result.stdout.len(), decoded_samples * channels * 4, "{label}: decoded channel/sample count");
|
||||
let pcm: Vec<f32> = result.stdout.as_chunks::<4>().0.iter().map(|bytes| f32::from_le_bytes(*bytes)).collect();
|
||||
assert!(pcm.iter().all(|v| v.is_finite()), "{label}: non-finite PCM");
|
||||
std::fs::remove_file(path).unwrap_or_else(|e| panic!("{label}: remove generated fixture: {e}"));
|
||||
pcm
|
||||
}
|
||||
|
||||
fn check_tones(pcm: &[f32], bins: &[usize], config: Config, label: &str) {
|
||||
let channels = config.channels();
|
||||
for channel in 0..channels {
|
||||
for (probe_channel, &bin) in bins.iter().enumerate() {
|
||||
let (mut sine, mut cosine) = (0.0, 0.0);
|
||||
for i in ANALYSIS_START..ANALYSIS_START + ANALYSIS_SAMPLES {
|
||||
let value = f64::from(pcm[(i + 256) * channels + channel]);
|
||||
let angle = TAU * bin as f64 * i as f64 / ANALYSIS_SAMPLES as f64;
|
||||
sine += value * angle.sin();
|
||||
cosine += value * angle.cos();
|
||||
}
|
||||
// Quadrature measures LFE gain without mistaking the low-pass phase shift for loss.
|
||||
let amplitude = 2.0 * sine.hypot(cosine) / ANALYSIS_SAMPLES as f64;
|
||||
if channel == probe_channel {
|
||||
assert!((0.18..=0.22).contains(&litude), "{label}: channel {channel} lost/rerouted/scaled: amplitude {amplitude}");
|
||||
if Some(channel) != lfe_index(config) {
|
||||
assert!(sine > 0.0 && cosine.abs() < sine * 0.05, "{label}: channel {channel} polarity/priming alignment");
|
||||
}
|
||||
} else {
|
||||
assert!(amplitude < 0.01, "{label}: channel {channel} contains channel {probe_channel}: amplitude {amplitude}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ffmpeg_decodes_encoder_modes_rates_and_channels() {
|
||||
let ffmpeg = require_ffmpeg!();
|
||||
let modes = [
|
||||
CodingMode::DualMono,
|
||||
CodingMode::Mono,
|
||||
CodingMode::Stereo,
|
||||
CodingMode::ThreeFront,
|
||||
CodingMode::StereoSurround,
|
||||
CodingMode::ThreeFrontSurround,
|
||||
CodingMode::StereoTwoSurround,
|
||||
CodingMode::ThreeFrontTwoSurround,
|
||||
];
|
||||
for sample_rate in [32_000, 44_100, 48_000] {
|
||||
for codec in [Codec::Ac3, Codec::Eac3] {
|
||||
for mode in modes {
|
||||
for lfe in [false, true] {
|
||||
// Dual mono's channels are independent programmes 1 and 2, not L/R speakers.
|
||||
run_case(&ffmpeg, Config { codec, sample_rate, bitrate_kbps: 448, mode, lfe });
|
||||
}
|
||||
}
|
||||
}
|
||||
run_case(&ffmpeg, Config { codec: Codec::Eac3, sample_rate, bitrate_kbps: 640, mode: CodingMode::Surround71, lfe: true });
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use craft_media_testkit as common;
|
||||
mod testkit;
|
||||
|
||||
mod aac;
|
||||
mod ac3_encoder_oracle;
|
||||
mod ac3_oracle;
|
||||
mod alac;
|
||||
mod concealment;
|
||||
|
||||
Reference in new issue
Block a user