Part 12 of Voice Coding, the finale. Eleven posts built a pure-Go vocoder stack from bits to conditioned audio. The obvious question about a from-scratch codec is: how do you know it’s *right? This post is the answer — the tooling that measures GopherTrunk’s output against decoders the world already trusts, and the series wrap-up.*
TL;DR: GopherTrunk validates its pure-Go vocoders three ways. Golden regression: deterministic seeding makes decode byte-reproducible, so a fixed frame stream must always produce the same PCM. Calibration: the
voice-calibrateharness decodes the same raw frames through GopherTrunk and reads a reference WAV from DSD-FME / OP25, then reports a loudness offset (RMSRatioDb) and a waveform-similarity score (PeakXcorr). Field triage:VoiceStatssummarises a call’s decode health without per-frame log spam. Pass =|RMSRatioDb| < 3 dBandPeakXcorr > 0.85.
Key takeaways
- Vocoders seed their noise source from a fixed default (
NewWithSeed(0)), so two decoders produce byte-identical output for the same frames — the foundation of golden-frame regression. calibrate.Comparequantifies agreement with a reference decoder as two numbers: RMS ratio (loudness) and peak cross-correlation (shape), aligned over a ±25 ms lag window.DecodeStreamturns any raw frame file into a WAV through the same registry the daemon uses — the offlinegophertrunk decodepath and the calibration fixture generator share one code path.VoiceStatsreads crest factor and clip percentage to catch an over-hot AGC from a single per-call line instead of a log dump.
Cheat sheet
| Tool | Entry point | Answers |
|---|---|---|
| Calibration | calibrate.Compare(raw, refWav, name) |
“does it match the reference?” |
| Similarity math | calibrate.CompareSamples(a, b) |
testable w/ synthetic PCM |
| Streaming decode | voice.DecodeStream(in, name, out) |
“decode this frame file to WAV” |
| CLI | voice-calibrate -raw … -ref-wav … -vocoder … |
one-off PASS/FAIL |
| Per-call health | voice.VoiceStats |
crest factor, clip %, b₀ range |
In this post
- Golden-frame regression — determinism as a test tool.
- The calibration harness — RMS ratio and cross-correlation.
- The CLI —
voice-calibrate, thresholds, and streaming decode. - Field triage — what
VoiceStatscatches that FEC counters can’t. - Series wrap-up.
Golden-frame regression
The first line of defense is determinism. An MBE vocoder needs a noise source for the §6.4 unvoiced excitation and §6.3 phase dispersion — but “random” would make output untestable. So the constructors seed from a fixed default:
// internal/voice/ambe2/decoder.go (shape)
func New() *Decoder { return NewWithSeed(0) } // fixed seed → reproducible output
Two decoders built with the same seed produce byte-identical PCM for the same frame stream. That turns “does the decoder still work?” into a checkable assertion: run a fixed frame stream through the decoder, compare the output to a committed golden WAV, fail on any diff. It’s the vocoder equivalent of the golden test vectors the rest of the codebase leans on — a change that alters decode by one sample is caught immediately, not in the field.
The calibration harness
Golden regression proves the decoder is stable. It doesn’t prove it’s correct — a byte-reproducible decoder can be reproducibly wrong. For correctness, GopherTrunk compares its output to decoders that are already trusted: DSD-FME and OP25. Both are fed the same raw vocoder frames, and the harness quantifies how close the two outputs are:
// internal/voice/calibrate/calibrate.go (shape)
func Compare(rawPath, refWavPath, vocoderName string) (Result, error) {
v, _ := voice.DefaultRegistry.New(vocoderName)
var inTree []int16
for i := 0; i < len(rawBytes); i += v.FrameSize() {
s, _ := v.Decode(rawBytes[i : i+v.FrameSize()])
inTree = append(inTree, s...) // GopherTrunk's decode
}
refSamples, _, _ := readWav(refWavPath) // the reference decoder's decode
return CompareSamples(inTree, refSamples), nil
}
CompareSamples returns two numbers that answer two different questions:
RMSRatioDb— loudness offset in dB, positive when GopherTrunk is louder than the reference. It isolates level, which is exactly the knob the MBE AGC’sTargetPeaktunes. A 4 dB offset means “turn the target down 4 dB,” not “the decode is broken.”PeakXcorr— normalised cross-correlation magnitude in[0, 1], the shape agreement independent of level. Above 0.85 the two waveforms are recognisably the same speech; below 0.5 they’re very likely not even the same source.
Because two decoders have different pipeline group delays, the harness searches
for the best alignment over ±MaxLagSamples (200 samples ≈ ±25 ms at 8 kHz):
// internal/voice/calibrate/calibrate.go (shape)
for lag := -maxLag; lag <= maxLag; lag++ {
// dot product of x against y shifted by lag, normalised by energy
v := math.Abs(dot / denom)
if v > bestVal { bestVal, bestLag = v, lag }
}
Taking the absolute correlation is deliberate: a 180°-phase-inverted decoder
still scores |xcorr| ≈ 1, and the sign mismatch is caught separately by
RMSRatioDb, so a polarity flip can’t sneak through as a “match.”
The CLI and streaming decode
voice-calibrate is the operator-facing wrapper — no test-writing required:
voice-calibrate -raw call.raw -ref-wav dsdfme.wav -vocoder ambe2-dmr
It imports the imbe and ambe2 packages so their factories register, then prints
the RMSRatioDb, PeakXcorr, and lag, and applies the pass/fail thresholds
(|RMSRatioDb| < 3 dB and PeakXcorr > 0.85), exiting non-zero under -strict
so it slots into CI. Producing the in-tree side of that comparison — turning a
raw frame file into a WAV — is DecodeStream, the same path the offline
gophertrunk decode tool uses:
// internal/voice/streamdecode.go (shape)
func DecodeStream(in io.Reader, vocoderName string, out io.WriteSeeker) (int, error)
// reads FrameSize()-aligned frames, decodes via the registry, writes a WAV;
// ErrPartialFrame if the input ends mid-frame (the WAV is still closed cleanly)
The WithVocoder variant lets a caller pass a decoder built with an explicit
seed or AGC config, so a calibration run can pin reproducibility exactly the way
the golden tests do. One decode path serves the daemon, the offline tool, and
the test harness — there’s no separate “test decoder” to drift out of sync.
Field triage: VoiceStats
Calibration needs a reference WAV. In the field you don’t have one — you have a
tester saying “it sounds robotic.” VoiceStats is the compact per-call summary
that turns that into a number without per-frame logging:
// internal/voice/stats.go (shape)
type VoiceStats struct {
Frames, Voiced, Unvoiced, Silent, Bad, Repeated int
MeanF0Hz, MeanL, MeanVoicedFrac float64
MeanAGCGain, MaxPreClipPeak, OutputRMS, CrestFactor float64
ClipSamples, TotalSamples int
MinB0, MaxB0 int
}
func (s VoiceStats) ClipPct() float64 { /* limited samples / total */ }
The single most useful field is CrestFactor (peak/RMS): natural speech sits
around 8–15, and a value near 3–4 with ClipPct > 0 is the unmistakable
signature of an over-hot AGC slamming frames into the rail — exactly the bug
Part 10
described, now diagnosable from one line. The MinB0/MaxB0 range disambiguates
a genuine low-modulation dead-key from empty frames reaching the vocoder (a
mistuned tap pins b₀ at 0). It’s exposed through the StatProvider interface, so
a vocoder that doesn’t track stats simply produces no summary — the recorder and
gophertrunk decode type-assert for it.
Series wrap-up
Twelve parts, one arc: bits to sound to trust.
- Parts 1–3 framed the vocoder problem and the Multi-Band Excitation model, then built the shared synthesis core.
- Parts 4–6 decoded IMBE for P25 Phase 1 — the FEC, deinterleave, and acquisition that turn a symbol stream into clean frames.
- Parts 7–8 crossed to AMBE+2, showing how one Go decoder serves three protocols with two rate tables, and where its error handling really lives.
- Parts 9–11 wired it into the system: the composer’s per-protocol chains, the post-synthesis conditioning and loudness stack, and the recording / encoding / hardware output stage.
- Part 12 closed the loop, measuring pure-Go output against the decoders the community already trusts.
The through-line is a single principle stated a dozen ways: be faithful, and be honest about the limits. The default decode path is byte-reproducible and byte-faithful; every “sound-good” and every hardware escape hatch is opt-in and labelled; and where the public spec goes dark — the knox tones — GopherTrunk ships the mechanism and refuses to guess the numbers. A vocoder you can’t verify is a vocoder you can’t trust, so the last thing the series built was the ruler.
Voice frames arrive from the Protocol Decoders series; calls come from the Trunking Engine series. And once the vocoder has produced PCM, the Recording, Composition & Streaming series picks up the output half — turning that audio into crash-safe files, a searchable call log, live streams, and uploads to the public call aggregators.
FAQ
How do you know the pure-Go decode is correct, not just consistent?
Consistency comes from deterministic seeding (golden regression). Correctness
comes from voice-calibrate: decoding the same raw frames through GopherTrunk
and through a reference decoder (DSD-FME / OP25), then scoring loudness
(RMSRatioDb) and waveform shape (PeakXcorr) separately.
What are the pass thresholds?
|RMSRatioDb| < 3 dB and PeakXcorr > 0.85. Above 0.85 cross-correlation the
two decoders are recognisably decoding the same speech; the 3 dB loudness
tolerance is tuned out via the AGC’s TargetPeak.
What does a bad crest factor tell me?
Natural speech has a crest factor around 8–15. A value near 3–4 with a non-zero
clip percentage means the AGC is over-driving the output into the limiter — the
“robotic” signature — diagnosable from the per-call VoiceStats line without
enabling per-frame logs.
Can I decode a raw frame file without the daemon?
Yes. voice.DecodeStream (and the gophertrunk decode tool built on it) reads a
raw vocoder frame file, decodes it through the named vocoder from the registry,
and writes a WAV — the same code path the daemon and the calibration harness use.
Series navigation
Part 12 of 12 · ← Part 11: Recording & Encoding