Also known as: Post_Process, output scaling stage, x2 post-processing
ACELP Post_Process is the final output stage of the
TETRA ACELP decoder specified by ETSI EN 300 395-2: a
saturating multiply-by-two applied to every decoded PCM
sample. It carries no spectral or excitation information — it exists solely to put the decoder’s
output at the reference level. GopherTrunk applies it in the vocoder wrapper’s Decode method,
and without it the rendered audio sits a full 6 dB below the ETSI reference decoder.1
What the stage does
The reference codec’s internal synthesis produces PCM at half the intended playback level, so the
last thing Decod_Tetra does is double it. GopherTrunk’s vocoder.Decode mirrors this exactly:
after the raw synthesis returns 240 int16 samples (30 ms at 8 kHz), it runs each sample through
addOp(sfin, sfin) — adding the sample to itself, which is a multiply-by-two using the
G.191 saturating add. The result is one call’s worth of
PCM at the reference level, ready for the downstream DC block,
AGC, and playback.
Six decibels is a factor of two in amplitude, which is exactly what a ×2 buys — so an implementation that renders otherwise-correct TETRA audio but sounds conspicuously quiet has almost always dropped this stage. It is a small, easy-to-miss line that is nonetheless part of the bit-exact contract with the reference decoder.
Why saturation, not a plain multiply
Doubling a value that is already near the top of the int16 range would overflow. In ordinary two’s-complement arithmetic that overflow wraps: a large positive sample flips to a large negative one, producing a loud click on every loud sample — the worst possible artifact on exactly the samples a listener notices most. Saturation arithmetic avoids this by clamping instead of wrapping: any product that exceeds +32767 pins to +32767, and any below −32768 pins to −32768. The clamp introduces a little harmonic distortion on peaks, but that is inaudible next to the full-scale sign inversion wrapping would cause. This is the same fixed-point discipline the entire ACELP decoder is built on — every intermediate operation saturates rather than wraps, so the whole signal path stays bounded and matches the reference bit for bit.
Using addOp rather than a bare int16(sfin * 2) is deliberate: addOp is the reference’s
add operator, which sets the saturation clamp and the global overflow flag exactly as the C
codec does. Substituting a native multiply would silently reintroduce the wrap-around the standard
was written to prevent.
Where it sits in the chain
Post_Process is the boundary between the ACELP codec proper and GopherTrunk’s audio output path. Upstream, the TCH/S speech-frame decode and the ACELP synthesis produce the raw PCM; Post_Process lifts it to reference level; downstream, generic audio conditioning takes over. Placing the ×2 at the codec’s own output — rather than folding it into a later gain stage — keeps GopherTrunk’s decode faithful to the reference at the exact point the standard specifies, so the codec’s output can be compared sample-for-sample against ETSI’s before any GopherTrunk-specific processing muddies the comparison.
Relevance to SDR
For a scanner the practical payoff is consistent, correct loudness: TETRA calls decode at the same level a reference-conformant radio would produce, so they sit sensibly alongside other protocols in a multi-system scanner without a per-protocol volume fudge. Because the stage is a fixed part of the reference algorithm, GopherTrunk verifies it as part of the ACELP decoder’s end-to-end conformance rather than tuning it by ear.
Sources
-
Saturation arithmetic — Wikipedia, on clamping versus wrap-around overflow in fixed-point audio arithmetic. ↩