Skip to content

# AC-3 decoded nothing from streams other encoders wrote - #200

Merged
Hawkynt merged 1 commit into
mainfrom
fix/ac3-sync
Sep 6, 2026
Merged

# AC-3 decoded nothing from streams other encoders wrote#200
Hawkynt merged 1 commit into
mainfrom
fix/ac3-sync

Conversation

@Hawkynt

@Hawkynt Hawkynt commented Sep 6, 2026

Copy link
Copy Markdown
Owner

A libavcodec AC-3 stream desynchronised inside the first audio block: 17 of 18 returned zero bytes, the 18th 3.1% coverage. The earlier diagnosis blamed the block-reuse paths — that was a symptom. The arithmetic core was wrong, and our encoder was wrong in the matching way, so the round trip passed.

Faults

  • Exponent expansion. A/52 §7.1.3 says exp[0] = absexp covers one bin; the code filled a whole D25/D45 group with it, shifting the envelope by 1 or 3 bins. The coupling channel's reference exponent is also doubled and occupies no bin.
  • A phsflginu bit was read that Table 5.3 does not have — one spurious bit per coupled block.
  • The rematrixing band count was 3/2/1 where the spec says 4/3/2.
  • deltbae codes and their segment lists were read interleaved; Table 5.3 has two separate loops.
  • Coupling order. The coupling channel's mantissas belong after the first coupled channel, and every later coupled channel's reconstruction was cleared right after being written.
  • Bit allocation. The excitation function added fgain/sgain instead of subtracting, and lacked the band 0–6 bootstrap, the dbknee compensation and the coupling leak init (§7.2.2.4/5).
  • The tables were approximations, not the spec's. Hearing threshold, log-add and window — Tables 7.15/7.14/7.33. latab was truncated to 193 of 256 entries with 0x01 where the spec has 0.
  • The IMDCT ran at twice the intended frequency and dropped the ×2 of §7.9.4 step 6.

Output is also now in ITU/WAVE channel order rather than the bit stream's own, derived empirically per layout.

Measured, against ffmpeg's decode of the same bytes

before after
35 streams 0 bytes on 17/18; 3.1% coverage on the 18th 100% coverage, rms < 1 LSB except coupled stereo
coupled stereo peak 293–590, rms 28–93 against reference rms 3147–13860

The coupled-stereo residual is the zero-bit dither §7.3.4 leaves to the implementation. Proved rather than assumed: disabling our dither drops the error by ≈√2, exactly what independent noise of equal power predicts.

The encoder shared the broken transform, so it moved too — ffmpeg now decodes its output at 57–74 dB SNR tonal, 60 dB mono, 53 dB 5.1.

Tests

Ac3ForeignStreamTests embeds ffmpeg-produced streams and ffmpeg's decode of them, asserting sample by sample. Four original faults were re-introduced one at a time and each is caught: exponent expansion, coupling order, the phase-flag bit, the channel map. Two needed better fixtures before they would fail — true-stereo noise, and a mono tone at 1411 kbit/s that reaches the fine quantiser steps — and those fixtures are committed.

64 passed, 0 failed for ~Ac3. Full suite on the source tree: 29030 passed, 0 failed, 1355 skipped.

Ac3CodecTests had a test pinning the exponent bug, and AudioRoutingOracleTests asserted the decoder refuses ffmpeg AC-3; both now assert it decodes.

Flagged, not fixed

E-AC-3 stereo now returns nothing where it previously returned full-length garbage — a behaviour change I caused. Ac3EnhancedFrameDecoder feeds AHT hebap pointers (> 15) straight into Ac3Mantissas.Next, which now throws, and RejectSilentDecode turns that into an explicit refusal, so no silent silence escapes. E-AC-3 mono went the other way: 6.2% coverage of garbage to 100% coverage at peak 10 LSB.

Ac3Codec.Encode still claims to be a "managed adaptation of FFmpeg's LGPL ac3enc.c" without the vendoring records AGENTS.md requires. Worth a look.

A libavcodec-encoded stream came back as zero samples, or a fraction of one
frame: the reader desynchronised inside the first audio block and every frame
after it was dropped. Our own encoder never sets the block-reuse, coupling,
rematrixing or delta-allocation fields, so none of those paths had ever run.

Root causes, all against ATSC A/52:2012:

- Exponents (7.1.3): the absolute exponent covers one bin, not a whole D25/D45
  group, and the coupling channel's reference exponent is doubled and occupies
  no bin at all.
- Audio block syntax (Table 5.3): a phase-flag bit was read that the syntax
  does not have; the rematrixing band count was one short whenever coupling was
  on; the delta bit-allocation codes and their segment lists were read
  interleaved rather than as the two separate loops the table specifies.
- Mantissas: the coupling channel's mantissas belong immediately after the
  first coupled channel, and the reconstruction of every later coupled channel
  was being cleared again right after it was written.
- Bit allocation (7.2.2): the excitation function added the gain terms instead
  of subtracting them and had neither the band 0..6 bootstrap, the dbknee
  compensation, nor the coupling leak initialisation; delta allocation used the
  wrong offset encoding and gain mapping.
- Tables: the hearing threshold, the log-add table and the transform window
  were approximations rather than Tables 7.15, 7.14 and 7.33.
- Transform (7.9.4): the IMDCT ran at twice the intended frequency and dropped
  the factor of two the overlap-add step applies.

Decoded PCM now comes out in the ITU/WAVE interleave order rather than the bit
stream's own channel order, so multichannel output lands where consumers expect
it; the encoder follows the corrected transform, exponent layout and input
channel order.

Verified against ffmpeg n9.0.1 over 35 streams (mono through 5.1, 32/44.1/48
kHz, 96-640 kbit/s, tones, noise, chirps, squares, transients, silence): all
decode at full length, error rms is below one LSB except on coupled stereo,
where what remains is the zero-bit dither A/52 7.3.4 leaves to the decoder.
@Hawkynt
Hawkynt merged commit 1520b8a into main Sep 6, 2026
4 checks passed
@Hawkynt
Hawkynt deleted the fix/ac3-sync branch September 6, 2026 16:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant