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 decoder (9/10) (#447)
* craft-audio: AC-3 and E-AC-3 decoder FilmCraft's AC-3 decoder as `craft_audio::ac3` behind `ac3-decode`: every AC-3 channel mode and frame size, and E-AC-3 independent substream 0 with spectral extension, in WAV order without downmix. `TrackDecoder` and the native read path take AC-3 and E-AC-3 in MP4, Matroska and raw streams. Not bit-exact: zero-bit mantissas are filled with decoder-specific dither (A/52 §7.3.4), so tests compare at 65-94 dB SNR on tones and 27-48 dB on white noise. 7.1 E-AC-3 decodes as its 5.1 core. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * craft-audio: review fixes, round 1 (#447) An AC-3 / E-AC-3 packet is as long as all the primary syncframes in it. Before the first decode, TrackDecoder::frames_per_packet gave 1536 for any AC-3 track, and conceal used the first syncframe's header only. An E-AC-3 stream of one-, two- or three-block syncframes then got the wrong MP4 length and seek positions and the wrong Matroska positions. A packet that groups several such syncframes concealed at a fraction of its length. Seeks in AC-3 and E-AC-3 now have to match the whole-file decode bit for bit, as every other codec's do. The dither is seeded from each syncframe, so the old 25 dB bound was looser than the decoder needs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * craft-audio: review fixes, round 2 (#447) - AC-3 / E-AC-3 packets that decode to different lengths (in the packets read ahead, or any decoded since) have no frames_per_packet, so a Matroska seek finds its packet by timestamp rather than by index times the first packet's length. - E-AC-3 syncframes of fewer than six blocks may reuse exponents from any syncframe before them: once one is seen, a seek decodes from the start of the stream instead of 1536 frames before the target. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * craft-audio: review fixes, round 3 (#447) - Matroska E-AC-3: the first seek reads every packet's syncframe headers (only the headers, header stripping undone) and places packets by the frames their primary syncframes hold. Packets that started to differ in length after the eight read ahead were placed as the first packet's length times their index. - A packet whose later syncframe fails no longer sets the length lost packets are concealed at, nor marks the packets' lengths as differing: it decoded only in part. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Merge main; E-AC-3 in Matroska: read packet headers only up to the seek target, and count a packet of dependent syncframes as zero frames --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
This commit is contained in:
17 files changed
+3074
-51
No files matched your search
@@ -39,7 +39,7 @@ jobs:
|
||||
# Each feature on its own, so code that needs another feature fails here.
|
||||
- name: Clippy, each feature alone
|
||||
run: |
|
||||
for f in "" decode decode-native symphonia encode lossless-encode pcm-encode flac-encode peaks aac-decode aac-encode "aac-encode,rayon" opus-decode serde; do
|
||||
for f in "" decode decode-native symphonia encode lossless-encode pcm-encode flac-encode peaks aac-decode aac-encode "aac-encode,rayon" opus-decode ac3-decode serde; do
|
||||
echo "::group::features: ${f:-none}"
|
||||
cargo clippy -p craft-audio --all-targets --locked --no-default-features ${f:+--features "$f"} -- -D warnings
|
||||
echo "::endgroup::"
|
||||
|
||||
@@ -92,9 +92,9 @@ craft-<name> = { git = "https://github.com/storytold/craft-libs", rev = "<commit
|
||||
scopes, conflicts, user overrides stored through craft-settings and import of other
|
||||
applications' shortcut sets. Command catalogs, file readers and the shortcut editor UI stay
|
||||
in each app.
|
||||
- [`craft-audio`](crates/audio/README.md): audio file I/O and codecs (WAV, AIFF, FLAC, AAC, Opus),
|
||||
streaming and seeking, and packet decoding for any demuxer. See its README for formats and
|
||||
features.
|
||||
- [`craft-audio`](crates/audio/README.md): audio file I/O and codecs (WAV, AIFF, FLAC, AAC, Opus,
|
||||
AC-3/E-AC-3), streaming and seeking, and packet decoding for any demuxer. See its README for
|
||||
formats and features.
|
||||
- [`craft-media`](crates/media/README.md): bit-level I/O (`bitstream`), clean-room MP4/MOV
|
||||
(`isobmff`) and Matroska/WebM (`matroska`) demuxers and muxers, an Ogg demuxer (`ogg`), from
|
||||
FilmCraft, and an ISO BMFF box reader. Containers only; codecs are in `craft-audio`, and
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "craft-audio"
|
||||
description = "Audio file I/O and codecs: WAV/AIFF read and write, FLAC encoder, AAC decoder and encoder, Opus decoder."
|
||||
description = "Audio file I/O and codecs: WAV/AIFF read and write, FLAC encoder, AAC decoder and encoder, Opus and AC-3/E-AC-3 decoders."
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
@@ -14,12 +14,12 @@ all-features = true
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
# Probe and decode files: WAV and AIFF natively; AAC, Opus and PCM through craft-media's
|
||||
# demuxers and this crate's decoders; MP1/MP2/MP3, FLAC, ALAC, Vorbis, CAF, ADPCM,
|
||||
# Probe and decode files: WAV and AIFF natively; AAC, Opus, AC-3/E-AC-3 and PCM through
|
||||
# craft-media's demuxers and this crate's decoders; MP1/MP2/MP3, FLAC, ALAC, Vorbis, CAF, ADPCM,
|
||||
# A-law and µ-law through symphonia.
|
||||
decode = ["decode-native", "symphonia"]
|
||||
# `decode` without symphonia: only the formats with a native decoder (others are Unsupported).
|
||||
decode-native = ["aac-decode", "opus-decode", "craft-media/isobmff", "craft-media/matroska", "craft-media/ogg", "dep:log"]
|
||||
decode-native = ["aac-decode", "opus-decode", "ac3-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 and the AAC encoder.
|
||||
@@ -34,6 +34,7 @@ flac-encode = ["craft-media/bitstream"]
|
||||
aac-decode = ["craft-media/bitstream"]
|
||||
aac-encode = ["craft-media/bitstream"]
|
||||
opus-decode = []
|
||||
ac3-decode = []
|
||||
# Multi-resolution waveform overviews (`peaks`).
|
||||
peaks = []
|
||||
# Encode AAC channels in parallel (with `aac-encode`).
|
||||
|
||||
+22
-11
@@ -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 decoder/encoder, Opus decoder.
|
||||
FLAC encoder, AAC decoder/encoder, Opus and AC-3/E-AC-3 decoders.
|
||||
Formats without a native decoder are decoded by [symphonia](#symphonia), which is to be replaced
|
||||
by native decoders.
|
||||
|
||||
@@ -24,7 +24,7 @@ No default features. The shared types (`AudioBuffer`, `AudioInfo`, `ChannelLayou
|
||||
| Feature | Provides | Pulls in |
|
||||
|---|---|---|
|
||||
| `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 two decoders below, craft-media `isobmff`/`matroska`/`ogg`, `log` |
|
||||
| `decode-native` | the same, native formats only (others are `Unsupported`) | the three 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 packet encoder | `lossless-encode`, `aac-encode` |
|
||||
| `lossless-encode` | WAV, AIFF and FLAC writing | `pcm-encode`, `flac-encode` |
|
||||
@@ -32,11 +32,12 @@ No default features. The shared types (`AudioBuffer`, `AudioInfo`, `ChannelLayou
|
||||
| `flac-encode` | `encode`, `FileEncoder` for FLAC | craft-media `bitstream` |
|
||||
| `aac-decode`, `aac-encode` | `aac::Decoder`, `aac::Encoder` | craft-media `bitstream` |
|
||||
| `opus-decode` | `opus::Decoder`/`StreamDecoder` | none |
|
||||
| `ac3-decode` | `ac3::Decoder` | none |
|
||||
| `peaks` | `peaks::Peaks` | none |
|
||||
| `rayon` | parallel AAC encoding (with `aac-encode`) | `rayon` |
|
||||
| `serde` | `Serialize`/`Deserialize` on the shared types | `serde` |
|
||||
|
||||
Any one decoder feature (`aac-decode`, `opus-decode`) also gives
|
||||
Any one decoder feature (`aac-decode`, `opus-decode`, `ac3-decode`) also gives
|
||||
`TrackDecoder`, for that codec and PCM.
|
||||
|
||||
An app that decodes 32-bit FLAC with `decode` needs a profile override in its workspace; see
|
||||
@@ -178,8 +179,8 @@ for p in &packets {
|
||||
}
|
||||
```
|
||||
|
||||
It decodes AAC (finding HE-AAC in the lookahead), Opus and PCM, and returns `AudioBuffer`s in WAV
|
||||
order when `layout()` is ordered. `codec()`, `sample_rate()` and `layout()`
|
||||
It decodes AAC (finding HE-AAC in the lookahead), Opus, AC-3/E-AC-3 and PCM, and returns
|
||||
`AudioBuffer`s in WAV order when `layout()` is ordered. `codec()`, `sample_rate()` and `layout()`
|
||||
describe the output. Container timing is the caller's.
|
||||
|
||||
Streams without codec configuration work where the packets carry it. AAC with empty `extradata`
|
||||
@@ -191,7 +192,7 @@ target, call `reset()`, decode from there, and drop the output before the target
|
||||
|
||||
The codec modules are also usable on their own. `aac::Decoder` and `opus::Decoder` return the
|
||||
codec's channel order; `aac::wav_order(channel_config)` and `opus::wav_order(channels)` map it to
|
||||
WAV order.
|
||||
WAV order. `ac3::Decoder` outputs WAV order.
|
||||
|
||||
### Encode a file
|
||||
|
||||
@@ -294,6 +295,7 @@ match craft_audio::decode(&bytes, None) {
|
||||
| HE-AAC v1/v2 | native, core only (see [Limits](#limits)) | — |
|
||||
| MP1, MP2, MP3 (MPEG audio Layer I/II/III) | symphonia | — |
|
||||
| Opus | native | — |
|
||||
| AC-3, E-AC-3 | native | — |
|
||||
| ALAC, Vorbis, ADPCM, A-law, µ-law | symphonia | — |
|
||||
|
||||
| Container | Demuxer | Timing |
|
||||
@@ -301,7 +303,7 @@ match craft_audio::decode(&bytes, None) {
|
||||
| MP4, M4A, MOV | `craft_media::isobmff` for native codecs; symphonia for the others (ALAC, MP3, FLAC) | edit lists (AAC priming, Opus pre-skip) |
|
||||
| Matroska, WebM | `craft_media::matroska` for native codecs; symphonia for the others (Vorbis, FLAC, MP3) | `CodecDelay`, `DiscardPadding` |
|
||||
| Ogg | `craft_media::ogg` (Opus); symphonia (Vorbis, FLAC) | pre-skip, end trimming |
|
||||
| Raw ADTS AAC | native frame sync | — |
|
||||
| Raw ADTS AAC, AC-3, E-AC-3 | native frame sync | — |
|
||||
| Raw MP1/MP2/MP3 | symphonia | — |
|
||||
| FLAC | symphonia | — |
|
||||
| CAF | symphonia | — |
|
||||
@@ -326,11 +328,17 @@ 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 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.
|
||||
- AC-3: not bit-exact. Zero-bit mantissas are filled with decoder-specific dither (A/52 §7.3.4),
|
||||
so white noise matches ffmpeg at only 27 dB (AC-3) and 48 dB (E-AC-3) SNR.
|
||||
- WebM Opus: `probe` can over-report by up to one block (the last block's `DiscardPadding` is
|
||||
known only when it is read); `decode` and `Decoder` are exact.
|
||||
- AAC channel configuration 7 and program-config layouts, Opus mapping families other than 0 and
|
||||
1, and AIFF with more than four channels decode with an unordered layout, in the codec's order.
|
||||
- Not decoded at all: AC-3, E-AC-3 and Speex.
|
||||
- A FLAC layout that decoders would not assume for the channel count depends on its
|
||||
`WAVEFORMATEXTENSIBLE_CHANNEL_MASK` comment. This includes the usual layouts for 5 and 6 channels:
|
||||
without the comment, back surrounds are read as side surrounds. It includes the usual 7-channel
|
||||
@@ -341,6 +349,7 @@ match craft_audio::decode(&bytes, None) {
|
||||
sample rate the frame header cannot name, or with a layout that needs the mask comment.
|
||||
- `ChannelLayout::unordered` is written to FLAC as mask `0x0000`, as RFC 9639 §8.6.2 allows, but
|
||||
ffmpeg and `flac -d` ignore that mask and use their default layout for the channel count.
|
||||
- Not decoded at all: Speex.
|
||||
- `TrackDecoder` returns `Unsupported` for MP3, FLAC, ALAC and Vorbis; `decode` and `Decoder` read
|
||||
those through symphonia.
|
||||
- MP4 edit lists: a single media edit (encoder priming and the end) is applied, alone or after one
|
||||
@@ -377,6 +386,7 @@ 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)) |
|
||||
|
||||
## symphonia
|
||||
|
||||
@@ -442,7 +452,7 @@ the full tests with ffmpeg and the vectors required.
|
||||
| Part | Source |
|
||||
|---|---|
|
||||
| WAV, AIFF, `decode`/`probe`, peaks | SoundCraft `soundcraft-audio-io` [7dc56a4](https://github.com/storytold/soundcraft/commit/7dc56a4dbeebf57eacece09536726c57844fc59d) |
|
||||
| FLAC encoder, AAC, Opus decoder, Ogg | FilmCraft [8fda2f2](https://github.com/storytold/filmcraft/commit/8fda2f232198c4c79ff9cfe5b6a347bb1375fa9e) |
|
||||
| FLAC encoder, AAC, Opus decoder, AC-3, Ogg | FilmCraft [8fda2f2](https://github.com/storytold/filmcraft/commit/8fda2f232198c4c79ff9cfe5b6a347bb1375fa9e) |
|
||||
|
||||
Where apps had overlapping implementations, the one that measured better was kept, with the
|
||||
tests of both:
|
||||
@@ -459,7 +469,8 @@ For the apps switching from their own code; to be removed once all three have sw
|
||||
|
||||
The codec modules' public items keep their names, so apps can re-export them
|
||||
(`pub use craft_audio::aac::*;`). Names kept only for that are `#[doc(hidden)]`:
|
||||
`aac::Upsample2x`, `aac::ProgramConfig::parse`, `aac::config_layout` and `aac::sample_rate_index`. What else an app adjusts:
|
||||
`ac3::Decoded` (now `ac3::Frame`), `aac::Upsample2x`, `aac::ProgramConfig::parse`,
|
||||
`aac::config_layout` and `aac::sample_rate_index`. What else an app adjusts:
|
||||
|
||||
- `AudioInfo` has `container`, `codec`, `layout` and `damaged` instead of SoundCraft's `format`.
|
||||
`detect_container` replaces `detect_format`. `probe` reports the layout instead of
|
||||
@@ -488,7 +499,7 @@ The codec modules' public items keep their names, so apps can re-export them
|
||||
short slices instead of filling them with silence.
|
||||
- `DecodeOptions` is not `Copy`.
|
||||
- The FLAC encoder's vendor string is `craft-audio <version>`.
|
||||
- `decode` tries native decoders first. AAC and Opus no longer go through symphonia, except AAC in
|
||||
- `decode` tries native decoders first. AAC, Opus and AC-3 no longer go through symphonia, except AAC in
|
||||
CAF.
|
||||
MP4 and Matroska files decode to presentation length. A packet that fails is concealed, not
|
||||
dropped. A failed source read is `AudioError::Io`.
|
||||
@@ -0,0 +1,635 @@
|
||||
//! E-AC-3 (ATSC A/52:2012 Annex E): the syncframe header and bit stream information (§E2.2.2),
|
||||
//! the audio frame (§E2.2.3) and spectral extension (§E2.2.4, §E3.6). The audio blocks share the
|
||||
//! AC-3 decoder (`Decoder::audio_block`), which reads the E-AC-3 differences from [`FrameInfo`].
|
||||
|
||||
use crate::ac3::{Bits, Error, Header, NFCHANS, Result};
|
||||
|
||||
/// Sample rates by fscod, and by fscod2 when fscod is 3 (Tables E2.2, E2.3).
|
||||
const RATES: [u32; 3] = [48_000, 44_100, 32_000];
|
||||
const REDUCED_RATES: [u32; 3] = [24_000, 22_050, 16_000];
|
||||
/// Audio blocks per syncframe by numblkscod (Table E2.4).
|
||||
const BLOCKS: [u8; 4] = [1, 2, 3, 6];
|
||||
|
||||
/// Exponent strategies of the six blocks for each frame-based strategy code (Table E2.10):
|
||||
/// 0 reuse, 1 D15, 2 D25, 3 D45.
|
||||
const FRAME_EXPSTR: [[u8; 6]; 32] = [
|
||||
[1, 0, 0, 0, 0, 0],
|
||||
[1, 0, 0, 0, 0, 3],
|
||||
[1, 0, 0, 0, 2, 0],
|
||||
[1, 0, 0, 0, 3, 3],
|
||||
[2, 0, 0, 2, 0, 0],
|
||||
[2, 0, 0, 2, 0, 3],
|
||||
[2, 0, 0, 3, 2, 0],
|
||||
[2, 0, 0, 3, 3, 3],
|
||||
[2, 0, 1, 0, 0, 0],
|
||||
[2, 0, 2, 0, 0, 3],
|
||||
[2, 0, 2, 0, 2, 0],
|
||||
[2, 0, 2, 0, 3, 3],
|
||||
[2, 0, 3, 2, 0, 0],
|
||||
[2, 0, 3, 2, 0, 3],
|
||||
[2, 0, 3, 3, 2, 0],
|
||||
[2, 0, 3, 3, 3, 3],
|
||||
[3, 1, 0, 0, 0, 0],
|
||||
[3, 1, 0, 0, 0, 3],
|
||||
[3, 2, 0, 0, 2, 0],
|
||||
[3, 2, 0, 0, 3, 3],
|
||||
[3, 2, 0, 2, 0, 0],
|
||||
[3, 2, 0, 2, 0, 3],
|
||||
[3, 2, 0, 3, 2, 0],
|
||||
[3, 2, 0, 3, 3, 3],
|
||||
[3, 3, 1, 0, 0, 0],
|
||||
[3, 3, 2, 0, 0, 3],
|
||||
[3, 3, 2, 0, 2, 0],
|
||||
[3, 3, 2, 0, 3, 3],
|
||||
[3, 3, 3, 2, 0, 0],
|
||||
[3, 3, 3, 2, 0, 3],
|
||||
[3, 3, 3, 3, 2, 0],
|
||||
[3, 3, 3, 3, 3, 3],
|
||||
];
|
||||
|
||||
/// Default coupling band structure by coupling sub-band (Table E2.12, "Default Coupling Banding
|
||||
/// Structure"; sub-band 0 never starts a combined band).
|
||||
const DEFAULT_CPLBNDSTRC: [bool; 18] = {
|
||||
let mut t = [false; 18];
|
||||
let ones = [8, 10, 11, 13, 14, 15, 16, 17];
|
||||
let mut i = 0;
|
||||
while i < ones.len() {
|
||||
t[ones[i]] = true;
|
||||
i += 1;
|
||||
}
|
||||
t
|
||||
};
|
||||
|
||||
/// Default spectral extension band structure by sub-band (Table E2.11).
|
||||
const DEFAULT_SPXBNDSTRC: [bool; 17] = {
|
||||
let mut t = [false; 17];
|
||||
let mut i = 8;
|
||||
while i < 17 {
|
||||
t[i] = true;
|
||||
i += 2;
|
||||
}
|
||||
t
|
||||
};
|
||||
|
||||
/// First transform coefficient of spectral extension sub-band `k` (Table E3.13, k ≤ 17).
|
||||
const fn spx_band_bin(k: usize) -> usize {
|
||||
25 + 12 * k
|
||||
}
|
||||
|
||||
/// Spectral extension attenuation (Table E3.14): spxattentab[code][binindex] = 2^(-(code + 1) ·
|
||||
/// (binindex + 1) / 15), which the table lists to nine decimals.
|
||||
fn spx_atten(code: usize, binindex: usize) -> f32 {
|
||||
let e = (code as f64 + 1.0) * (binindex as f64 + 1.0) / 15.0;
|
||||
(-e).exp2() as f32
|
||||
}
|
||||
|
||||
/// Parse the header of an E-AC-3 syncframe (bsid 11-16) at the start of `b` (at least 7 bytes).
|
||||
pub(crate) fn parse_header(b: &[u8], bsid: u8) -> Result<Header> {
|
||||
let mut r = Bits::new(b.get(2..).unwrap_or_default());
|
||||
let strmtyp = r.read(2) as u8;
|
||||
let substreamid = r.read(3) as u8;
|
||||
let words = r.read(11) as usize + 1;
|
||||
let fscod = r.read(2) as usize;
|
||||
let (sample_rate, blocks) = match RATES.get(fscod) {
|
||||
Some(&rate) => (rate, BLOCKS[r.read(2) as usize & 3]),
|
||||
None => (*REDUCED_RATES.get(r.read(2) as usize).ok_or(Error::Invalid("reserved fscod2"))?, 6),
|
||||
};
|
||||
let acmod = r.read(3) as u8;
|
||||
let lfeon = r.bit();
|
||||
if strmtyp == 3 {
|
||||
return Err(Error::Unsupported("reserved E-AC-3 stream type 3".into()));
|
||||
}
|
||||
let frame_bytes = words * 2;
|
||||
if frame_bytes < 8 {
|
||||
return Err(Error::Invalid("E-AC-3 syncframe too short"));
|
||||
}
|
||||
// frame bits per second of audio
|
||||
let bits = frame_bytes as u64 * 8 * u64::from(sample_rate) / (256 * u64::from(blocks));
|
||||
let bitrate_kbps = u32::try_from(bits / 1000).unwrap_or(u32::MAX);
|
||||
Ok(Header { sample_rate, bitrate_kbps, frame_bytes, bsid, acmod, lfeon, blocks, strmtyp, substreamid })
|
||||
}
|
||||
|
||||
/// What the audio frame and blocks need from the bit stream information.
|
||||
pub(crate) struct Bsi {
|
||||
pub strmtyp: u8,
|
||||
/// Sample rate code for the bit allocation's hearing threshold: fscod, or fscod2 at the
|
||||
/// reduced rates.
|
||||
pub fscod: usize,
|
||||
pub numblkscod: usize,
|
||||
}
|
||||
|
||||
/// Read syncinfo and bsi (§E2.2.1-2), leaving `r` at the audio frame.
|
||||
#[allow(clippy::cognitive_complexity, reason = "the A/52 Annex E bit stream parse, ported as-is from FilmCraft")]
|
||||
pub(crate) fn read_bsi(r: &mut Bits) -> Result<Bsi> {
|
||||
r.skip(16); // syncword
|
||||
let strmtyp = r.read(2) as u8;
|
||||
r.skip(3 + 11); // substreamid, frmsiz
|
||||
let fscod = r.read(2) as usize;
|
||||
let (rate_code, numblkscod) = if fscod == 3 {
|
||||
let fscod2 = r.read(2) as usize;
|
||||
if fscod2 == 3 {
|
||||
return Err(Error::Invalid("reserved fscod2"));
|
||||
}
|
||||
(fscod2, 3)
|
||||
} else {
|
||||
(fscod, r.read(2) as usize)
|
||||
};
|
||||
let nblocks = BLOCKS[numblkscod & 3] as usize;
|
||||
let acmod = r.read(3) as usize;
|
||||
let lfeon = r.bit();
|
||||
r.skip(5 + 5); // bsid, dialnorm
|
||||
if r.bit() {
|
||||
r.skip(8); // compr
|
||||
}
|
||||
if acmod == 0 {
|
||||
r.skip(5); // dialnorm2
|
||||
if r.bit() {
|
||||
r.skip(8); // compr2
|
||||
}
|
||||
}
|
||||
if strmtyp == 1 && r.bit() {
|
||||
r.skip(16); // chanmap
|
||||
}
|
||||
if r.bit() {
|
||||
// mixing metadata
|
||||
if acmod > 2 {
|
||||
r.skip(2); // dmixmod
|
||||
}
|
||||
if acmod & 1 != 0 && acmod > 2 {
|
||||
r.skip(6); // ltrtcmixlev, lorocmixlev
|
||||
}
|
||||
if acmod & 4 != 0 {
|
||||
r.skip(6); // ltrtsurmixlev, lorosurmixlev
|
||||
}
|
||||
if lfeon && r.bit() {
|
||||
r.skip(5); // lfemixlevcod
|
||||
}
|
||||
if strmtyp == 0 {
|
||||
if r.bit() {
|
||||
r.skip(6); // pgmscl
|
||||
}
|
||||
if acmod == 0 && r.bit() {
|
||||
r.skip(6); // pgmscl2
|
||||
}
|
||||
if r.bit() {
|
||||
r.skip(6); // extpgmscl
|
||||
}
|
||||
match r.read(2) {
|
||||
1 => r.skip(5), // premixcmpsel, drcsrc, premixcmpscl
|
||||
2 => r.skip(12),
|
||||
// mixdeflen: the mixdata field (through mixdatafill) is mixdeflen + 2 bytes
|
||||
3 => {
|
||||
let len = r.read(5) as usize;
|
||||
r.skip(8 * (len + 2));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if acmod < 2 {
|
||||
if r.bit() {
|
||||
r.skip(14); // panmean, paninfo
|
||||
}
|
||||
if acmod == 0 && r.bit() {
|
||||
r.skip(14); // panmean2, paninfo2
|
||||
}
|
||||
}
|
||||
if r.bit() {
|
||||
// frame mixing configuration information
|
||||
if numblkscod == 0 {
|
||||
r.skip(5);
|
||||
} else {
|
||||
for _ in 0..nblocks {
|
||||
if r.bit() {
|
||||
r.skip(5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if r.bit() {
|
||||
// informational metadata
|
||||
r.skip(5); // bsmod, copyrightb, origbs
|
||||
if acmod == 2 {
|
||||
r.skip(4); // dsurmod, dheadphonmod
|
||||
}
|
||||
if acmod >= 6 {
|
||||
r.skip(2); // dsurexmod
|
||||
}
|
||||
if r.bit() {
|
||||
r.skip(8); // mixlevel, roomtyp, adconvtyp
|
||||
}
|
||||
if acmod == 0 && r.bit() {
|
||||
r.skip(8); // mixlevel2, roomtyp2, adconvtyp2
|
||||
}
|
||||
if fscod < 3 {
|
||||
r.skip(1); // sourcefscod
|
||||
}
|
||||
}
|
||||
if strmtyp == 0 && numblkscod != 3 {
|
||||
r.skip(1); // convsync
|
||||
}
|
||||
if strmtyp == 2 && (numblkscod == 3 || r.bit()) {
|
||||
r.skip(6); // blkid → frmsizecod
|
||||
}
|
||||
if r.bit() {
|
||||
let l = r.read(6) as usize;
|
||||
r.skip((l + 1) * 8); // addbsi
|
||||
}
|
||||
Ok(Bsi { strmtyp, fscod: rate_code, numblkscod })
|
||||
}
|
||||
|
||||
/// The audio frame (§E2.2.3) and the state that runs from block to block within a syncframe.
|
||||
pub(crate) struct FrameInfo {
|
||||
pub strmtyp: u8,
|
||||
pub blkswe: bool,
|
||||
pub dithflage: bool,
|
||||
pub bamode: bool,
|
||||
pub frmfgaincode: bool,
|
||||
pub dbaflde: bool,
|
||||
pub skipflde: bool,
|
||||
pub snroffststr: u8,
|
||||
pub frmcsnroffst: i32,
|
||||
pub frmfsnroffst: i32,
|
||||
pub cplstre: [bool; 6],
|
||||
pub cplinu: [bool; 6],
|
||||
pub cplexpstr: [u8; 6],
|
||||
pub chexpstr: [[u8; 5]; 6],
|
||||
pub lfeexpstr: [u8; 6],
|
||||
/// Spectral extension attenuation code per channel (spxattencod), when in use.
|
||||
pub spxatten: [Option<usize>; 5],
|
||||
pub firstcplcos: [bool; 5],
|
||||
pub firstcplleak: bool,
|
||||
/// Coupling band structure by absolute coupling sub-band.
|
||||
pub cplbndstrc: [bool; 18],
|
||||
pub spx: Spx,
|
||||
}
|
||||
|
||||
impl FrameInfo {
|
||||
/// Read the audio frame. `words` is the syncframe size in 16-bit words.
|
||||
pub(crate) fn read(r: &mut Bits, bsi: &Bsi, acmod: usize, lfeon: bool, nblocks: usize, words: usize) -> Result<FrameInfo> {
|
||||
let nf = NFCHANS[acmod & 7];
|
||||
let six = bsi.numblkscod == 3;
|
||||
let (expstre, ahte) = if six { (r.bit(), r.bit()) } else { (true, false) };
|
||||
let snroffststr = r.read(2) as u8;
|
||||
if snroffststr == 3 {
|
||||
return Err(Error::Invalid("reserved SNR offset strategy"));
|
||||
}
|
||||
let transproce = r.bit();
|
||||
let mut e = FrameInfo {
|
||||
strmtyp: bsi.strmtyp,
|
||||
blkswe: r.bit(),
|
||||
dithflage: r.bit(),
|
||||
bamode: r.bit(),
|
||||
frmfgaincode: r.bit(),
|
||||
dbaflde: r.bit(),
|
||||
skipflde: r.bit(),
|
||||
snroffststr,
|
||||
frmcsnroffst: 0,
|
||||
frmfsnroffst: 0,
|
||||
cplstre: [false; 6],
|
||||
cplinu: [false; 6],
|
||||
cplexpstr: [0; 6],
|
||||
chexpstr: [[0; 5]; 6],
|
||||
lfeexpstr: [0; 6],
|
||||
spxatten: [None; 5],
|
||||
firstcplcos: [true; 5],
|
||||
firstcplleak: true,
|
||||
cplbndstrc: DEFAULT_CPLBNDSTRC,
|
||||
spx: Spx::new(),
|
||||
};
|
||||
let spxattene = r.bit();
|
||||
if acmod > 1 {
|
||||
e.cplstre[0] = true;
|
||||
e.cplinu[0] = r.bit();
|
||||
for blk in 1..nblocks {
|
||||
e.cplstre[blk] = r.bit();
|
||||
e.cplinu[blk] = if e.cplstre[blk] { r.bit() } else { e.cplinu[blk - 1] };
|
||||
}
|
||||
}
|
||||
if expstre {
|
||||
for blk in 0..nblocks {
|
||||
if e.cplinu[blk] {
|
||||
e.cplexpstr[blk] = r.read(2) as u8;
|
||||
}
|
||||
for ch in 0..nf {
|
||||
e.chexpstr[blk][ch] = r.read(2) as u8;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// frame-based strategies (six blocks only)
|
||||
if acmod > 1 && e.cplinu.iter().any(|&c| c) {
|
||||
e.cplexpstr = FRAME_EXPSTR[r.read(5) as usize & 31];
|
||||
}
|
||||
for ch in 0..nf {
|
||||
let s = FRAME_EXPSTR[r.read(5) as usize & 31];
|
||||
for (blk, &v) in s.iter().enumerate() {
|
||||
e.chexpstr[blk][ch] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
if lfeon {
|
||||
for blk in 0..nblocks {
|
||||
e.lfeexpstr[blk] = r.read(1) as u8;
|
||||
}
|
||||
}
|
||||
if bsi.strmtyp == 0 && (six || r.bit()) {
|
||||
r.skip(5 * nf); // convexpstr
|
||||
}
|
||||
if ahte {
|
||||
// AHT flags exist for channels whose exponents are sent once in the frame (§E3.4.2)
|
||||
let unsupported = || Err(Error::Unsupported("E-AC-3 adaptive hybrid transform".into()));
|
||||
let ncplblks = e.cplinu.iter().filter(|&&c| c).count();
|
||||
let ncplregs = (0..6).filter(|&b| e.cplstre[b] || e.cplexpstr[b] != 0).count();
|
||||
if ncplblks == 6 && ncplregs == 1 && r.bit() {
|
||||
return unsupported();
|
||||
}
|
||||
for ch in 0..nf {
|
||||
if (0..6).filter(|&b| e.chexpstr[b][ch] != 0).count() == 1 && r.bit() {
|
||||
return unsupported();
|
||||
}
|
||||
}
|
||||
if lfeon && e.lfeexpstr.iter().filter(|&&s| s != 0).count() == 1 && r.bit() {
|
||||
return unsupported();
|
||||
}
|
||||
}
|
||||
if snroffststr == 0 {
|
||||
e.frmcsnroffst = r.read(6) as i32;
|
||||
e.frmfsnroffst = r.read(4) as i32;
|
||||
}
|
||||
if transproce {
|
||||
// transient pre-noise processing (§E3.7) is not applied
|
||||
for _ in 0..nf {
|
||||
if r.bit() {
|
||||
r.skip(10 + 8); // transprocloc, transproclen
|
||||
}
|
||||
}
|
||||
}
|
||||
if spxattene {
|
||||
for ch in 0..nf {
|
||||
if r.bit() {
|
||||
e.spxatten[ch] = Some(r.read(5) as usize);
|
||||
}
|
||||
}
|
||||
}
|
||||
if bsi.numblkscod != 0 && r.bit() {
|
||||
// blkstrtinfo: (blocks - 1) × (4 + ceil(log2(words per frame))) bits (§E2.3.2.27)
|
||||
let log2 = (usize::BITS - words.saturating_sub(1).leading_zeros()) as usize;
|
||||
r.skip((nblocks - 1) * (4 + log2));
|
||||
}
|
||||
Ok(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// Spectral extension parameters (§E2.2.4, §E3.6), carried from block to block of a syncframe.
|
||||
pub(crate) struct Spx {
|
||||
pub inu: bool,
|
||||
pub chinspx: [bool; 5],
|
||||
pub begf: usize,
|
||||
strtf: usize,
|
||||
/// First sub-band and one past the last (spx_begin_subbnd, spx_end_subbnd).
|
||||
begin: usize,
|
||||
end: usize,
|
||||
bndstrc: [bool; 17],
|
||||
/// Band sizes in coefficients (spxbndsztab) and their count (nspxbnds).
|
||||
sizes: [usize; 17],
|
||||
nbands: usize,
|
||||
first: [bool; 5],
|
||||
co: [[f32; 17]; 5],
|
||||
nblend: [[f32; 17]; 5],
|
||||
sblend: [[f32; 17]; 5],
|
||||
}
|
||||
|
||||
impl Spx {
|
||||
fn new() -> Spx {
|
||||
Spx {
|
||||
inu: false,
|
||||
chinspx: [false; 5],
|
||||
begf: 0,
|
||||
strtf: 0,
|
||||
begin: 0,
|
||||
end: 0,
|
||||
bndstrc: DEFAULT_SPXBNDSTRC,
|
||||
sizes: [0; 17],
|
||||
nbands: 0,
|
||||
first: [true; 5],
|
||||
co: [[0.0; 17]; 5],
|
||||
nblend: [[0.0; 17]; 5],
|
||||
sblend: [[1.0; 17]; 5],
|
||||
}
|
||||
}
|
||||
|
||||
/// First coefficient of the extension region.
|
||||
pub fn begin_bin(&self) -> usize {
|
||||
spx_band_bin(self.begin)
|
||||
}
|
||||
|
||||
/// Read the strategy and coordinates of block `blk`.
|
||||
pub(crate) fn read(&mut self, r: &mut Bits, blk: usize, acmod: usize, nf: usize) -> Result<()> {
|
||||
if blk == 0 || r.bit() {
|
||||
self.inu = r.bit();
|
||||
if self.inu {
|
||||
if acmod == 1 {
|
||||
self.chinspx = [true, false, false, false, false];
|
||||
} else {
|
||||
for ch in 0..nf {
|
||||
self.chinspx[ch] = r.bit();
|
||||
}
|
||||
}
|
||||
self.strtf = r.read(2) as usize;
|
||||
self.begf = r.read(3) as usize;
|
||||
let endf = r.read(3) as usize;
|
||||
self.begin = if self.begf < 6 { self.begf + 2 } else { self.begf * 2 - 3 };
|
||||
self.end = if endf < 3 { endf + 5 } else { endf * 2 + 3 };
|
||||
if r.bit() {
|
||||
for b in self.begin + 1..self.end {
|
||||
self.bndstrc[b] = r.bit();
|
||||
}
|
||||
}
|
||||
if self.begin >= self.end {
|
||||
return Err(Error::Invalid("spectral extension ends before it begins"));
|
||||
}
|
||||
if self.strtf >= self.begin {
|
||||
return Err(Error::Invalid("spectral extension copies from above its start"));
|
||||
}
|
||||
self.nbands = 1;
|
||||
self.sizes = [0; 17];
|
||||
self.sizes[0] = 12;
|
||||
for b in self.begin + 1..self.end {
|
||||
if self.bndstrc[b] {
|
||||
self.sizes[self.nbands - 1] += 12;
|
||||
} else {
|
||||
self.sizes[self.nbands] = 12;
|
||||
self.nbands += 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.chinspx = [false; 5];
|
||||
self.first = [true; 5];
|
||||
}
|
||||
}
|
||||
if !self.inu {
|
||||
return Ok(());
|
||||
}
|
||||
for ch in 0..nf {
|
||||
if !self.chinspx[ch] {
|
||||
self.first[ch] = true;
|
||||
continue;
|
||||
}
|
||||
let coe = std::mem::replace(&mut self.first[ch], false) || r.bit();
|
||||
if !coe {
|
||||
continue;
|
||||
}
|
||||
let blend = r.read(5) as f32 / 32.0;
|
||||
let mstr = r.read(2) as i32;
|
||||
// coordinates (§E3.6.3) and blending factors (§E3.6.4.2.1)
|
||||
let mut mant = self.begin_bin() as f32;
|
||||
let end = spx_band_bin(self.end) as f32;
|
||||
for b in 0..self.nbands {
|
||||
let e = r.read(4) as i32;
|
||||
let m = r.read(2) as f32;
|
||||
let v = if e == 15 { m / 4.0 } else { (m + 4.0) / 8.0 };
|
||||
self.co[ch][b] = v * 2f32.powi(-(e + 3 * mstr));
|
||||
let size = self.sizes[b] as f32;
|
||||
let nratio = ((mant + 0.5 * size) / end - blend).clamp(0.0, 1.0);
|
||||
self.nblend[ch][b] = nratio.sqrt();
|
||||
self.sblend[ch][b] = (1.0 - nratio).sqrt();
|
||||
mant += size;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Synthesize the extension region of one channel's coefficients (§E3.6.4): translate the
|
||||
/// low band up, notch-filter the borders when attenuation is on, blend with `noise` (zero
|
||||
/// mean, unit variance) scaled to the band's energy, and scale by the coordinates.
|
||||
pub(crate) fn synthesize(&self, ch: usize, atten: Option<usize>, tc: &mut [f32; 256], mut noise: impl FnMut() -> f32) {
|
||||
let copystart = spx_band_bin(self.strtf);
|
||||
let copyend = self.begin_bin();
|
||||
let sizes = &self.sizes[..self.nbands];
|
||||
// translation
|
||||
let mut wrap = [false; 17];
|
||||
let (mut copy, mut insert) = (copystart, copyend);
|
||||
for (b, &size) in sizes.iter().enumerate() {
|
||||
if copy + size > copyend {
|
||||
copy = copystart;
|
||||
wrap[b] = true;
|
||||
}
|
||||
for _ in 0..size {
|
||||
if copy == copyend {
|
||||
copy = copystart;
|
||||
}
|
||||
if let (Some(&v), true) = (tc.get(copy), insert < tc.len()) {
|
||||
tc[insert] = v;
|
||||
}
|
||||
insert += 1;
|
||||
copy += 1;
|
||||
}
|
||||
}
|
||||
// banded RMS energy of the translated coefficients
|
||||
let mut rms = [0f32; 17];
|
||||
let mut at = copyend;
|
||||
for (b, &size) in sizes.iter().enumerate() {
|
||||
let band = tc.get(at..at + size).unwrap_or_default();
|
||||
rms[b] = (band.iter().map(|v| v * v).sum::<f32>() / size as f32).sqrt();
|
||||
at += size;
|
||||
}
|
||||
// notch filter around the baseband / extension border and each wrap point
|
||||
if let Some(code) = atten {
|
||||
let g = [spx_atten(code, 0), spx_atten(code, 1), spx_atten(code, 2)];
|
||||
let mut notch = |center: usize| {
|
||||
for (k, gain) in [g[0], g[1], g[2], g[1], g[0]].into_iter().enumerate() {
|
||||
if let Some(v) = (center + k).checked_sub(2).and_then(|i| tc.get_mut(i)) {
|
||||
*v *= gain;
|
||||
}
|
||||
}
|
||||
};
|
||||
notch(copyend);
|
||||
let mut start = copyend;
|
||||
for (b, &size) in sizes.iter().enumerate() {
|
||||
if b > 0 && wrap[b] {
|
||||
notch(start);
|
||||
}
|
||||
start += size;
|
||||
}
|
||||
}
|
||||
// noise blending and scaling
|
||||
let mut at = copyend;
|
||||
for (b, &size) in sizes.iter().enumerate() {
|
||||
let nscale = rms[b] * self.nblend[ch][b];
|
||||
let sscale = self.sblend[ch][b];
|
||||
let co = self.co[ch][b] * 32.0;
|
||||
for _ in 0..size {
|
||||
let n = noise();
|
||||
if let Some(v) = tc.get_mut(at) {
|
||||
*v = (*v * sscale + n * nscale) * co;
|
||||
}
|
||||
at += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn attenuation_matches_table_e3_14() {
|
||||
// rows 0, 4, 14 and 31 as printed
|
||||
let rows: [(usize, [f64; 3]); 4] = [
|
||||
(0, [0.954841604, 0.911722489, 0.870550563]),
|
||||
(4, [0.793700526, 0.629960525, 0.500000000]),
|
||||
(14, [0.500000000, 0.250000000, 0.125000000]),
|
||||
(31, [0.227930622, 0.051952369, 0.011841536]),
|
||||
];
|
||||
for (code, want) in rows {
|
||||
for (i, w) in want.into_iter().enumerate() {
|
||||
assert!((f64::from(spx_atten(code, i)) - w).abs() < 1e-7, "{code} {i}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_band_structures() {
|
||||
assert_eq!(DEFAULT_CPLBNDSTRC.iter().filter(|&&b| b).count(), 8);
|
||||
assert!(DEFAULT_CPLBNDSTRC[8] && !DEFAULT_CPLBNDSTRC[9] && !DEFAULT_CPLBNDSTRC[12] && DEFAULT_CPLBNDSTRC[17]);
|
||||
assert_eq!(DEFAULT_SPXBNDSTRC.iter().enumerate().filter(|(_, b)| **b).map(|(i, _)| i).collect::<Vec<_>>(), vec![8, 10, 12, 14, 16]);
|
||||
// every frame-based strategy sends new exponents in block 0
|
||||
assert!(FRAME_EXPSTR.iter().all(|s| s[0] != 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spectral_extension_translation_and_scaling() {
|
||||
// begin at sub-band 2 (bin 49), end at 5 (bin 85), copy from sub-band 0 (bin 25): three
|
||||
// 12-bin bands, the third wraps back to the copy start
|
||||
let mut s = Spx::new();
|
||||
s.inu = true;
|
||||
(s.strtf, s.begin, s.end, s.nbands) = (0, 2, 5, 3);
|
||||
s.sizes[..3].copy_from_slice(&[12, 12, 12]);
|
||||
s.co[0][..3].copy_from_slice(&[1.0 / 32.0, 0.5 / 32.0, 0.25 / 32.0]);
|
||||
s.nblend[0] = [0.0; 17];
|
||||
s.sblend[0] = [1.0; 17];
|
||||
let mut tc = [0f32; 256];
|
||||
for (i, v) in tc[25..49].iter_mut().enumerate() {
|
||||
*v = i as f32 + 1.0;
|
||||
}
|
||||
s.synthesize(0, None, &mut tc, || 0.0);
|
||||
assert_eq!(tc[49], 1.0);
|
||||
assert_eq!(tc[61], 13.0 * 0.5);
|
||||
assert_eq!(tc[73], 1.0 * 0.25);
|
||||
assert_eq!(tc[84], 12.0 * 0.25);
|
||||
assert_eq!(tc[85], 0.0);
|
||||
// pure noise: the band gets the energy of its translated coefficients
|
||||
s.nblend[0] = [1.0; 17];
|
||||
s.sblend[0] = [0.0; 17];
|
||||
s.co[0] = [1.0 / 32.0; 17];
|
||||
let mut tc2 = [0f32; 256];
|
||||
tc2[25..49].fill(2.0);
|
||||
let mut k = 0;
|
||||
s.synthesize(0, None, &mut tc2, || {
|
||||
k += 1;
|
||||
if k % 2 == 0 { 1.0 } else { -1.0 }
|
||||
});
|
||||
assert!(tc2[49..85].iter().all(|v| v.abs() == 2.0));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,71 @@
|
||||
// Generated from the tables of ATSC A/52:2012 (Tables 7.12-7.16, 7.33).
|
||||
pub(crate) const MASKTAB: [u8; 256] = [
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 28, 28, 29, 29, 29, 30, 30, 30, 31, 31,
|
||||
31, 32, 32, 32, 33, 33, 33, 34, 34, 34, 35, 35, 35, 35, 35, 35, 36, 36, 36, 36, 36, 36, 37, 37, 37, 37, 37, 37, 38, 38, 38, 38, 38, 38, 39, 39,
|
||||
39, 39, 39, 39, 40, 40, 40, 40, 40, 40, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 43, 43,
|
||||
43, 43, 43, 43, 43, 43, 43, 43, 43, 43, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45,
|
||||
45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 47, 47,
|
||||
47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48,
|
||||
48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 0, 0, 0,
|
||||
];
|
||||
|
||||
pub(crate) const LATAB: [u16; 256] = [
|
||||
64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 52, 51, 50, 49, 48, 47, 47, 46, 45, 44, 44, 43, 42, 41, 41, 40, 39, 38, 38, 37, 36, 36, 35,
|
||||
35, 34, 33, 33, 32, 32, 31, 30, 30, 29, 29, 28, 28, 27, 27, 26, 26, 25, 25, 24, 24, 23, 23, 22, 22, 21, 21, 21, 20, 20, 19, 19, 19, 18, 18, 18,
|
||||
17, 17, 17, 16, 16, 16, 15, 15, 15, 14, 14, 14, 13, 13, 13, 13, 12, 12, 12, 12, 11, 11, 11, 11, 10, 10, 10, 10, 10, 9, 9, 9, 9, 9, 8, 8, 8, 8, 8,
|
||||
8, 7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0,
|
||||
];
|
||||
|
||||
pub(crate) const BAPTAB: [u8; 64] = [
|
||||
0, 1, 1, 1, 1, 1, 2, 2, 3, 3, 3, 4, 4, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 11, 11, 11, 11, 12, 12, 12, 12, 13,
|
||||
13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15,
|
||||
];
|
||||
|
||||
pub(crate) const BNDTAB: [u8; 50] = [
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 31, 34, 37, 40, 43, 46, 49, 55, 61, 67,
|
||||
73, 79, 85, 97, 109, 121, 133, 157, 181, 205, 229,
|
||||
];
|
||||
|
||||
pub(crate) const BNDSZ: [u8; 50] = [
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 3, 3, 3, 6, 6, 6, 6, 6, 6, 12, 12, 12, 12, 24,
|
||||
24, 24, 24, 24,
|
||||
];
|
||||
|
||||
pub(crate) const HTH: [[u16; 50]; 3] = [
|
||||
[
|
||||
1232, 1232, 1088, 1024, 992, 960, 944, 944, 928, 928, 928, 928, 928, 912, 912, 912, 896, 896, 880, 880, 864, 864, 848, 848, 832, 832, 816,
|
||||
800, 784, 768, 752, 752, 752, 752, 768, 784, 832, 912, 992, 1056, 1120, 1168, 1184, 1120, 1088, 1088, 1312, 2048, 2112, 2112,
|
||||
],
|
||||
[
|
||||
1264, 1264, 1120, 1040, 992, 976, 960, 944, 944, 928, 928, 928, 928, 928, 912, 912, 912, 896, 896, 896, 880, 880, 864, 864, 848, 848, 832,
|
||||
832, 800, 784, 768, 752, 752, 752, 752, 768, 800, 848, 912, 992, 1056, 1104, 1184, 1168, 1120, 1088, 1152, 1584, 2112, 2112,
|
||||
],
|
||||
[
|
||||
1408, 1408, 1200, 1104, 1056, 1008, 992, 976, 960, 944, 944, 944, 928, 928, 928, 928, 928, 928, 928, 928, 912, 912, 912, 912, 896, 896, 896,
|
||||
880, 864, 848, 832, 816, 800, 784, 768, 752, 752, 752, 768, 784, 816, 848, 960, 1040, 1136, 1184, 1120, 1088, 1104, 1248,
|
||||
],
|
||||
];
|
||||
/// Table 7.33 as printed (5 decimals), for checking the computed window.
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::approx_constant)]
|
||||
pub(crate) const WINDOW_TABLE: [f32; 256] = [
|
||||
0.00014, 0.00024, 0.00037, 0.00051, 0.00067, 0.00086, 0.00107, 0.0013, 0.00157, 0.00187, 0.0022, 0.00256, 0.00297, 0.00341, 0.0039, 0.00443,
|
||||
0.00501, 0.00564, 0.00632, 0.00706, 0.00785, 0.00871, 0.00962, 0.01061, 0.01166, 0.01279, 0.01399, 0.01526, 0.01662, 0.01806, 0.01959, 0.02121,
|
||||
0.02292, 0.02472, 0.02662, 0.02863, 0.03073, 0.03294, 0.03527, 0.0377, 0.04025, 0.04292, 0.04571, 0.04862, 0.05165, 0.05481, 0.0581, 0.06153,
|
||||
0.06508, 0.06878, 0.07261, 0.07658, 0.08069, 0.08495, 0.08935, 0.09389, 0.09859, 0.10343, 0.10842, 0.11356, 0.11885, 0.12429, 0.12988, 0.13563,
|
||||
0.14152, 0.14757, 0.15376, 0.16011, 0.16661, 0.17325, 0.18005, 0.18699, 0.19407, 0.2013, 0.20867, 0.21618, 0.22382, 0.23161, 0.23952, 0.24757,
|
||||
0.25574, 0.26404, 0.27246, 0.281, 0.28965, 0.29841, 0.30729, 0.31626, 0.32533, 0.3345, 0.34376, 0.35311, 0.36253, 0.37204, 0.38161, 0.39126,
|
||||
0.40096, 0.41072, 0.42054, 0.4304, 0.4403, 0.45023, 0.4602, 0.47019, 0.4802, 0.49022, 0.50025, 0.51028, 0.52031, 0.53033, 0.54033, 0.55031,
|
||||
0.56026, 0.57019, 0.58007, 0.58991, 0.5997, 0.60944, 0.61912, 0.62873, 0.63827, 0.64774, 0.65713, 0.66643, 0.67564, 0.68476, 0.69377, 0.70269,
|
||||
0.7115, 0.72019, 0.72877, 0.73723, 0.74557, 0.75378, 0.76186, 0.76981, 0.77762, 0.7853, 0.79283, 0.80022, 0.80747, 0.81457, 0.82151, 0.82831,
|
||||
0.83496, 0.84145, 0.84779, 0.85398, 0.86001, 0.86588, 0.8716, 0.87716, 0.88257, 0.88782, 0.89291, 0.89785, 0.90264, 0.90728, 0.91176, 0.9161,
|
||||
0.92028, 0.92432, 0.92822, 0.93197, 0.93558, 0.93906, 0.9424, 0.9456, 0.94867, 0.95162, 0.95444, 0.95713, 0.95971, 0.96217, 0.96451, 0.96674,
|
||||
0.96887, 0.97089, 0.97281, 0.97463, 0.97635, 0.97799, 0.97953, 0.98099, 0.98236, 0.98366, 0.98488, 0.98602, 0.9871, 0.98811, 0.98905, 0.98994,
|
||||
0.99076, 0.99153, 0.99225, 0.99291, 0.99353, 0.99411, 0.99464, 0.99513, 0.99558, 0.996, 0.99639, 0.99674, 0.99706, 0.99736, 0.99763, 0.99788,
|
||||
0.99811, 0.99831, 0.9985, 0.99867, 0.99882, 0.99895, 0.99908, 0.99919, 0.99929, 0.99938, 0.99946, 0.99953, 0.99959, 0.99965, 0.99969, 0.99974,
|
||||
0.99978, 0.99981, 0.99984, 0.99986, 0.99988, 0.9999, 0.99992, 0.99993, 0.99994, 0.99995, 0.99996, 0.99997, 0.99998, 0.99998, 0.99998, 0.99999,
|
||||
0.99999, 0.99999, 0.99999, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
|
||||
];
|
||||
@@ -0,0 +1,274 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn computed_window_matches_table_7_33() {
|
||||
let w = window();
|
||||
for (n, (&a, &b)) in w.iter().zip(tables::WINDOW_TABLE.iter()).enumerate() {
|
||||
assert!((a - b).abs() <= 6e-6, "w[{n}] = {a}, table {b}");
|
||||
}
|
||||
// princen-bradley: w[n]² + w[255-n]²... (the second half mirrors): w[n]² + w[511-n]² = 1
|
||||
for n in 0..256 {
|
||||
let m = w[255 - n];
|
||||
assert!((w[n] * w[n] + m * m - 1.0).abs() < 1e-5);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tables_are_consistent() {
|
||||
// masktab maps every bin of a band to that band
|
||||
for b in 0..50 {
|
||||
for bin in tables::BNDTAB[b] as usize..(tables::BNDTAB[b] + tables::BNDSZ[b]) as usize {
|
||||
if bin < 253 {
|
||||
assert_eq!(tables::MASKTAB[bin] as usize, b, "bin {bin}");
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(tables::BNDTAB[49] as usize + tables::BNDSZ[49] as usize, 253);
|
||||
// latab is non-increasing from 64 to 0
|
||||
assert_eq!(tables::LATAB[0], 64);
|
||||
assert!(tables::LATAB.windows(2).all(|w| w[0] >= w[1]));
|
||||
assert!(tables::BAPTAB.windows(2).all(|w| w[0] <= w[1]));
|
||||
assert_eq!((tables::BAPTAB[0], tables::BAPTAB[63]), (0, 15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inverse_fft_matches_the_definition() {
|
||||
for n in [64usize, 128] {
|
||||
let re0: Vec<f32> = (0..n).map(|i| ((i * 7 + 3) % 11) as f32 - 5.0).collect();
|
||||
let im0: Vec<f32> = (0..n).map(|i| ((i * 5 + 1) % 13) as f32 - 6.0).collect();
|
||||
let (mut re, mut im) = (re0.clone(), im0.clone());
|
||||
ifft(&mut re, &mut im);
|
||||
for t in 0..n {
|
||||
let (mut sr, mut si) = (0f64, 0f64);
|
||||
for k in 0..n {
|
||||
let a = 2.0 * std::f64::consts::PI * (k * t) as f64 / n as f64;
|
||||
sr += re0[k] as f64 * a.cos() - im0[k] as f64 * a.sin();
|
||||
si += re0[k] as f64 * a.sin() + im0[k] as f64 * a.cos();
|
||||
}
|
||||
assert!((sr - re[t] as f64).abs() < 1e-3 && (si - im[t] as f64).abs() < 1e-3, "n {n} t {t}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headers_and_gains() {
|
||||
// 48 kHz, 448 kb/s, 3/2 + LFE: bsid 8, bsmod 0, acmod 7 (cmixlev, surmixlev), lfeon
|
||||
let b = [0x0B, 0x77, 0, 0, 30, 8 << 3, 0b1110_0001, 0b0100_0000];
|
||||
let h = parse_header(&b).unwrap();
|
||||
assert_eq!((h.sample_rate, h.bitrate_kbps, h.frame_bytes, h.acmod, h.lfeon, h.channels()), (48_000, 448, 1792, 7, true, 6));
|
||||
assert_eq!(parse_header(&[0x0B, 0x77, 0, 0, 0, 17 << 3, 0, 0]), Err(Error::Unsupported("bsid 17".into())));
|
||||
assert_eq!(dynrng_gain(0), 1.0);
|
||||
assert!((dynrng_gain(0b1110_0000) - 0.5).abs() < 1e-7);
|
||||
assert!((dynrng_gain(0b0111_1111) - 16.0 * 63.0 / 64.0).abs() < 1e-5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_order() {
|
||||
let ch = |n: usize| (0..n).map(|i| vec![i as f32]).collect::<Vec<_>>();
|
||||
let ids = |v: Vec<Vec<f32>>| v.iter().map(|c| c[0] as usize).collect::<Vec<_>>();
|
||||
// coded L C R Ls Rs LFE → L R C LFE Ls Rs
|
||||
assert_eq!(ids(wav_order(ch(6), 7, true)), vec![0, 2, 1, 5, 3, 4]);
|
||||
assert_eq!(ids(wav_order(ch(3), 2, true)), vec![0, 1, 2]);
|
||||
assert_eq!(ids(wav_order(ch(1), 1, false)), vec![0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn garbage_never_panics() {
|
||||
let mut d = Decoder::new();
|
||||
let mut x = 0xACu32;
|
||||
for len in [0usize, 5, 8, 100, 2000] {
|
||||
for _ in 0..200 {
|
||||
let mut f: Vec<u8> = (0..len)
|
||||
.map(|_| {
|
||||
x ^= x << 13;
|
||||
x ^= x >> 17;
|
||||
x ^= x << 5;
|
||||
x as u8
|
||||
})
|
||||
.collect();
|
||||
if len >= 6 {
|
||||
f[0] = 0x0B;
|
||||
f[1] = 0x77;
|
||||
f[4] &= 0x3F;
|
||||
f[5] = (f[5] & 7) | (8 << 3);
|
||||
}
|
||||
let _ = d.decode(&f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// MSB-first bit writer for synthetic E-AC-3 syncframes.
|
||||
#[derive(Default)]
|
||||
struct W {
|
||||
b: Vec<u8>,
|
||||
n: usize,
|
||||
}
|
||||
|
||||
impl W {
|
||||
fn put(&mut self, bits: u32, v: u32) {
|
||||
for i in (0..bits).rev() {
|
||||
if self.n.is_multiple_of(8) {
|
||||
self.b.push(0);
|
||||
}
|
||||
let bit = ((v >> i) & 1) as u8;
|
||||
*self.b.last_mut().unwrap() |= bit << (7 - self.n % 8);
|
||||
self.n += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An E-AC-3 syncframe of `words` 16-bit words: syncinfo and a bsi with no optional metadata
|
||||
/// (fscod < 3 with `numblkscod`), then `audio` writes the rest; zero padded.
|
||||
fn eac3_frame(strmtyp: u32, substreamid: u32, words: u32, numblkscod: u32, acmod: u32, audio: impl FnOnce(&mut W)) -> Vec<u8> {
|
||||
let mut w = W::default();
|
||||
w.put(16, 0x0B77);
|
||||
w.put(2, strmtyp);
|
||||
w.put(3, substreamid);
|
||||
w.put(11, words - 1);
|
||||
w.put(2, 0); // 48 kHz
|
||||
w.put(2, numblkscod);
|
||||
w.put(3, acmod);
|
||||
w.put(1, 0); // lfeon
|
||||
w.put(5, 16); // bsid
|
||||
w.put(5, 31); // dialnorm
|
||||
w.put(1, 0); // compre
|
||||
if strmtyp == 1 {
|
||||
w.put(1, 0); // chanmape
|
||||
}
|
||||
w.put(2, 0); // mixmdate, infomdate
|
||||
if strmtyp == 0 && numblkscod != 3 {
|
||||
w.put(1, 0); // convsync
|
||||
}
|
||||
w.put(1, 0); // addbsie
|
||||
audio(&mut w);
|
||||
let mut b = w.b;
|
||||
b.resize(words as usize * 2, 0);
|
||||
b
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eac3_headers() {
|
||||
let f = eac3_frame(0, 0, 384, 3, 2, |_| {});
|
||||
let h = parse_header(&f).unwrap();
|
||||
assert_eq!((h.sample_rate, h.blocks, h.samples(), h.frame_bytes, h.bitrate_kbps, h.channels()), (48_000, 6, 1536, 768, 192, 2));
|
||||
assert!(h.is_eac3() && h.is_primary());
|
||||
// one block per syncframe: 256 samples
|
||||
let h = parse_header(&eac3_frame(0, 0, 100, 0, 7, |_| {})).unwrap();
|
||||
assert_eq!((h.blocks, h.samples(), h.channels(), h.bitrate_kbps), (1, 256, 5, 300));
|
||||
// reduced sample rate (fscod 3, fscod2 1): six blocks at 22.05 kHz
|
||||
let mut f = eac3_frame(0, 0, 384, 0, 2, |_| {});
|
||||
f[4] = 0b1101_0100;
|
||||
let h = parse_header(&f).unwrap();
|
||||
assert_eq!((h.sample_rate, h.blocks, h.acmod), (22_050, 6, 2));
|
||||
f[4] = 0b1111_0100;
|
||||
assert_eq!(parse_header(&f), Err(Error::Invalid("reserved fscod2")));
|
||||
// a dependent substream and a second independent program are recognised and skipped
|
||||
let mut d = Decoder::new();
|
||||
for (strmtyp, id) in [(1, 0), (0, 1)] {
|
||||
let f = eac3_frame(strmtyp, id, 64, 3, 2, |_| {});
|
||||
let out = d.decode(&f).unwrap();
|
||||
assert!(!out.header.is_primary() && out.channels.is_empty());
|
||||
}
|
||||
assert_eq!(parse_header(&eac3_frame(3, 0, 64, 3, 2, |_| {})), Err(Error::Unsupported("reserved E-AC-3 stream type 3".into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eac3_tools_we_do_not_decode_are_refused() {
|
||||
// mono, six blocks, AHT enabled and used: chexpstr D15 then reuse, so chahtinu is sent
|
||||
let aht = eac3_frame(0, 0, 64, 3, 1, |w| {
|
||||
w.put(1, 1); // expstre
|
||||
w.put(1, 1); // ahte
|
||||
w.put(2, 0); // snroffststr
|
||||
w.put(8, 0); // transproce … spxattene
|
||||
w.put(2, 1); // chexpstr[0] = D15
|
||||
w.put(10, 0); // chexpstr[1..6] = reuse
|
||||
w.put(5, 0); // convexpstr
|
||||
w.put(1, 1); // chahtinu
|
||||
});
|
||||
assert_eq!(Decoder::new().decode(&aht).unwrap_err(), Error::Unsupported("E-AC-3 adaptive hybrid transform".into()));
|
||||
// stereo with coupling in block 0 using enhanced coupling
|
||||
let ecpl = eac3_frame(0, 0, 64, 3, 2, |w| {
|
||||
w.put(1, 1); // expstre
|
||||
w.put(1, 0); // ahte
|
||||
w.put(2, 0); // snroffststr
|
||||
w.put(8, 0);
|
||||
w.put(1, 1); // cplinu[0]
|
||||
w.put(5, 0); // cplstre[1..6]
|
||||
w.put(6, 0b01_01_01); // block 0: cplexpstr, chexpstr × 2
|
||||
w.put(30, 0); // blocks 1-5: reuse
|
||||
w.put(10, 0); // convexpstr
|
||||
w.put(10, 0); // frmcsnroffst, frmfsnroffst
|
||||
w.put(1, 0); // blkstrtinfoe
|
||||
w.put(1, 0); // dynrnge
|
||||
w.put(1, 0); // spxinu
|
||||
w.put(1, 1); // ecplinu
|
||||
});
|
||||
assert_eq!(Decoder::new().decode(&ecpl).unwrap_err(), Error::Unsupported("E-AC-3 enhanced coupling".into()));
|
||||
}
|
||||
|
||||
/// A mono E-AC-3 syncframe of one block (256 samples) that sends exponents (all 10, bandwidth
|
||||
/// code 0) when `new_exps`, else reuses the last ones. Zero SNR offsets allocate no bits: every
|
||||
/// coefficient is dither.
|
||||
pub(crate) fn single_block_eac3(new_exps: bool) -> Vec<u8> {
|
||||
eac3_frame(0, 0, 32, 0, 1, |w| {
|
||||
// (expstre and ahte are not sent with fewer than six blocks)
|
||||
w.put(2, 0); // snroffststr
|
||||
w.put(8, 0); // transproce … spxattene
|
||||
w.put(2, new_exps as u32); // chexpstr[0]: D15 or reuse
|
||||
w.put(1, 0); // convexpstre
|
||||
w.put(10, 0); // frmcsnroffst, frmfsnroffst
|
||||
w.put(1, 0); // dynrnge
|
||||
w.put(1, 0); // spxinu
|
||||
if new_exps {
|
||||
w.put(6, 0); // chbwcod → 73 coefficients
|
||||
w.put(4, 10); // absolute exponent
|
||||
for _ in 0..24 {
|
||||
w.put(7, 62); // three zero deltas
|
||||
}
|
||||
w.put(2, 0); // gainrng
|
||||
}
|
||||
w.put(1, 0); // convsnroffste
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eac3_single_block_frames_reuse_exponents_across_syncframes() {
|
||||
// the first sends exponents, the second reuses them
|
||||
let (first, second) = (single_block_eac3(true), single_block_eac3(false));
|
||||
let mut d = Decoder::new();
|
||||
let a = d.decode(&first).unwrap();
|
||||
assert_eq!((a.header.samples(), a.channels.len(), a.channels[0].len()), (256, 1, 256));
|
||||
let b = d.decode(&second).unwrap();
|
||||
let peak = |c: &[f32]| c.iter().fold(0f32, |m, v| m.max(v.abs()));
|
||||
assert!(peak(&b.channels[0]) > 1e-5 && peak(&b.channels[0]) < 0.1, "{}", peak(&b.channels[0]));
|
||||
// after a reset there are no exponents to reuse: no coefficients, silence
|
||||
d.reset();
|
||||
let c = d.decode(&second).unwrap();
|
||||
assert_eq!(peak(&c.channels[0]), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eac3_garbage_never_panics() {
|
||||
let mut d = Decoder::new();
|
||||
let mut x = 0x5EEDu32;
|
||||
for len in [8usize, 40, 300, 1500, 4096] {
|
||||
for _ in 0..300 {
|
||||
let mut f: Vec<u8> = (0..len)
|
||||
.map(|_| {
|
||||
x ^= x << 13;
|
||||
x ^= x >> 17;
|
||||
x ^= x << 5;
|
||||
x as u8
|
||||
})
|
||||
.collect();
|
||||
f[0] = 0x0B;
|
||||
f[1] = 0x77;
|
||||
// independent substream 0, frmsiz within the buffer, bsid 11-16
|
||||
let words = (len / 2).clamp(4, 2048) - 1;
|
||||
f[2] = (words >> 8) as u8 & 7;
|
||||
f[3] = words as u8;
|
||||
f[5] = (f[5] & 7) | ((11 + x % 6) as u8) << 3;
|
||||
let _ = d.decode(&f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,7 +145,7 @@ impl TryFrom<RawLayout> for ChannelLayout {
|
||||
}
|
||||
|
||||
/// A channel count as the API holds it, saturating at `u16::MAX`.
|
||||
#[cfg(any(feature = "aac-decode", feature = "opus-decode", feature = "pcm-encode"))]
|
||||
#[cfg(any(feature = "aac-decode", feature = "opus-decode", feature = "ac3-decode", feature = "pcm-encode"))]
|
||||
pub(crate) fn count(n: usize) -> u16 {
|
||||
u16::try_from(n).unwrap_or(u16::MAX)
|
||||
}
|
||||
|
||||
+18
-6
@@ -8,8 +8,8 @@
|
||||
//! `wasm32-unknown-unknown`.
|
||||
//!
|
||||
//! - [`decode`], [`probe`], [`Decoder`] (streaming, seekable), [`tracks`], [`detect_container`]
|
||||
//! (feature `decode`): WAV and AIFF are read natively; AAC, Opus and PCM in MP4/MOV,
|
||||
//! Matroska/WebM, Ogg or raw streams go through craft-media's demuxers and
|
||||
//! (feature `decode`): WAV and AIFF are read natively; AAC, Opus, AC-3/E-AC-3 and PCM in
|
||||
//! MP4/MOV, Matroska/WebM, Ogg or raw streams go through craft-media's demuxers and
|
||||
//! [`TrackDecoder`]; everything else (MP1/MP2/MP3, FLAC, ALAC, Vorbis, CAF, ADPCM, A-law,
|
||||
//! µ-law) through `symphonia`, which is to be replaced by native decoders. Feature
|
||||
//! `decode-native` is the same without symphonia.
|
||||
@@ -21,23 +21,25 @@
|
||||
//! `lossless-encode` and `aac-encode`.
|
||||
//! - [`peaks`] (feature `peaks`): min/max/RMS pyramid for waveform drawing.
|
||||
//! - Codec modules for packet-level use, each with its own features: [`aac`] (`aac-decode`,
|
||||
//! `aac-encode`), [`opus`] (`opus-decode`).
|
||||
//! `aac-encode`), [`opus`] (`opus-decode`), [`ac3`] (`ac3-decode`).
|
||||
//!
|
||||
//! The shared types below build with no features.
|
||||
|
||||
#[cfg(any(feature = "aac-decode", feature = "aac-encode"))]
|
||||
pub mod aac;
|
||||
#[cfg(feature = "ac3-decode")]
|
||||
pub mod ac3;
|
||||
mod buffer;
|
||||
mod layout;
|
||||
#[cfg(feature = "opus-decode")]
|
||||
pub mod opus;
|
||||
#[cfg(any(feature = "aac-decode", feature = "opus-decode"))]
|
||||
#[cfg(any(feature = "aac-decode", feature = "opus-decode", feature = "ac3-decode"))]
|
||||
mod pcm;
|
||||
#[cfg(feature = "peaks")]
|
||||
pub mod peaks;
|
||||
#[cfg(feature = "decode-native")]
|
||||
mod read;
|
||||
#[cfg(any(feature = "aac-decode", feature = "opus-decode"))]
|
||||
#[cfg(any(feature = "aac-decode", feature = "opus-decode", feature = "ac3-decode"))]
|
||||
mod track;
|
||||
#[cfg(any(feature = "decode-native", feature = "pcm-encode"))]
|
||||
mod wav;
|
||||
@@ -54,7 +56,7 @@ pub use layout::{ChannelLayout, Speaker};
|
||||
pub use read::SharedSource;
|
||||
#[cfg(feature = "decode-native")]
|
||||
pub use read::{DEFAULT_MAX_SAMPLES, DecodeOptions, Decoder, TrackInfo, decode, decode_with, detect_container, probe, probe_with, tracks, tracks_of};
|
||||
#[cfg(any(feature = "aac-decode", feature = "opus-decode"))]
|
||||
#[cfg(any(feature = "aac-decode", feature = "opus-decode", feature = "ac3-decode"))]
|
||||
pub use track::{LOOKAHEAD_PACKETS, TrackDecoder};
|
||||
#[cfg(feature = "pcm-encode")]
|
||||
pub use write::wav_float_header;
|
||||
@@ -111,6 +113,16 @@ impl From<opus::Error> for AudioError {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ac3-decode")]
|
||||
impl From<ac3::Error> for AudioError {
|
||||
fn from(e: ac3::Error) -> Self {
|
||||
match e {
|
||||
ac3::Error::Unsupported(_) => AudioError::Unsupported(e.to_string()),
|
||||
_ => AudioError::Malformed(e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The file formats [`encode`] writes. Each needs its feature: WAV and AIFF `pcm-encode`, FLAC
|
||||
/// `flac-encode`; asking for one that is not built returns [`AudioError::Unsupported`].
|
||||
#[non_exhaustive]
|
||||
|
||||
@@ -183,9 +183,10 @@ enum Inner<'a> {
|
||||
/// A file's audio, decoded block by block: the streaming counterpart of [`decode`], with seeking
|
||||
/// and track choice.
|
||||
///
|
||||
/// Native formats (WAV, AIFF, AAC, Opus and PCM in MP4/MOV, Matroska/WebM and Ogg, and raw ADTS
|
||||
/// streams) read their sample data from the source packet by packet. The container's packet index
|
||||
/// is held in memory (tens of bytes per packet), and opening an Ogg or Matroska file or a raw ADTS
|
||||
/// Native formats (WAV, AIFF, AAC, Opus, AC-3/E-AC-3 and PCM in MP4/MOV, Matroska/WebM and Ogg, and
|
||||
/// raw ADTS and AC-3 streams) read their sample data from the source packet by packet. The
|
||||
/// container's packet index is held in memory (tens of bytes per packet), and opening an Ogg or
|
||||
/// Matroska file or a raw
|
||||
/// stream reads through the whole file to build it; WAV and AIFF need no index. Formats symphonia
|
||||
/// decodes (MP1/MP2/MP3, FLAC, ALAC, Vorbis, CAF, ADPCM, A-law, µ-law) read the whole source into
|
||||
/// memory first; their decoded audio still streams.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! The native decode path: craft-media's demuxers (ISO BMFF / QuickTime, Matroska / WebM), raw
|
||||
//! ADTS streams, and WAV / AIFF PCM, feeding [`TrackDecoder`].
|
||||
//! ADTS and AC-3 streams, and WAV / AIFF PCM, feeding [`TrackDecoder`].
|
||||
//! [`Native::open`] answers `None` for a stream with no native codec (MP1/MP2/MP3, FLAC, ALAC,
|
||||
//! Vorbis, ...) or a container these demuxers do not read; the caller then uses symphonia.
|
||||
//!
|
||||
@@ -13,7 +13,7 @@ use super::id3v2_len;
|
||||
use super::source::{Src, io_error};
|
||||
use super::wav::{self, WavParse};
|
||||
use crate::track::{TrackDecoder, adts_format_of, adts_frame_count, adts_stream_header};
|
||||
use crate::{AudioBuffer, AudioError, AudioInfo, CodecId, CodecParams, Container, PcmFormat, Result, aac};
|
||||
use crate::{AudioBuffer, AudioError, AudioInfo, CodecId, CodecParams, Container, PcmFormat, Result, aac, ac3};
|
||||
use craft_media::{isobmff, matroska, ogg};
|
||||
|
||||
/// Corrupt packets tolerated before a stream that has produced nothing is an error.
|
||||
@@ -196,6 +196,10 @@ enum MkvFrames {
|
||||
Constant(u64),
|
||||
/// PCM: the stored sizes of the track's packets, and the bytes per frame.
|
||||
Pcm { sizes: Vec<u32>, frame_bytes: u64 },
|
||||
/// E-AC-3, whose packets can differ in length: each packet's length from the headers of its
|
||||
/// primary syncframes. The packets' places and stored sizes, the bytes header stripping took
|
||||
/// off each one, and the starts of the packets read so far.
|
||||
Eac3 { samples: Vec<(u64, u32)>, prefix: Vec<u8>, starts: Vec<u64> },
|
||||
/// Opus: each packet's length is in its first bytes (RFC 6716 §3.1); the packets' places and
|
||||
/// sizes, and their starts once read.
|
||||
Opus { samples: Vec<(u64, u32)>, starts: Option<Vec<u64>> },
|
||||
@@ -209,6 +213,25 @@ impl MkvFrames {
|
||||
match self {
|
||||
MkvFrames::Constant(n) => Some(k as u64 * *n),
|
||||
MkvFrames::Pcm { sizes, frame_bytes } => Some(sizes.iter().take(k).map(|&s| u64::from(s)).sum::<u64>() / *frame_bytes),
|
||||
MkvFrames::Eac3 { samples, prefix, starts } => {
|
||||
// Headers are read only as far as packet `k`; a later seek extends the list. A
|
||||
// packet that cannot be read leaves the rest unknown (the caller then places the
|
||||
// packet by its timestamp) and is tried again on the next seek.
|
||||
if k >= samples.len() {
|
||||
return None;
|
||||
}
|
||||
while starts.len() <= k {
|
||||
let start = match starts.len().checked_sub(1) {
|
||||
None => 0,
|
||||
Some(prev) => {
|
||||
let &(at, size) = samples.get(prev)?;
|
||||
starts.get(prev)? + eac3_packet_frames(src, prefix, at, size)? as u64
|
||||
}
|
||||
};
|
||||
starts.push(start);
|
||||
}
|
||||
starts.get(k).copied()
|
||||
}
|
||||
MkvFrames::Opus { samples, starts } => {
|
||||
if starts.is_none() {
|
||||
// A read that fails leaves the starts unknown (the caller then places the
|
||||
@@ -230,6 +253,31 @@ impl MkvFrames {
|
||||
}
|
||||
}
|
||||
|
||||
/// The frames a stored Matroska E-AC-3 packet decodes to, from the headers of its primary
|
||||
/// syncframes, reading only those headers; `None` when a read fails or a header does not parse.
|
||||
/// A packet of dependent syncframes alone is zero frames, not a failure.
|
||||
fn eac3_packet_frames(src: &Src<'_>, prefix: &[u8], at: u64, size: u32) -> Option<usize> {
|
||||
// Enough bytes for any AC-3 / E-AC-3 header ahead of the audio blocks.
|
||||
const HEADER_BYTES: usize = 32;
|
||||
let len = prefix.len() + size as usize;
|
||||
let mut frames = 0;
|
||||
let mut p = 0;
|
||||
while p < len {
|
||||
let n = HEADER_BYTES.min(len - p);
|
||||
let mut head = prefix.get(p..).unwrap_or_default().iter().copied().take(n).collect::<Vec<u8>>();
|
||||
if head.len() < n {
|
||||
let stored = (p + head.len() - prefix.len()) as u64;
|
||||
head.extend(src.bytes(at + stored, n - head.len())?);
|
||||
}
|
||||
let h = ac3::parse_header(&head).ok()?;
|
||||
if h.is_primary() {
|
||||
frames += h.samples();
|
||||
}
|
||||
p += h.frame_bytes.max(1);
|
||||
}
|
||||
(len > 0).then_some(frames)
|
||||
}
|
||||
|
||||
/// The next packet of Matroska track `track` and its timestamp in nanoseconds.
|
||||
fn mkv_next(demux: &mut matroska::Demuxer<Src<'_>>, track: usize, rate: u32) -> Option<Result<(Packet, i64)>> {
|
||||
loop {
|
||||
@@ -319,7 +367,9 @@ impl<'a> Native<'a> {
|
||||
Some(Container::Ogg) if ogg::sniff(body) => open_ogg(src, track),
|
||||
Some(c @ (Container::Mp4 | Container::QuickTime)) if head.get(4..8) == Some(b"ftyp") => open_mp4(src, c, track),
|
||||
Some(c @ (Container::Matroska | Container::WebM)) if body.starts_with(&[0x1A, 0x45, 0xDF, 0xA3]) => open_mkv(src, c, track),
|
||||
Some(Container::Adts) | None => single(open_adts(src, at)),
|
||||
Some(Container::Adts) => single(open_adts(src, at)),
|
||||
Some(Container::Ac3) => single(open_ac3(src, at)),
|
||||
None => single(open_ac3(src, at).or_else(|| open_adts(src, at))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -649,6 +699,23 @@ fn open_adts<'a>(src: &Src<'a>, at: u64) -> Option<Result<Native<'a>>> {
|
||||
Some(Ok(n))
|
||||
}
|
||||
|
||||
/// A raw AC-3 or E-AC-3 stream; E-AC-3 dependent substreams decode to nothing and count no frames.
|
||||
fn open_ac3<'a>(src: &Src<'a>, at: u64) -> Option<Result<Native<'a>>> {
|
||||
let frames = match raw_frames(src, at, |rest| {
|
||||
ac3::parse_header(rest).ok().map(|h| (h.frame_bytes, if h.is_primary() { h.samples() as u32 } else { 0 }))
|
||||
}) {
|
||||
Ok(frames) => frames?,
|
||||
Err(e) => return Some(Err(e)),
|
||||
};
|
||||
let total = frames.iter().map(|&(_, _, f)| u64::from(f)).sum();
|
||||
let lookahead = match raw_lookahead(src, &frames, 1) {
|
||||
Ok(lookahead) => lookahead,
|
||||
Err(e) => return Some(Err(e)),
|
||||
};
|
||||
native(Container::Ac3, Packets::Raw { frames, src: src.clone(), next: 0 }, &CodecParams::new(CodecId::Ac3), &lookahead, 0, None, Some(total))
|
||||
.map(Ok)
|
||||
}
|
||||
|
||||
/// The `n`-th audio track (the first when `None`) among `kinds`, as an index into them.
|
||||
fn audio_track(kinds: impl Iterator<Item = bool>, n: Option<usize>) -> Option<Result<usize>> {
|
||||
let audio: Vec<usize> = kinds.enumerate().filter(|&(_, audio)| audio).map(|(i, _)| i).collect();
|
||||
@@ -766,6 +833,9 @@ fn open_mkv<'a>(src: &Src<'a>, container: Container, track: Option<usize>) -> Op
|
||||
let probe = TrackDecoder::new(¶ms, &lookahead.iter().map(Vec::as_slice).collect::<Vec<_>>()).ok()?;
|
||||
let rate = probe.sample_rate();
|
||||
let frames_of = match (probe.frames_per_packet(), params.pcm) {
|
||||
_ if probe.codec() == CodecId::Eac3 => {
|
||||
MkvFrames::Eac3 { samples: t.samples.iter().map(|s| (s.offset, s.size)).collect(), prefix: t.frame_prefix(0), starts: Vec::new() }
|
||||
}
|
||||
(Some(n), _) => MkvFrames::Constant(n as u64),
|
||||
(None, Some(pcm)) => {
|
||||
let frame_bytes = u64::from(pcm.bits.div_ceil(8)) * u64::from(params.channels.unwrap_or(1).max(1));
|
||||
@@ -790,8 +860,12 @@ fn open_mkv<'a>(src: &Src<'a>, container: Container, track: Option<usize>) -> Op
|
||||
let duration = if s.duration > 0 {
|
||||
s.duration
|
||||
} else {
|
||||
// ffmpeg 6 writes AAC without block durations, and records the end in a tag.
|
||||
let MkvFrames::Constant(n) = frames_of else { return None };
|
||||
// ffmpeg 6 writes AAC and E-AC-3 without block durations, and records the end in a tag.
|
||||
let n = match &frames_of {
|
||||
MkvFrames::Constant(n) => *n,
|
||||
MkvFrames::Eac3 { prefix, .. } => eac3_packet_frames(src, prefix, s.offset, s.size)? as u64,
|
||||
_ => return None,
|
||||
};
|
||||
let packet = rescale_round(n.saturating_mul(den), u64::from(rate).saturating_mul(num), 1);
|
||||
tagged_last_duration(track_duration_tag(&file.tags, t.uid)?, s.pts, delay_ticks, packet, t.timebase)?
|
||||
};
|
||||
@@ -1212,3 +1286,21 @@ mod adts_tests {
|
||||
assert!(matches!(opened, Err(AudioError::Io(_))), "{:?}", opened.err());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "ac3-decode"))]
|
||||
mod ac3_tests {
|
||||
use crate::ac3::tests::single_block_eac3;
|
||||
use crate::{DecodeOptions, Decoder};
|
||||
|
||||
/// One-block E-AC-3 syncframes may reuse exponents sent long before a seek target.
|
||||
#[test]
|
||||
fn a_seek_in_one_block_e_ac3_keeps_exponents_sent_before_the_preroll() {
|
||||
// Exponents in the first syncframe only, reused by the 19 after it (256 frames each).
|
||||
let file = [single_block_eac3(true), single_block_eac3(false).repeat(19)].concat();
|
||||
let mut dec = Decoder::open(file, Some("ac3"), &DecodeOptions::default()).unwrap();
|
||||
assert_eq!(dec.seek(3000).unwrap(), 3000);
|
||||
let block = dec.next_block().unwrap().unwrap();
|
||||
let peak = block.channels[0].iter().fold(0f32, |m, v| m.max(v.abs()));
|
||||
assert!(peak > 1e-5, "the target decodes with the first syncframe's exponents, not as silence");
|
||||
}
|
||||
}
|
||||
+203
-12
@@ -4,6 +4,8 @@
|
||||
|
||||
#[cfg(feature = "aac-decode")]
|
||||
use crate::aac;
|
||||
#[cfg(feature = "ac3-decode")]
|
||||
use crate::ac3;
|
||||
#[cfg(feature = "opus-decode")]
|
||||
use crate::opus;
|
||||
use crate::pcm::{PcmKind, deinterleave};
|
||||
@@ -22,6 +24,9 @@ enum Inner {
|
||||
/// The format ADTS packets must keep, when their headers gave the configuration.
|
||||
adts_format: Option<AdtsFormat>,
|
||||
},
|
||||
/// The decoder, and what the packets read so far say of the track's packets.
|
||||
#[cfg(feature = "ac3-decode")]
|
||||
Ac3(Box<ac3::Decoder>, Ac3Packets),
|
||||
#[cfg(feature = "opus-decode")]
|
||||
Opus {
|
||||
dec: Box<opus::Decoder>,
|
||||
@@ -38,7 +43,7 @@ enum Inner {
|
||||
///
|
||||
/// It takes the track's [`CodecParams`] (from `isobmff::Track::codec_params`,
|
||||
/// `matroska::Track::codec_params`, `ogg::Stream::codec_params`, or built by hand for another
|
||||
/// container) and decodes AAC (LC; HE-AAC as its upsampled LC core), Opus and PCM.
|
||||
/// container) and decodes AAC (LC; HE-AAC as its upsampled LC core), Opus, AC-3/E-AC-3 and PCM.
|
||||
/// Container timing (AAC priming, Opus pre-skip, end trimming) is the caller's: every decoded
|
||||
/// sample is returned.
|
||||
///
|
||||
@@ -82,7 +87,9 @@ impl std::fmt::Debug for TrackDecoder {
|
||||
impl TrackDecoder {
|
||||
/// A decoder for the track `params` describes. `lookahead` holds the track's first packets
|
||||
/// (up to [`LOOKAHEAD_PACKETS`]; empty when they are not at hand): HE-AAC signalled only
|
||||
/// inside the stream is found in them.
|
||||
/// inside the stream is found in them, and AC-3 takes its rate and channel layout from the
|
||||
/// first frame. Without them those are taken from `params` and set
|
||||
/// from the first decoded packet.
|
||||
///
|
||||
/// Streams without codec configuration are accepted where the packets carry it: AAC with
|
||||
/// empty `extradata` takes its configuration from the ADTS headers of the lookahead (MPEG-TS,
|
||||
@@ -96,9 +103,9 @@ impl TrackDecoder {
|
||||
pub fn new(params: &CodecParams, lookahead: &[&[u8]]) -> Result<TrackDecoder> {
|
||||
let rate = params.sample_rate.unwrap_or(0);
|
||||
let channels = usize::from(params.channels.unwrap_or(0));
|
||||
// Only AAC reads the lookahead; Opus needs none.
|
||||
#[cfg(not(feature = "aac-decode"))]
|
||||
let _ = lookahead;
|
||||
// AC-3 reads its parameters from the first frame; AAC reads the lookahead; Opus needs none.
|
||||
#[cfg_attr(not(feature = "ac3-decode"), allow(unused_variables))]
|
||||
let first = lookahead.first().copied().unwrap_or_default();
|
||||
let (inner, codec, sample_rate, layout, sample_format) = match params.codec {
|
||||
#[cfg(feature = "aac-decode")]
|
||||
CodecId::Aac | CodecId::HeAac => {
|
||||
@@ -132,6 +139,20 @@ impl TrackDecoder {
|
||||
let order = layout.is_ordered().then(|| aac::wav_order(cfg)).flatten();
|
||||
(Inner::Aac { dec: Box::new(dec), up, order, adts_format }, codec, out, layout, SampleFormat::Compressed)
|
||||
}
|
||||
#[cfg(feature = "ac3-decode")]
|
||||
CodecId::Ac3 | CodecId::Eac3 => match ac3::parse_header(first) {
|
||||
Ok(h) => {
|
||||
let codec = if h.is_eac3() { CodecId::Eac3 } else { CodecId::Ac3 };
|
||||
(Inner::Ac3(Box::default(), Ac3Packets::of(lookahead)), codec, h.sample_rate, ac3_layout(&h), SampleFormat::Compressed)
|
||||
}
|
||||
Err(_) => (
|
||||
Inner::Ac3(Box::default(), Ac3Packets::default()),
|
||||
params.codec,
|
||||
rate,
|
||||
ChannelLayout::unordered(crate::layout::count(channels)),
|
||||
SampleFormat::Compressed,
|
||||
),
|
||||
},
|
||||
#[cfg(feature = "opus-decode")]
|
||||
CodecId::Opus => {
|
||||
// Opus over RTP or in MPEG-TS has no OpusHead: mono and stereo need none (RFC 7845
|
||||
@@ -163,7 +184,7 @@ impl TrackDecoder {
|
||||
}
|
||||
|
||||
/// The codec, refined from the stream: [`CodecId::HeAac`] for AAC with spectral band
|
||||
/// replication.
|
||||
/// replication, `Eac3` for Dolby Digital Plus.
|
||||
pub fn codec(&self) -> CodecId {
|
||||
self.codec
|
||||
}
|
||||
@@ -188,9 +209,14 @@ impl TrackDecoder {
|
||||
self.sample_format
|
||||
}
|
||||
|
||||
/// Decode one packet. A packet that fails is an error for every codec: call [`TrackDecoder::conceal`] for audio of its length, so what follows keeps
|
||||
/// Decode one packet. An MP4 or Matroska packet may hold several AC-3 frames;
|
||||
/// an E-AC-3 dependent substream decodes to no frames. A packet that fails is an error for
|
||||
/// every codec: call [`TrackDecoder::conceal`] for audio of its length, so what follows keeps
|
||||
/// its timing.
|
||||
pub fn decode(&mut self, packet: &[u8]) -> Result<AudioBuffer> {
|
||||
// A packet decoded only in part says nothing of how long packets are.
|
||||
#[cfg_attr(not(feature = "ac3-decode"), allow(unused_mut))]
|
||||
let mut partial = false;
|
||||
let channels = match &mut self.inner {
|
||||
#[cfg(feature = "aac-decode")]
|
||||
Inner::Aac { dec, up, order, adts_format } => {
|
||||
@@ -217,6 +243,33 @@ impl TrackDecoder {
|
||||
}
|
||||
reorder(out, *order)
|
||||
}
|
||||
#[cfg(feature = "ac3-decode")]
|
||||
Inner::Ac3(d, shape) => {
|
||||
let mut out: Vec<Vec<f32>> = Vec::new();
|
||||
let mut p = 0;
|
||||
while let Some(rest) = packet.get(p..).filter(|r| !r.is_empty()) {
|
||||
let f = match d.decode(rest) {
|
||||
Ok(f) => f,
|
||||
Err(e) if out.is_empty() => return Err(e.into()),
|
||||
Err(_) => {
|
||||
partial = true;
|
||||
break;
|
||||
}
|
||||
};
|
||||
p += f.header.frame_bytes.max(1);
|
||||
shape.short |= f.header.is_primary() && f.header.samples() < AC3_FRAME_SAMPLES;
|
||||
if !f.channels.is_empty() {
|
||||
if self.last_frames == 0 {
|
||||
self.sample_rate = f.header.sample_rate;
|
||||
self.layout = ac3_layout(&f.header);
|
||||
}
|
||||
append(&mut out, f.channels)?;
|
||||
}
|
||||
}
|
||||
let frames = out.first().map_or(0, Vec::len);
|
||||
shape.varies |= !partial && frames > 0 && shape.first > 0 && frames != shape.first;
|
||||
out
|
||||
}
|
||||
#[cfg(feature = "opus-decode")]
|
||||
Inner::Opus { dec, order } => {
|
||||
let out = dec.decode(Some(packet))?;
|
||||
@@ -227,7 +280,7 @@ impl TrackDecoder {
|
||||
deinterleave(packet, *kind, *channels, frames)?
|
||||
}
|
||||
};
|
||||
if let Some(n) = channels.iter().map(Vec::len).min().filter(|&n| n > 0) {
|
||||
if let Some(n) = channels.iter().map(Vec::len).min().filter(|&n| n > 0 && !partial) {
|
||||
self.last_frames = n;
|
||||
if channels.len() != usize::from(self.layout.channels()) {
|
||||
self.layout = ChannelLayout::unordered(crate::layout::count(channels.len()));
|
||||
@@ -265,6 +318,8 @@ impl TrackDecoder {
|
||||
None => Some(per_unit),
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "ac3-decode")]
|
||||
Inner::Ac3(..) => Some(ac3_packet_frames(data)),
|
||||
Inner::Pcm { kind, channels } => Some(data.len() / (kind.bytes() * *channels).max(1)),
|
||||
};
|
||||
let frames = frames.filter(|&n| n > 0).unwrap_or(self.last_frames);
|
||||
@@ -272,12 +327,21 @@ impl TrackDecoder {
|
||||
}
|
||||
|
||||
/// The frames every packet decodes to, for codecs whose packets all have one length: AAC
|
||||
/// (1024, 2048 for HE-AAC; a packet of several ADTS frames decodes to that many times this);
|
||||
/// `None` for Opus and PCM.
|
||||
/// (1024, 2048 for HE-AAC; a packet of several ADTS frames decodes to that many times this) and
|
||||
/// AC-3 / E-AC-3 (the frames of the last decoded packet, else of the first packet's primary
|
||||
/// syncframes, else 1536); `None` for Opus and PCM, and for AC-3 / E-AC-3 whose packets read so
|
||||
/// far differ in length.
|
||||
pub fn frames_per_packet(&self) -> Option<usize> {
|
||||
match &self.inner {
|
||||
#[cfg(feature = "aac-decode")]
|
||||
Inner::Aac { up, .. } => Some(aac::FRAME_LEN * if up.is_some() { 2 } else { 1 }),
|
||||
#[cfg(feature = "ac3-decode")]
|
||||
Inner::Ac3(_, shape) if shape.varies => None,
|
||||
#[cfg(feature = "ac3-decode")]
|
||||
Inner::Ac3(_, shape) => Some(match (self.last_frames, shape.first) {
|
||||
(0, 0) => AC3_FRAME_SAMPLES,
|
||||
(0, n) | (n, _) => n,
|
||||
}),
|
||||
#[cfg(feature = "opus-decode")]
|
||||
Inner::Opus { .. } => None,
|
||||
Inner::Pcm { .. } => None,
|
||||
@@ -285,11 +349,18 @@ impl TrackDecoder {
|
||||
}
|
||||
|
||||
/// Frames to decode before a seek target and drop, so the decoder has settled when the target
|
||||
/// is reached: two AAC frames, 80 ms of Opus (RFC 7845 §4.6), none for PCM.
|
||||
/// is reached: two AAC frames, 80 ms of Opus (RFC 7845 §4.6), one AC-3 frame (its filter bank
|
||||
/// overlaps one frame), none for PCM. E-AC-3 syncframes of fewer than six blocks may reuse
|
||||
/// exponents from any syncframe before them, so once one has been read the stream decodes from
|
||||
/// its start (`usize::MAX`).
|
||||
pub fn preroll(&self) -> usize {
|
||||
match &self.inner {
|
||||
#[cfg(feature = "aac-decode")]
|
||||
Inner::Aac { up, .. } => 2 * aac::FRAME_LEN * if up.is_some() { 2 } else { 1 },
|
||||
#[cfg(feature = "ac3-decode")]
|
||||
Inner::Ac3(_, shape) if shape.short => usize::MAX,
|
||||
#[cfg(feature = "ac3-decode")]
|
||||
Inner::Ac3(..) => AC3_FRAME_SAMPLES,
|
||||
#[cfg(feature = "opus-decode")]
|
||||
Inner::Opus { .. } => 3840,
|
||||
Inner::Pcm { .. } => 0,
|
||||
@@ -310,6 +381,8 @@ impl TrackDecoder {
|
||||
up.reset();
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "ac3-decode")]
|
||||
Inner::Ac3(d, _) => d.reset(),
|
||||
#[cfg(feature = "opus-decode")]
|
||||
Inner::Opus { dec, .. } => dec.reset(),
|
||||
Inner::Pcm { .. } => {}
|
||||
@@ -320,6 +393,7 @@ impl TrackDecoder {
|
||||
fn unsupported(codec: CodecId) -> AudioError {
|
||||
let feature = match codec {
|
||||
CodecId::Aac | CodecId::HeAac => Some("aac-decode"),
|
||||
CodecId::Ac3 | CodecId::Eac3 => Some("ac3-decode"),
|
||||
CodecId::Opus => Some("opus-decode"),
|
||||
_ => None,
|
||||
};
|
||||
@@ -345,7 +419,7 @@ fn reorder(mut channels: Vec<Vec<f32>>, order: Option<&[usize]>) -> Vec<Vec<f32>
|
||||
|
||||
/// Append `more` to `out` channel by channel. A packet whose frames differ in channel count is
|
||||
/// malformed: one buffer cannot hold them.
|
||||
#[cfg(feature = "aac-decode")]
|
||||
#[cfg(any(feature = "aac-decode", feature = "ac3-decode"))]
|
||||
fn append(out: &mut Vec<Vec<f32>>, more: Vec<Vec<f32>>) -> Result<()> {
|
||||
if out.is_empty() {
|
||||
*out = more;
|
||||
@@ -371,6 +445,51 @@ pub(crate) fn adts_stream_header(headers: &[aac::AdtsHeader]) -> Option<aac::Adt
|
||||
headers.iter().rev().max_by_key(|h| headers.iter().filter(|o| adts_format_of(o) == adts_format_of(h)).count()).copied()
|
||||
}
|
||||
|
||||
/// The frames of a six-block (AC-3 or E-AC-3) syncframe.
|
||||
#[cfg(feature = "ac3-decode")]
|
||||
const AC3_FRAME_SAMPLES: usize = 1536;
|
||||
|
||||
/// What the AC-3 / E-AC-3 packets read so far say of the track's packets.
|
||||
#[cfg(feature = "ac3-decode")]
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
struct Ac3Packets {
|
||||
/// The frames the first packet's primary syncframes hold (0 when unknown).
|
||||
first: usize,
|
||||
/// Packets have decoded to different lengths: a packet's index does not give its position.
|
||||
varies: bool,
|
||||
/// A primary syncframe of fewer than six blocks (E-AC-3), whose exponents may come from any
|
||||
/// syncframe before it.
|
||||
short: bool,
|
||||
}
|
||||
|
||||
#[cfg(feature = "ac3-decode")]
|
||||
impl Ac3Packets {
|
||||
fn of(lookahead: &[&[u8]]) -> Ac3Packets {
|
||||
let lengths: Vec<usize> = lookahead.iter().map(|p| ac3_packet_frames(p)).filter(|&n| n > 0).collect();
|
||||
let short = lookahead.iter().any(|p| ac3_syncframes(p).any(|h| h.is_primary() && h.samples() < AC3_FRAME_SAMPLES));
|
||||
Ac3Packets { first: lookahead.first().map_or(0, |p| ac3_packet_frames(p)), varies: lengths.windows(2).any(|w| w[0] != w[1]), short }
|
||||
}
|
||||
}
|
||||
|
||||
/// The headers of the syncframes `packet` holds, up to the first that does not parse.
|
||||
#[cfg(feature = "ac3-decode")]
|
||||
fn ac3_syncframes(packet: &[u8]) -> impl Iterator<Item = ac3::Header> + '_ {
|
||||
let mut p = 0;
|
||||
std::iter::from_fn(move || {
|
||||
let h = ac3::parse_header(packet.get(p..)?).ok()?;
|
||||
p += h.frame_bytes.max(1);
|
||||
Some(h)
|
||||
})
|
||||
}
|
||||
|
||||
/// The frames an AC-3 / E-AC-3 packet decodes to: those of every primary syncframe in it (an MP4
|
||||
/// or Matroska packet may hold several, as E-AC-3 frames of fewer than six blocks are grouped),
|
||||
/// up to the first that does not parse. 0 when none does.
|
||||
#[cfg(feature = "ac3-decode")]
|
||||
fn ac3_packet_frames(packet: &[u8]) -> usize {
|
||||
ac3_syncframes(packet).filter(ac3::Header::is_primary).map(|h| h.samples()).sum()
|
||||
}
|
||||
|
||||
/// The ADTS frames `packet` holds, 0 when it is not ADTS-framed.
|
||||
#[cfg(feature = "decode-native")]
|
||||
pub(crate) fn adts_frame_count(packet: &[u8]) -> usize {
|
||||
@@ -480,6 +599,23 @@ pub(crate) fn aac_layout(channel_config: u8, channels: usize) -> ChannelLayout {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ac3-decode")]
|
||||
/// The speakers of an AC-3 / E-AC-3 program (A/52 Table 5.8) in the decoder's WAV order: fronts,
|
||||
/// LFE, surrounds. Dual mono (1+1) has no speaker assignment.
|
||||
pub(crate) fn ac3_layout(h: &ac3::Header) -> ChannelLayout {
|
||||
let fronts = match h.acmod & 7 {
|
||||
0 => return ChannelLayout::unordered(crate::layout::count(h.channels())),
|
||||
1 => 0x4,
|
||||
2 => 0x3,
|
||||
3 => 0x7,
|
||||
4 => 0x103,
|
||||
5 => 0x107,
|
||||
6 => 0x603,
|
||||
_ => 0x607,
|
||||
};
|
||||
ChannelLayout::from_mask(fronts | if h.lfeon { 0x8 } else { 0 })
|
||||
}
|
||||
|
||||
#[cfg(feature = "opus-decode")]
|
||||
/// The speakers of an Opus stream (RFC 7845 §5.1.1): Vorbis order for mapping family 1, reordered
|
||||
/// to WAV order by [`opus::wav_order`]; other families have no speaker assignment.
|
||||
@@ -807,6 +943,61 @@ mod tests {
|
||||
assert!(matches!(TrackDecoder::new(&aac_params(), &[&bare]), Err(AudioError::Malformed(_))));
|
||||
}
|
||||
|
||||
#[cfg(feature = "ac3-decode")]
|
||||
#[test]
|
||||
fn e_ac3_packets_are_as_long_as_all_their_syncframes() {
|
||||
use crate::ac3::tests::single_block_eac3;
|
||||
let params = CodecParams::new(CodecId::Eac3);
|
||||
// One block per packet: 256 frames, known before any packet is decoded.
|
||||
let one = single_block_eac3(true);
|
||||
let dec = TrackDecoder::new(¶ms, &[&one]).unwrap();
|
||||
assert_eq!((dec.codec(), dec.frames_per_packet()), (CodecId::Eac3, Some(256)));
|
||||
// Six one-block syncframes to a packet (as MP4 groups them): 1536.
|
||||
let six = [one.clone(), (0..5).flat_map(|_| single_block_eac3(false)).collect()].concat();
|
||||
let mut dec = TrackDecoder::new(¶ms, &[&six]).unwrap();
|
||||
assert_eq!(dec.frames_per_packet(), Some(1536));
|
||||
assert_eq!(dec.decode(&six).unwrap().channels[0].len(), 1536);
|
||||
// A damaged packet conceals at the length of all its syncframes.
|
||||
dec.reset();
|
||||
assert_eq!(dec.conceal(Some(&six)).unwrap().channels[0].len(), 1536);
|
||||
assert_eq!(dec.conceal(Some(&one)).unwrap().channels[0].len(), 256);
|
||||
}
|
||||
|
||||
#[cfg(feature = "ac3-decode")]
|
||||
#[test]
|
||||
fn a_packet_decoded_in_part_keeps_the_length_lost_packets_are_concealed_at() {
|
||||
use crate::ac3::tests::single_block_eac3;
|
||||
let params = CodecParams::new(CodecId::Eac3);
|
||||
let one = single_block_eac3(true);
|
||||
let six = [one.clone(), (0..5).flat_map(|_| single_block_eac3(false)).collect()].concat();
|
||||
let mut dec = TrackDecoder::new(¶ms, &[&six, &six]).unwrap();
|
||||
assert_eq!(dec.decode(&six).unwrap().channels[0].len(), 1536);
|
||||
// The second syncframe is damaged: its first one decodes, the rest of the packet is lost.
|
||||
let damaged = [one.clone(), vec![0x0B, 0x77, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]].concat();
|
||||
assert_eq!(dec.decode(&damaged).unwrap().channels[0].len(), 256);
|
||||
assert_eq!(dec.conceal(None).unwrap().channels[0].len(), 1536, "a lost packet is as long as the last whole one");
|
||||
assert_eq!(dec.frames_per_packet(), Some(1536), "a damaged packet does not make the packets' lengths differ");
|
||||
}
|
||||
|
||||
#[cfg(feature = "ac3-decode")]
|
||||
#[test]
|
||||
fn e_ac3_packets_of_different_lengths_or_short_syncframes_are_not_indexed() {
|
||||
use crate::ac3::tests::single_block_eac3;
|
||||
let params = CodecParams::new(CodecId::Eac3);
|
||||
let one = single_block_eac3(true);
|
||||
let two = [single_block_eac3(true), single_block_eac3(false)].concat();
|
||||
// Packets of 256 and 512 frames: a packet's index does not give its position.
|
||||
let dec = TrackDecoder::new(¶ms, &[&one, &one, &two]).unwrap();
|
||||
assert_eq!(dec.frames_per_packet(), None);
|
||||
// Known only once decoded: from then on too.
|
||||
let mut dec = TrackDecoder::new(¶ms, &[&one]).unwrap();
|
||||
assert_eq!(dec.frames_per_packet(), Some(256));
|
||||
dec.decode(&two).unwrap();
|
||||
assert_eq!(dec.frames_per_packet(), None);
|
||||
// One-block syncframes may reuse exponents from any syncframe before them.
|
||||
assert_eq!(TrackDecoder::new(¶ms, &[&one]).unwrap().preroll(), usize::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codecs_without_a_packet_decoder_are_unsupported() {
|
||||
for codec in [CodecId::Flac, CodecId::Vorbis, CodecId::Alac, CodecId::Mp3, CodecId::H264] {
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
//! AC-3 and E-AC-3 against ffmpeg: ffmpeg's AC-3 and E-AC-3 encoders write raw syncframes from
|
||||
//! generated tones and noise; ffmpeg's decode of the same file is the reference. AC-3 output is not bit-exact across
|
||||
//! decoders (zero-bit mantissas are filled with decoder-specific dither, §7.3.4; float vs fixed
|
||||
//! arithmetic), so the criterion is SNR against ffmpeg's decode.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
struct Spec {
|
||||
name: &'static str,
|
||||
/// ffmpeg encoder and raw muxer: "ac3" or "eac3".
|
||||
codec: &'static str,
|
||||
input: &'static str,
|
||||
rate: u32,
|
||||
channels: u32,
|
||||
enc: &'static [&'static str],
|
||||
/// Minimum SNR (dB) against ffmpeg.
|
||||
min_snr: f64,
|
||||
}
|
||||
|
||||
const SPECS: &[Spec] = &[
|
||||
Spec {
|
||||
name: "ac3_stereo_192k",
|
||||
codec: "ac3",
|
||||
input: "aevalsrc=exprs=0.3*sin(2*PI*441*t)+0.1*sin(2*PI*3100*t)|0.25*sin(2*PI*660*t)+0.1*sin(2*PI*(500+4000*t)*t):s=48000",
|
||||
rate: 48_000,
|
||||
channels: 2,
|
||||
enc: &["-b:a", "192k"],
|
||||
min_snr: 60.0,
|
||||
},
|
||||
// low bitrate stereo: coupling and rematrixing
|
||||
Spec {
|
||||
name: "ac3_stereo_96k_coupling",
|
||||
codec: "ac3",
|
||||
input: "aevalsrc=exprs=0.3*sin(2*PI*441*t)+0.05*sin(2*PI*9100*t)|0.3*sin(2*PI*441*t)+0.05*sin(2*PI*11000*t):s=44100",
|
||||
rate: 44_100,
|
||||
channels: 2,
|
||||
enc: &["-b:a", "96k", "-channel_coupling", "1"],
|
||||
min_snr: 40.0,
|
||||
},
|
||||
Spec {
|
||||
name: "ac3_mono_32k",
|
||||
codec: "ac3",
|
||||
input: "sine=frequency=1000:sample_rate=32000",
|
||||
rate: 32_000,
|
||||
channels: 1,
|
||||
enc: &["-b:a", "96k"],
|
||||
min_snr: 60.0,
|
||||
},
|
||||
// 5.1 with LFE
|
||||
Spec {
|
||||
name: "ac3_51_448k",
|
||||
codec: "ac3",
|
||||
input: "aevalsrc=exprs=0.3*sin(2*PI*300*t)|0.3*sin(2*PI*400*t)|0.3*sin(2*PI*500*t)|0.3*sin(2*PI*60*t)|0.2*sin(2*PI*700*t)|0.2*sin(2*PI*800*t):s=48000:c=5.1",
|
||||
rate: 48_000,
|
||||
channels: 6,
|
||||
enc: &["-b:a", "448k"],
|
||||
min_snr: 55.0,
|
||||
},
|
||||
// noise (wideband, many zero-bit mantissas): the SNR is limited by dither
|
||||
Spec {
|
||||
name: "ac3_noise_stereo",
|
||||
codec: "ac3",
|
||||
input: "anoisesrc=color=pink:sample_rate=48000:amplitude=0.3:seed=3,pan=stereo|c0=c0|c1=-0.5*c0",
|
||||
rate: 48_000,
|
||||
channels: 2,
|
||||
enc: &["-b:a", "128k"],
|
||||
min_snr: 18.0,
|
||||
},
|
||||
// E-AC-3 (Annex E): ffmpeg writes six-block syncframes with frame-based exponent strategies
|
||||
Spec {
|
||||
name: "eac3_stereo_192k",
|
||||
codec: "eac3",
|
||||
input: "aevalsrc=exprs=0.3*sin(2*PI*441*t)+0.1*sin(2*PI*3100*t)|0.25*sin(2*PI*660*t)+0.1*sin(2*PI*(500+4000*t)*t):s=48000",
|
||||
rate: 48_000,
|
||||
channels: 2,
|
||||
enc: &["-b:a", "192k"],
|
||||
min_snr: 60.0,
|
||||
},
|
||||
// low bitrate stereo: coupling (with the default band structure) and rematrixing
|
||||
Spec {
|
||||
name: "eac3_stereo_64k_coupling",
|
||||
codec: "eac3",
|
||||
input: "aevalsrc=exprs=0.3*sin(2*PI*441*t)+0.05*sin(2*PI*9100*t)|0.3*sin(2*PI*441*t)+0.05*sin(2*PI*11000*t):s=44100",
|
||||
rate: 44_100,
|
||||
channels: 2,
|
||||
enc: &["-b:a", "64k", "-channel_coupling", "1"],
|
||||
min_snr: 35.0,
|
||||
},
|
||||
Spec {
|
||||
name: "eac3_mono_32k",
|
||||
codec: "eac3",
|
||||
input: "sine=frequency=1000:sample_rate=32000",
|
||||
rate: 32_000,
|
||||
channels: 1,
|
||||
enc: &["-b:a", "64k"],
|
||||
min_snr: 60.0,
|
||||
},
|
||||
// 5.1 with LFE and mixing / informational metadata in the bsi
|
||||
Spec {
|
||||
name: "eac3_51_384k_metadata",
|
||||
codec: "eac3",
|
||||
input: "aevalsrc=exprs=0.3*sin(2*PI*300*t)|0.3*sin(2*PI*400*t)|0.3*sin(2*PI*500*t)|0.3*sin(2*PI*60*t)|0.2*sin(2*PI*700*t)|0.2*sin(2*PI*800*t):s=48000:c=5.1",
|
||||
rate: 48_000,
|
||||
channels: 6,
|
||||
enc: &[
|
||||
"-b:a",
|
||||
"384k",
|
||||
"-dmix_mode",
|
||||
"loro",
|
||||
"-ltrt_cmixlev",
|
||||
"0.707",
|
||||
"-loro_surmixlev",
|
||||
"0.5",
|
||||
"-dsurex_mode",
|
||||
"on",
|
||||
"-mixing_level",
|
||||
"100",
|
||||
"-room_type",
|
||||
"small",
|
||||
],
|
||||
min_snr: 55.0,
|
||||
},
|
||||
Spec {
|
||||
name: "eac3_noise_stereo",
|
||||
codec: "eac3",
|
||||
input: "anoisesrc=color=pink:sample_rate=48000:amplitude=0.3:seed=3,pan=stereo|c0=c0|c1=-0.5*c0",
|
||||
rate: 48_000,
|
||||
channels: 2,
|
||||
enc: &["-b:a", "96k", "-dsur_mode", "on", "-dheadphone_mode", "off"],
|
||||
min_snr: 18.0,
|
||||
},
|
||||
];
|
||||
|
||||
fn make(ff: &Path, s: &Spec) -> Option<PathBuf> {
|
||||
let out = crate::testkit::fixtures_dir("ac3").join(format!("{}.{}", s.name, s.codec));
|
||||
crate::testkit::fixtures::generate(&out, |tmp| {
|
||||
let st = Command::new(ff)
|
||||
.args(["-hide_banner", "-v", "error", "-y", "-f", "lavfi", "-i", s.input, "-t", "1"])
|
||||
.args(["-c:a", s.codec])
|
||||
.args(s.enc)
|
||||
.args(["-f", s.codec])
|
||||
.arg(tmp)
|
||||
.stdin(Stdio::null())
|
||||
.status();
|
||||
matches!(st, Ok(x) if x.success())
|
||||
})
|
||||
}
|
||||
|
||||
fn frames(data: &[u8]) -> Vec<&[u8]> {
|
||||
let mut out = Vec::new();
|
||||
let mut p = 0;
|
||||
while let Ok(h) = craft_audio::ac3::parse_header(&data[p..]) {
|
||||
if p + h.frame_bytes > data.len() {
|
||||
break;
|
||||
}
|
||||
out.push(&data[p..p + h.frame_bytes]);
|
||||
p += h.frame_bytes;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ffmpeg_encoded_streams_decode() {
|
||||
let ff = require_ffmpeg!();
|
||||
for s in SPECS {
|
||||
let f = make(&ff, s).unwrap_or_else(|| panic!("could not generate {}", s.name));
|
||||
let data = std::fs::read(&f).unwrap();
|
||||
let mut dec = craft_audio::ac3::Decoder::new();
|
||||
let mut ours: Vec<Vec<f32>> = vec![Vec::new(); s.channels as usize];
|
||||
for fr in frames(&data) {
|
||||
let d = dec.decode(fr).unwrap();
|
||||
assert_eq!((d.header.sample_rate, d.channels.len() as u32), (s.rate, s.channels), "{}", s.name);
|
||||
assert_eq!(d.header.is_eac3(), s.codec == "eac3", "{}", s.name);
|
||||
for (o, c) in ours.iter_mut().zip(d.channels) {
|
||||
o.extend(c);
|
||||
}
|
||||
}
|
||||
let o = Command::new(&ff).args(["-v", "error", "-i"]).arg(&f).args(["-f", "f32le", "-"]).output().unwrap();
|
||||
let want: Vec<f32> = o.stdout.as_chunks::<4>().0.iter().map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect();
|
||||
let ch = s.channels as usize;
|
||||
let n = want.len() / ch;
|
||||
assert_eq!(n, ours[0].len(), "{}: sample count", s.name);
|
||||
let mut worst = f64::INFINITY;
|
||||
for c in 0..ch {
|
||||
let (mut sig, mut err) = (0f64, 0f64);
|
||||
for i in 0..n {
|
||||
let (a, b) = (ours[c][i] as f64, want[i * ch + c] as f64);
|
||||
sig += b * b;
|
||||
err += (a - b) * (a - b);
|
||||
}
|
||||
let snr = 10.0 * (sig / err.max(1e-30)).log10();
|
||||
worst = worst.min(snr);
|
||||
}
|
||||
// the same stream decoded with another dither sequence: how far dither alone moves the
|
||||
// output (the difference to ffmpeg must be of that order)
|
||||
let mut dec2 = craft_audio::ac3::Decoder::new();
|
||||
dec2.set_dither_seed(0xDEAD_BEEF);
|
||||
let other: Vec<f32> = frames(&data).iter().flat_map(|fr| dec2.decode(fr).unwrap().channels.swap_remove(0)).collect();
|
||||
let (mut sig, mut err) = (0f64, 0f64);
|
||||
for i in 0..n {
|
||||
sig += (ours[0][i] as f64).powi(2);
|
||||
err += (ours[0][i] as f64 - other[i] as f64).powi(2);
|
||||
}
|
||||
let dither_snr = 10.0 * (sig / err.max(1e-30)).log10();
|
||||
println!(
|
||||
"{}: {} Hz, {} ch, {} frames, worst channel SNR vs ffmpeg {worst:.1} dB (two dither seeds: {dither_snr:.1} dB)",
|
||||
s.name,
|
||||
s.rate,
|
||||
ch,
|
||||
frames(&data).len()
|
||||
);
|
||||
assert!(worst >= s.min_snr, "{}: SNR {worst:.1} dB < {}", s.name, s.min_snr);
|
||||
}
|
||||
}
|
||||
|
||||
/// Damaged E-AC-3 syncframes (bit flips, truncation, corrupt sizes) give errors or noise, never
|
||||
/// a panic.
|
||||
#[test]
|
||||
fn damaged_eac3_frames_never_panic() {
|
||||
let ff = require_ffmpeg!();
|
||||
let mut x = 0x00C0_FFEEu32;
|
||||
let mut rnd = move || {
|
||||
x ^= x << 13;
|
||||
x ^= x >> 17;
|
||||
x ^= x << 5;
|
||||
x
|
||||
};
|
||||
for s in SPECS.iter().filter(|s| s.codec == "eac3") {
|
||||
let f = make(&ff, s).unwrap_or_else(|| panic!("could not generate {}", s.name));
|
||||
let data = std::fs::read(&f).unwrap();
|
||||
let mut dec = craft_audio::ac3::Decoder::new();
|
||||
for fr in frames(&data).into_iter().take(8) {
|
||||
for k in 0..300 {
|
||||
let mut m = fr.to_vec();
|
||||
match k % 3 {
|
||||
0 => {
|
||||
for _ in 0..1 + rnd() % 8 {
|
||||
let bit = rnd() as usize % (m.len() * 8);
|
||||
m[bit / 8] ^= 0x80 >> (bit % 8);
|
||||
}
|
||||
}
|
||||
1 => m.truncate(rnd() as usize % m.len()),
|
||||
_ => {
|
||||
// keep the sync word, scramble frmsiz / fscod / numblkscod / acmod
|
||||
m[2] ^= rnd() as u8;
|
||||
m[3] ^= rnd() as u8;
|
||||
m[4] ^= rnd() as u8;
|
||||
}
|
||||
}
|
||||
let _ = dec.decode(&m);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn generate_fixtures() {
|
||||
let Some(ff) = crate::testkit::ffmpeg_or_skip("ac3 fixtures") else { return };
|
||||
for s in SPECS {
|
||||
let out = crate::testkit::fixtures_dir("ac3").join(format!("{}.{}", s.name, s.codec));
|
||||
crate::testkit::fixtures::generate_and_report(&format!("ac3/{}", s.name), &[out], || make(&ff, s));
|
||||
}
|
||||
}
|
||||
|
||||
/// E-AC-3 in Matroska whose packets change length after the packets read ahead at open (here
|
||||
/// one syncframe each, then two): a seek finds its packet's place from the packets' own headers,
|
||||
/// not as the first packet's length times its index.
|
||||
#[test]
|
||||
fn a_seek_in_matroska_e_ac3_whose_packets_change_length_lands_exactly() {
|
||||
use craft_media::matroska::{MkvWriter, MuxOptions, TrackKind, TrackSpec};
|
||||
let ff = require_ffmpeg!();
|
||||
let s = SPECS.iter().find(|s| s.name == "eac3_stereo_192k").unwrap();
|
||||
let raw = std::fs::read(make(&ff, s).unwrap_or_else(|| panic!("could not generate {}", s.name))).unwrap();
|
||||
let syncframes = frames(&raw);
|
||||
let mut track = TrackSpec::new(TrackKind::Audio, "A_EAC3");
|
||||
track.audio = Some((f64::from(s.rate), u64::from(s.channels), None));
|
||||
let mut mkv = MkvWriter::new(std::io::Cursor::new(Vec::new()), vec![track], MuxOptions::default()).unwrap();
|
||||
let (mut i, mut pts_ns) = (0, 0i64);
|
||||
while i < syncframes.len() {
|
||||
let n = if i < 12 { 1 } else { 2.min(syncframes.len() - i) };
|
||||
let packet = syncframes[i..i + n].concat();
|
||||
let duration = 32_000_000 * n as u64;
|
||||
mkv.write_frame(0, pts_ns, true, &packet, Some(duration)).unwrap();
|
||||
pts_ns += duration as i64;
|
||||
i += n;
|
||||
}
|
||||
let file = mkv.finish().unwrap().into_inner();
|
||||
|
||||
let (_, whole) = craft_audio::decode(&file, Some("mkv")).unwrap();
|
||||
assert_eq!(whole.frames(), syncframes.len() * 1536);
|
||||
for target in [20 * 1536 + 700, 30 * 1536] {
|
||||
let mut dec = craft_audio::Decoder::open(file.clone(), Some("mkv"), &craft_audio::DecodeOptions::default()).unwrap();
|
||||
assert_eq!(dec.seek(target as u64).unwrap(), target as u64);
|
||||
let block = dec.next_block().unwrap().unwrap();
|
||||
let n = block.channels[0].len();
|
||||
assert!(n > 0);
|
||||
for (ours, theirs) in block.channels.iter().zip(&whole.channels) {
|
||||
assert!(ours[..] == theirs[target..target + n], "after a seek to {target}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use craft_media_testkit as common;
|
||||
mod testkit;
|
||||
|
||||
mod aac;
|
||||
mod ac3_oracle;
|
||||
mod concealment;
|
||||
mod opus;
|
||||
mod routing_oracle;
|
||||
|
||||
@@ -27,7 +27,7 @@ struct Case {
|
||||
channels: usize,
|
||||
/// Presented frames, or `None` for a raw stream (as many as ffmpeg decodes).
|
||||
frames: Option<usize>,
|
||||
/// Minimum SNR against ffmpeg.
|
||||
/// Minimum SNR against ffmpeg (AC-3 is limited by decoder-specific dither, §7.3.4).
|
||||
min_snr: f64,
|
||||
}
|
||||
|
||||
@@ -149,6 +149,71 @@ const CASES: &[Case] = &[
|
||||
frames: Some(240_000),
|
||||
min_snr: 120.0,
|
||||
},
|
||||
Case {
|
||||
name: "ac3.ac3",
|
||||
surround: false,
|
||||
mono: false,
|
||||
rate: 48_000,
|
||||
args: &["-c:a", "ac3", "-b:a", "192k"],
|
||||
container: Container::Ac3,
|
||||
codec: CodecId::Ac3,
|
||||
layout: ChannelLayout::STEREO,
|
||||
channels: 2,
|
||||
frames: None,
|
||||
min_snr: 25.0,
|
||||
},
|
||||
Case {
|
||||
name: "ac3-5.1.ac3",
|
||||
surround: true,
|
||||
mono: false,
|
||||
rate: 48_000,
|
||||
args: &["-c:a", "ac3", "-b:a", "448k"],
|
||||
container: Container::Ac3,
|
||||
codec: CodecId::Ac3,
|
||||
layout: ChannelLayout::SURROUND_5_1_SIDE,
|
||||
channels: 6,
|
||||
frames: None,
|
||||
min_snr: 25.0,
|
||||
},
|
||||
Case {
|
||||
name: "eac3.eac3",
|
||||
surround: false,
|
||||
mono: false,
|
||||
rate: 48_000,
|
||||
args: &["-c:a", "eac3", "-b:a", "192k"],
|
||||
container: Container::Ac3,
|
||||
codec: CodecId::Eac3,
|
||||
layout: ChannelLayout::STEREO,
|
||||
channels: 2,
|
||||
frames: None,
|
||||
min_snr: 25.0,
|
||||
},
|
||||
Case {
|
||||
name: "ac3.mp4",
|
||||
surround: false,
|
||||
mono: false,
|
||||
rate: 48_000,
|
||||
args: &["-c:a", "ac3"],
|
||||
container: Container::Mp4,
|
||||
codec: CodecId::Ac3,
|
||||
layout: ChannelLayout::STEREO,
|
||||
channels: 2,
|
||||
frames: Some(240_000),
|
||||
min_snr: 25.0,
|
||||
},
|
||||
Case {
|
||||
name: "eac3-5.1.mkv",
|
||||
surround: true,
|
||||
mono: false,
|
||||
rate: 48_000,
|
||||
args: &["-c:a", "eac3"],
|
||||
container: Container::Matroska,
|
||||
codec: CodecId::Eac3,
|
||||
layout: ChannelLayout::SURROUND_5_1_SIDE,
|
||||
channels: 6,
|
||||
frames: Some(240_000),
|
||||
min_snr: 25.0,
|
||||
},
|
||||
Case {
|
||||
name: "pcm-s24be.mov",
|
||||
surround: false,
|
||||
@@ -222,6 +287,7 @@ fn ffmpeg_major(ff: &Path) -> Option<u32> {
|
||||
fn reference_lead(name: &str) -> usize {
|
||||
match name {
|
||||
"aac.mkv" => 1024,
|
||||
"eac3-5.1.mkv" => 256,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
@@ -243,6 +309,16 @@ fn snr(ours: &[Vec<f32>], reference: &[f32]) -> f64 {
|
||||
if e == 0.0 { f64::INFINITY } else { 10.0 * (s / e).log10() }
|
||||
}
|
||||
|
||||
/// How many frames short of the input ffmpeg before 9 ends `name`: its lengths for these tracks
|
||||
/// leave out about the encoder's delay. The decoder presents what the file declares.
|
||||
fn early_end(name: &str) -> usize {
|
||||
match name {
|
||||
"ac3.mp4" => 288,
|
||||
"eac3-5.1.mkv" => 240,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_container_and_codec_decodes_home_grown() {
|
||||
let ff = require_ffmpeg!();
|
||||
@@ -258,7 +334,8 @@ fn every_container_and_codec_decodes_home_grown() {
|
||||
assert_eq!((info.container, info.codec, info.sample_rate, buf.channels.len()), (c.container, c.codec, c.rate, c.channels), "{}", c.name);
|
||||
assert_eq!(info.layout, c.layout, "{}: layout", c.name);
|
||||
let expected = c.frames.unwrap_or(reference.len() / c.channels);
|
||||
assert_eq!(buf.frames(), expected, "{}: presented frames", c.name);
|
||||
let frames = buf.frames();
|
||||
assert!(frames == expected || frames == expected - early_end(c.name), "{}: presented frames: {frames} of {expected}", c.name);
|
||||
// One alignment, so audio that keeps the priming, or loses more than it, cannot pass.
|
||||
let lead = reference_lead(c.name) * c.channels;
|
||||
let s = match major {
|
||||
@@ -315,8 +392,7 @@ fn snr_planar(ours: &[Vec<f32>], reference: &[Vec<f32>]) -> f64 {
|
||||
}
|
||||
|
||||
/// The streaming decoder gives the same audio as `decode`, block by block, and a seek lands on the
|
||||
/// exact frame: what follows matches `decode` from there (bit for bit for the deterministic
|
||||
/// codecs; within the codec's bound where the decoder state is reconstructed from a pre-roll).
|
||||
/// exact frame: what follows matches `decode` from there, bit for bit.
|
||||
#[test]
|
||||
fn streaming_and_seeking_match_the_whole_file_decode() {
|
||||
use craft_audio::{DecodeOptions, Decoder};
|
||||
@@ -345,7 +421,9 @@ fn streaming_and_seeking_match_the_whole_file_decode() {
|
||||
assert_eq!(after[0].len(), whole.frames() - target, "{}: frames after a seek to {target}", c.name);
|
||||
let expected: Vec<Vec<f32>> = whole.channels.iter().map(|ch| ch[target..].to_vec()).collect();
|
||||
let s = snr_planar(&after, &expected);
|
||||
// Opus rebuilds its predictor state from the pre-roll.
|
||||
// Opus rebuilds its predictor state from the pre-roll. The rest are exact, AC-3 / E-AC-3
|
||||
// too: their dither is seeded from each syncframe, and the pre-roll frame restores the
|
||||
// overlap.
|
||||
let bound = if c.codec == CodecId::Opus { c.min_snr.min(25.0) } else { f64::INFINITY };
|
||||
assert!(s >= bound, "{}: SNR {s:.1} dB after a seek to {target}", c.name);
|
||||
}
|
||||
|
||||
@@ -58,4 +58,9 @@ pub mod fixtures {
|
||||
std::fs::rename(&tmp, out).ok()?;
|
||||
Some(out.to_path_buf())
|
||||
}
|
||||
|
||||
/// Run a fixture generator; FilmCraft's version also reports it to `cargo xtask fixtures`.
|
||||
pub fn generate_and_report<T>(_name: &str, _outputs: &[PathBuf], generate: impl FnOnce() -> Option<T>) -> Option<T> {
|
||||
generate()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user