Add AC3 RF audio demodulation in Python (lddecode/ac3rf.py)#1050
Merged
Conversation
Contributor
Author
|
Pushed ef928ce, syncing the DPLL loop gains with the reference C++ implementation (museld b82e92b): the proportional/integral gains are now computed from the second-order loop design parameters (1800 Hz natural frequency, damping 0.6, detector/VCO gain 0.3) instead of hard-coded constants. Output on real captures is unchanged (re-verified 100% symbol agreement against the updated C++ on three captures at 40 MHz and 62.5 MHz); the faster loop tracks wow/flutter substantially better per the reference implementation's testing. |
Port of the Ac3RfDemodulator/Ac3DPLL C++ classes from https://github.com/staffanu/museld (player/src/ac3/) to Python, following the same implementation pattern as efm_pll.py: numpy/scipy for filter design, numba for the sequential DPLL loop and the FIR decimation kernel. The demodulator takes raw RF samples (any block length; filter and timing state is carried across calls) and outputs a stream of raw differential QPSK symbols. Framing, Reed-Solomon error correction and AC3 frame assembly happen downstream in decode-orc's ac3rf_sink stage. Verified bit-exact (100.000% symbol agreement) against the C++ implementation on three real AC3 disc captures at 40 MHz and 62.5 MHz sample rates. Runs at ~1.6x realtime single-threaded for 40 MSps input. Includes a synthetic DQPSK loopback test.
Replace the dead ac3_pipe code (which shelled out to the C++ ld-ac3-demodulate/ld-ac3-decode tools removed in the removetools-202601 cleanup) with the in-tree Python demodulator. With --AC3, demodulated QPSK symbols are written to <output>.ac3sym and the per-field symbol offset is recorded in the field metadata; decode-orc's ac3rf_sink stage consumes both to produce the final .ac3 file. Compared to the reverted PR happycube#1034 this needs no external module: the demodulator accepts any block length, so the StridedCollector buffering and the input alignment assert are gone, and the logger plumbing uses lddecode's own logger. Verified end-to-end on real captures: ld-decode --AC3 output decodes through decode-orc to clean AC3 (48 kHz 5.1, 384 kb/s) with near-zero Reed-Solomon corrections.
Store the number of symbols demodulated during each field as ac3Symbols / ac3_symbols, matching the efmTValues convention and the column decode-orc's metadata readers have queried since AC3 support was added there (the previous ac3_sym_offset column was never read by decode-orc, which fell back to distributing symbols uniformly across fields). The per-field counts sum exactly to the .ac3sym file size, so consumers can reconstruct each field's symbol range by summation, exactly as they do for EFM T-values.
Synthesizes an NTSC LD signal - a black raster FM modulated onto the video carrier, with the AC3 QPSK subcarrier mixed in underneath - runs ld-decode --AC3 over it, and checks the demodulated symbols against the transmitted ones and the per-field symbol counts recorded in the metadata. This covers the demodulator's integration into the decoder, which the loopback tests do not reach, without needing an AC3 capture to test against. The subcarrier is modulated against the AC3 spec's carrier and symbol rate rather than ac3rf.py's own constants, so the test would notice if those drifted.
Port of museld commit b82e92b: the proportional and integral gains are derived from a second-order loop design (natural frequency 1800 Hz, damping factor 0.6) divided by the combined phase-detector/VCO gain of 0.3, instead of the hard-coded 1/16 and 1/512 which ignored the sub-unity detector gain and made the realized loop ~2.5x slower than designed. Per the reference implementation's A/B testing, output is bit-identical on real captures while tracking through wow/flutter improves substantially. Re-verified 100.000% symbol agreement against the updated C++ implementation on the same three captures as before.
staffanu
force-pushed
the
ac3-python-port
branch
from
July 17, 2026 14:16
ef928ce to
bf2b6d5
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This is the resubmission of AC3 RF audio decoding (previously PR #1034, merged and then reverted in #1040). Following the discussion on #1034, the demodulator is now implemented in-tree in Python, following the same pattern as
lddecode/efm_pll.py— numpy/scipy for filter design, numba for the DPLL and FIR kernels. There is no external repository, no nix flake dependency, and no new Python dependency (numpy/scipy/numba are already required).With
--AC3(NTSC only), ld-decode demodulates the 2.88 MHz QPSK subcarrier and writes the raw symbols to<output>.ac3sym(one symbol per byte, values 0–3). The number of symbols demodulated during each field is recorded in the field metadata asac3Symbols/ac3_symbols, exactly analogous toefmTValues/efm_t_values. Framing, Reed-Solomon error correction and AC3 frame assembly happen downstream in decode-orc's AC3 RF Sink stage, which already queries this metadata column; a companion decode-orc PR restores that stage's TBC-source read path.What's in the PR
lddecode/ac3rf.py— the demodulator: half-band Kaiser decimation stages, 2.88 MHz mixer, root-raised-cosine matched filter, and a DPLL for symbol timing recovery (numba jitclass, likeEFM_PLL). Accepts arbitrary block lengths; all filter/timing state carries across calls.core.pyreplacing the deadac3_pipecode (which shelled out to C++ tools removed in the removetools-202601 cleanup).tests/test_ac3rf.py— self-contained tests, no capture files needed: DQPSK loopback (clean + noisy), and an end-to-end test that synthesizes a complete NTSC LD signal (FM video carrier + AC3 subcarrier), runsld-decode --AC3on it, and validates the demodulated symbols and the per-field metadata counts.--AC3behavior and the decode-orc handoff indocs/user-guide/command.md.Validation
.ac3symoutput decoded through the AC3 framing/RS stage produces a clean AC3 elementary stream (48 kHz, 5.1, 384 kb/s), with near-zero Reed-Solomon corrections (e.g. 3 corrected symbols in 11,952 C1 codewords), and audio verified by listening.nogilso it overlaps the decoder's other threads.Notes
.s16input; I'll rebase it out once that PR merges.