Before this:Decimation & resamplingDemodulation in codeClock & symbol recovery
DSP in GopherTrunk
Key takeaways
GopherTrunk’s decode path is every concept in this module, in order: capture
I/Q, downconvert (mix + filter + resample) to the channel rate, demodulate,
then recover symbols into bits. Because the DDC normalizes to a fixed channel
rate, the decoder is rate-invariant — it behaves the same whatever the capture
rate. Samples flow as Go complex64 over channels between concurrent stages.
This is where it all comes together. We’ll walk GopherTrunk’s real signal chain and label each stage with the lesson that explains it — so the pipeline stops being abstract and becomes something you can read in the source.
The chain, end to end
Stage by stage
1. Capture — I/Q in. The SDR front-end (the sdr package) reads samples off the
radio and pushes them downstream as a stream of complex64 — the
I/Q samples from Unit 1. Concurrent stages hand
this stream along over Go channels, exactly the pattern from the
Go concurrency lessons.
2. Downconvert — mix, filter, decimate. A digital downconverter centres the
target channel at zero with an NCO mix, runs a
FIR low-pass (a Kaiser-windowed design with a stopband more
than 60 dB down — the windowing from Unit 2), and
resamples to the per-protocol channel rate —
48 kHz for the 4800-baud C4FM family, 144 kHz for TETRA. GopherTrunk has two
DDC implementations: a single-channel Downconverter (used by the replay/tune path)
and a multi-tap wideband DDCBank (many channels from one live capture). They’re
separate code paths doing the same conceptual job — a distinction the project
documents deliberately, because a fix to one does not touch the other.
3. Demodulate. The channel, now at baseband and at the channel rate, is FM/C4FM demodulated — the phase-change discriminator that turns I/Q into a signal stepping between symbol levels.
4. Symbol & clock recovery. A matched filter and timing loop lock onto the 4800-baud symbol clock and read each symbol at its centre, with an AGC holding the level steady. Out come bits.
5. Framing and decode. The scanner/radio packages assemble bits into frames,
follow the control channel, and hand voice frames to the vocoder — the boundary where
DSP ends and the digital trunking module picks up.
The rate-invariance payoff
Notice what the downconverter guarantees: whatever the capture rate — 2.4 MS/s, 10 MS/s — everything after step 2 sees the same channel rate. The receiver, matched filter, and AGC are all sized from that channel rate, so the decoder is rate-invariant. This isn’t a detail; it’s a load-bearing design fact. It means a signal that decodes at one capture rate but not another points at the captured data (front-end overload, phase noise) rather than the steady-state DSP — a diagnosis the project has used to run down real field bugs.
Reading it in the source
With this map, the code is navigable. Following the
reading-real-Go habits: start where samples
enter, follow the complex64 channel from sdr into the downconverter, then into the
demodulator and symbol recovery. Each package is one box in the diagram above.
Quick check: why does GopherTrunk's decoder behave the same regardless of capture rate?
Recap
- GopherTrunk’s chain is this module in order: capture → downconvert → demodulate → recover symbols → frame.
- The DDC (mix + FIR + resample) normalizes to a fixed channel rate (48 kHz C4FM, 144 kHz TETRA).
- That makes the decoder rate-invariant — a powerful diagnostic property.
- Two DDCs exist — single-channel and wideband — as separate code paths; samples
flow as
complex64over Go channels.
Next up: the last lesson — how numbers are stored, and why it matters for performance.
Frequently asked questions
Does GopherTrunk use fixed-point or floating-point DSP?
GopherTrunk is pure Go and carries I/Q samples as complex64 — a pair of 32-bit floats — throughout its pipeline. Floating point keeps the DSP code simple and accurate, and modern CPUs run 32-bit float math fast enough for the channel rates involved. The next lesson explains the fixed-versus-float tradeoff in general.
Why does GopherTrunk have two different downconverters?
The single-channel Downconverter is used by the replay/tune path to decode one channel from a capture, while the wideband DDCBank extracts many channels at once from a live stream. They are separate code paths that implement the same conceptual mix-filter-decimate DDC, so a fix to one does not automatically apply to the other — a distinction the project documents carefully.