Part 12 of SDR Internals. Digital voice isn’t recorded audio — it’s a model of your voice, a few kilobits per second. This post covers the vocoders that reconstruct speech and the registry that makes them pluggable.
In this post
- What a vocoder is and why digital radio can’t work without one.
- GopherTrunk’s pure-Go IMBE (P25 Phase 1) and AMBE+2 (P25 Phase 2, DMR) decoders.
- The shared MBE synthesis core and the
Vocoderregistry — a plugin + DRY design.
What a vocoder does
A vocoder compresses speech to a few kilobits per second by modeling how speech is produced — pitch, voicing, and a spectral envelope — rather than recording the waveform. The radio sends those model parameters; the receiver resynthesizes speech from them. Every digital voice mode depends on one. (digital voice)
GopherTrunk implements two in pure Go:
- IMBE — 4.4 kbps, P25 Phase 1. (reference)
- AMBE+2 — 2.4 kbps, P25 Phase 2, DMR, NXDN. (reference)
Both belong to the Multi-Band Excitation family — and that shared lineage is the key to the design.
How GopherTrunk implements it in Go
IMBE and AMBE+2 differ in how they unpack and error-correct their on-air bits,
but they synthesize speech almost identically. So GopherTrunk factors the
synthesis into a shared internal/voice/mbe core, and the two codecs become thin
front-ends:
internal/voice/imbe— unpacks 144-bit IMBE frames, runs Golay/Hamming FEC, de-interleaves, and produces MBE parameters.internal/voice/ambe2— unpacks AMBE+2 frames, runs BPTC FEC and codebook lookups, and produces MBE parameters.internal/voice/mbe— takes those parameters and synthesizes 8 kHz PCM: voiced-harmonic generation, unvoiced FFT excitation with overlap-add, spectral enhancement, and per-frame AGC.
Every vocoder satisfies one interface:
// internal/voice/vocoder.go (shape)
type Vocoder interface {
Name() string
FrameSize() int
Decode(frame []byte) ([]int16, error)
Reset()
Close() error
}
…and registers itself with a factory registry, exactly like the SDR drivers from Part 2:
var DefaultRegistry = NewRegistry() // name -> VocoderFactory
// imbe/register.go and ambe2/register.go call Register() at init()
The design principle: plugin registry + shared-core DRY
Two principles combine. The plugin registry lets the engine pick a vocoder by
name without importing it. And DRY — don’t repeat yourself — drives the shared
mbe core, so the genuinely-common synthesis math is written once.
How that principle shaped the Go code
- One synthesis engine, many front-ends. Because IMBE and AMBE+2 both reduce to MBE parameters, the hard DSP — harmonic synthesis, overlap-add, enhancement — lives in a single tested package. A future Codec2 or hardware vocoder reuses it.
- Per-call instances, no shared state. Each active call gets its own
Vocoderinstance with its ownReset()-able state (cross-frame prediction needs memory), so two simultaneous calls never interfere. - Backends swap by name. The protocol decoder asks the registry for “imbe” or “ambe2”; the planned DVSI hardware backend slots in behind the same interface under a build tag, with zero changes to the decoders.
- A
NullVocoderalways exists. Registering a silence vocoder means the pipeline always has a valid backend, so an unsupported codec degrades to silence instead of crashing.
Where this goes next
Vocoders are a deep, fascinating topic — the source-filter model, how MBE splits the spectrum into voiced and unvoiced bands, and the patent history that makes a clean-room Go implementation valuable. A future “Digital Voice in Go” series can dissect a single frame bit by bit. Next, we wire these audio samples into recordings and live streams.
FAQ
Why are IMBE and AMBE+2 implemented separately if they share a core?
Their bit-packing and FEC differ completely — only the synthesis is shared. The
split front-ends handle the on-air differences; the common mbe package handles
what’s genuinely identical.
Is implementing AMBE in Go a patent issue? Re-implementing a codec in a new language doesn’t change its patent status. GopherTrunk provides the decoders; whether a given codec is encumbered depends on your jurisdiction. IMBE’s patents have expired; AMBE+2’s situation varies.
Why 8 kHz mono PCM output? That’s the native rate vocoders model speech at — 160 samples per 20 ms frame. It matches telephone-quality voice and keeps recordings small.
Series navigation
Part 12 of 14 · ← Part 11 · Next → Part 13: Recording, composition & streaming