Part 7 of TETRA End to End, a 14-part deep dive into how GopherTrunk turns one real 25 kHz TETRA carrier into clear recorded voice. Part 6 closed with a promise: the pure-Go ACELP decoder is bit-exact against the ETSI reference, and that claim has proof. This part is the proof — and the method behind it, because the method is the real deliverable. This series’ villain is the test that validates its own bugs; the antidote is an external reference, and TETRA voice has two independent ones: the ETSI reference C codec, and the air itself. Between them sits a build trap that makes the reference tools produce garbage on every modern 64-bit machine, and a diagnostic rule this repo now treats as standing guidance.
TL;DR: Two independent conformance passes, both reproducible. Pass one (
internal/voice/acelp/etsi_reference_test.go,TestETSIReferenceConformance): feed the same 137-bit-per-frame bitstream to the ETSIsdecoderand to GopherTrunk’s decoder and demand zero mismatched samples — the decoder is fixed-point, so a faithful port matches exactly. Pass two (cmd/gophertrunk/tetra_multislot_replay_test.go,TestTETRAMultiSlotReplay): replay a real cs16 IQ capture through DDC → receiver → extractor → TCH/S → ACELP and correlate per-slot audio against the control channel’s grant timeslots. Both are skip-guarded behind env vars because the ETSI vectors are copyrighted and captures are large. The trap: the reference sources assume a 32-bitWord32(typedef long) — on an LP64 host every saturating op silently returns garbage, so build the tools with a 32-bitWord32(or-m32) before trusting a byte they emit. The rule the passes enforce: validate the whole chain against the reference, not just parts — when voice doesn’t decode but the vocoder’s unit tests pass, suspect the channel coding.
Key takeaways
- Bit-exactness is a binary verdict, and that’s its value. Fixed-point
codecs permit
mismatches == 0as the pass condition — no perceptual scoring, no tolerance tuning, no argument. One wrong saturating op fails loudly at a specific frame. - Two passes catch different lies. The bitstream pass pins the vocoder in isolation; the IQ replay pins everything upstream of it. Part 3’s CRC bug passed the first kind of scrutiny and only the second kind caught it.
- The reference itself can be built wrong. ETSI’s fixed-point basic ops predate LP64; an unmodified 64-bit build produces confidently wrong vectors. Verify your reference before you verify against your reference.
- Skip-guarded is not second-class. Harnesses gated on
GT_ETSI_SERIAL/GT_TETRA_IQkeep copyrighted vectors and bulky captures out of the repo while keeping the procedure executable by anyone — the reproducibility lives in the test file, not the test data.
Cheat sheet
| Concern | What it does | Where it lives |
|---|---|---|
| Bitstream conformance | same serial file → GT vs ETSI PCM, 0 mismatches | internal/voice/acelp/etsi_reference_test.go (TestETSIReferenceConformance) |
| Serial format | int16 LE, 138 words/frame: BFI + 137 bits | etsi_reference_test.go (doc comment) |
| Level matching | reference Post_Process saturating ×2 |
etsi_reference_test.go (postProcessX2) |
| Whole-chain replay | real cs16 IQ → per-slot audio vs grants | cmd/gophertrunk/tetra_multislot_replay_test.go (TestTETRAMultiSlotReplay) |
| Replay knobs | GT_TETRA_IQ, GT_TETRA_IQ_RATE, GT_TETRA_COLOUR, GT_TETRA_OUT |
same file |
| Reference build trap | Word32 must be 32-bit; LP64 long breaks saturation |
etsi_reference_test.go (build note) |
In this post
- Why conformance needed two passes — what each can and cannot see.
- Pass one: the shared bitstream — serial format, the ×2, and zero as the bar.
- The Word32 trap — when the reference implementation isn’t the reference.
- Pass two: the air — replaying a capture through everything at once.
- The diagnostic rule — chain validation as standing guidance.
Why conformance needed two passes
A voice path is a chain: demod → burst extraction → descramble → channel decode → vocoder. Part 3 told the story of a bug that lived in the middle — the class-2 CRC — while the pieces on either side of it were provably fine. That episode fixed more than a CRC; it fixed the testing model. A unit test per stage, each validated against its own encoder, proves only that each stage agrees with itself. What’s needed is external anchoring at two scopes: the most complex single component (the vocoder) pinned against an independent implementation, and the chain as a whole pinned against the only artifact that exercises every stage with real-world conventions at once — an off-air capture. Neither pass substitutes for the other. The bitstream pass would never notice a scrambler seed bug (it enters below the scrambler); the replay pass can’t tell you which stage broke, only that one did. Together they triangulate.
Pass one: the shared bitstream
The ETSI distribution ships a fixed-point encoder and decoder. The harness runs them once to produce two files, then holds GopherTrunk to their output:
scoder in.pcm serial.bin synth_local.pcm # ETSI reference encoder
sdecoder serial.bin ref_out.pcm # ETSI reference decoder
serial.bin is the shared bitstream — int16 little-endian, 138 words per
frame: word 0 is the bad-frame indicator, words 1..137 are the coded speech
bits, exactly the format the reference’s Bits2prm_Tetra reads and
GopherTrunk’s Decoder.Decode consumes. ref_out.pcm is the reference
decoder’s 240-samples-per-frame output. The test walks both in lockstep:
// internal/voice/acelp/etsi_reference_test.go (shape)
dec := NewDecoder()
for f := 0; f < nFrames; f++ {
bfi := serial[base] != 0
/* … unpack 137 bits … */
out := dec.Decode(bits, bfi)
for i, s := range out {
got := postProcessX2(s) // reference Post_Process: saturating ×2
if got != ref[f*pcmPerFrame+i] {
mismatches++ /* … track maxAbs, firstBadFrame … */
}
}
}
// pass condition: mismatches == 0
The bar is zero. Not “SNR above X,” not “perceptually transparent” —
identical int16s, every sample of every frame, BFI frames included. That bar
is only reachable because the codec is defined in saturating fixed-point
arithmetic (Part 6’s point): determinism is the spec. And it’s why the pass
condition doubles as a diagnostic — firstBadFrame and maxAbsDiff in the
failure log localise a divergence to the frame where some ported operator
first disagreed, which during development repeatedly turned “the decoder is
wrong somewhere” into “look at this one subframe’s gain path.” Note the test
applies the postProcessX2 itself: GopherTrunk’s raw Decoder omits the
reference’s output doubling (the registry adapter applies it in production),
so the harness levels the two before comparing.
The vectors are not committed — ETSI’s sources and outputs are copyrighted —
so the test skips unless GT_ETSI_SERIAL and GT_ETSI_REF point at files you
built yourself. The procedure is the artifact under version control.
The Word32 trap
Which brings us to the trap, preserved as a build note in the test’s doc
comment because it burned real hours: the ETSI basic operators assume
Word32 is 32 bits, via typedef long — true under ILP32, false on every
LP64 Linux/macOS host, where long is 64 bits. The consequence is not a
crash. Saturation tests like “does this exceed the 32-bit range” simply never
fire, so every saturating operation returns unsaturated garbage and the
reference tools emit confidently wrong PCM. Diff GopherTrunk against that
and you’d conclude the Go port is broken — or worse, “fix” the Go port into
agreement with a miscompiled reference, manufacturing a self-consistent pair
of wrong decoders. The villain, one meta-level up. The fix is mechanical
(edit the typedef to int, or build -m32); the lesson is not: a
reference implementation is only a reference once you’ve verified the build
assumptions it was written under. Trust, then verify — in that order,
applied to the yardstick itself.
Pass two: the air
The second pass is Part 5’s replay harness wearing its conformance hat.
TestTETRAMultiSlotReplay takes a real cs16 capture of a TETRA voice carrier
(GT_TETRA_IQ + GT_TETRA_IQ_RATE), pushes it through the production
chain — the same ccdecoder.NewDownconverter, the same receiver options, the
same TrafficExtractor and TCHSpeechFrames and ACELP decode the daemon
runs — and emits per-slot WAVs plus activity timelines. The conformance
content is the correlation step: decoded speech must cluster on the timeslots
the control channel granted, at the times it granted them, with CRC yields
well off the 1/256 chance floor. A capture is the one test vector nobody on
the implementation side could have accidentally shaped, which is why this
pass caught what the unit tests could not: the class-2 CRC bug (all slots at
the chance floor), and later the slot-anchor shift (activity on the wrong
timeslots). It has since become the standing A/B instrument — the
soft-decision work of
Part 8
and the equalizer work after it are all scored as CRC-yield deltas on this
same harness, against the same captures.
The diagnostic rule
The two passes compose into a decision procedure that is now standing guidance in the repo, verbatim: when “voice doesn’t decode” but the vocoder unit tests pass, suspect the channel coding (CRC / interleave / reorder), not the vocoder — validate the whole chain against the reference, not just parts. The reasoning generalises. Pass one is cheap, deterministic, and absolute — once it holds, the vocoder is effectively eliminated as a suspect for any field symptom. So a field symptom with a green pass one localises itself to the chain between demod and vocoder. That inversion — conformance tests as suspect-elimination for future debugging, not just release gating — is what the harnesses actually bought. Every subsequent TETRA investigation in this series (soft decision, equalizers, DMO’s false “encryption”) started from “the vocoder is proven; where in the chain is the loss?” and was faster for it.
How that principle shaped the Go code
- Formats are documented at the test site. The 138-word serial layout and the Word32 build note live in the test’s doc comment — the next person to regenerate vectors gets the traps handed to them with the procedure.
- The comparison is production-honest. The harness applies the same
Post_Process×2 the registry adapter applies, so pass one certifies the audio path as shipped, not a lab variant. - One harness, many questions. The replay test grew counters
(
trafficMarkedCRC, soft-path yields, per-marker clustering) instead of spawning sibling tests — every voice-path change since is an A/B on the same instrument, so numbers stay comparable across months.
Where this goes next
The chain is proven end to end — on a clean capture. The next problem is
the marginal one: a same-carrier call where the hard-decision path threw away
~70% of the bursts that were actually recoverable.
Part 8
follows the soft path — the receiver’s SoftSink differentials through soft
depuncture and Viterbi to DecodeTCHSSoft — and the ~2 dB that turned
short, garbled recordings into complete ones.
FAQ
Why aren’t the conformance vectors committed to the repo? Because the ETSI reference sources and their outputs are ETSI-copyrighted. The harness is committed; the vectors are regenerated locally from the ETSI distribution by anyone who wants to run it. Reproducibility of the procedure is the goal — the test file documents the exact commands.
How would I know if I hit the Word32 trap?
The reference tools build clean and run without error — the tell is
downstream: synth_local.pcm sounds wrong (harsh, clipped-then-not), and
GopherTrunk “fails” conformance with enormous maxAbsDiff from frame 0.
If a supposedly bit-exact port diverges immediately and hugely, audit the
reference build before the port.
Does pass one cover the encoder too?
No — GopherTrunk only decodes (it’s a receiver), so only sdecoder’s side is
ported and pinned. The ETSI scoder is used solely to manufacture realistic
serial files from PCM for the comparison.
What counts as passing pass two? No single number — it’s a correlation judgment made against the capture’s own control channel: CRC-valid speech clustered on granted timeslots at granted times, yields far above the 1/256 floor, and per-usage-marker activity mapping one-to-one onto physical slots. The harness prints all of it; the operator’s capture is the ground truth.
Is bit-exactness overkill when the output is lossy codec speech anyway?
It’s the opposite of overkill — it’s the cheapest strong claim available.
Perceptual similarity requires listeners and thresholds; sample equality
requires ==. And only the exact form has the elimination power the
diagnostic rule depends on: “close” leaves the vocoder a suspect forever.
Series navigation
Part 7 of 14 · ← Part 6: A Clean-Room ACELP Vocoder in Pure Go · Next → Part 8: Going Soft — Soft-Decision TCH/S