AMBE parameter codec — the DV-family voice frames ↔︎ MBE vocoder parameters
The voice layer shared by DMR, NXDN, YSF and D-STAR made fully native: the half-rate AMBE+2 (49-bit) and full-rate AMBE (48-bit) parameter frames decoded to the (f₀, per-band voiced/unvoiced, harmonic amplitudes) triple the native MBE vocoder synthesises — and encoded back — with the Golay+PN 72-bit channel frame that carries them over the air modelled around it. The path from a DMR/NXDN/YSF/D-STAR voice burst to PCM contains no DVSI hardware and no mbelib.
Rafe project · app/radio/ambe.py + radio/mbe.py, radio/imbe.py · in-repo NumPy · frame sizes, field order and the Golay/PN channel-frame structure spec-accurate to the published AMBE layouts; the amplitude/pitch quantiser tables structurally AMBE, self-consistent — DVSI's exact VQ tables, PN seed constant and inter-frame prediction remain for bit-exact on-air audio
Abstract
AMBE (Advanced Multi-Band Excitation) and its successor AMBE+2 are DVSI's proprietary refinements of the MBE model: the same \((f_0,\ \{v_\ell\},\ \{A_\ell\})\) representation P25's IMBE uses, packed into smaller frames by better quantisation. Four modes in this catalogue transmit AMBE voice — DMR, NXDN and YSF DN carry the half-rate AMBE+2 frame; D-STAR carries the older full-rate AMBE frame — and every one of them delegates the actual voice synthesis to the same vocoder. This module is that shared layer: it maps each mode's parameter frame onto the MBE vocoder exactly the way radio/imbe.py maps P25's 88-bit IMBE frame, and wraps it in the Golay(24,12)/Golay(23,12)+PN channel-frame structure the modes broadcast.
Two things are transcribed from the published layouts (frame sizes, the field order, the Golay/PN channel frame); one thing — the quantiser tables — is structurally the AMBE scheme and round-trips self-consistently but is not DVSI's exact VQ. §9 states the split precisely so nobody mistakes which numbers are protocol facts and which are the honest Rafe stand-in.
1. Motivation & scope
The open-source route to AMBE speech has always been DVSI silicon (the AMBE-2000 / AMBE-3000 vocoder chips) or, illicitly, patched builds of mbelib. Rafe wants none of that in the voice path. The observation that makes this practical is the same one that made P25 native (see IMBE): AMBE is MBE with a better bit codec. Once the MBE vocoder exists as the model layer — parameters in, speech out — each DV mode needs only:
- its framing (bursts, sync, slot structure) — the mode's own spec; and
- a parameter mapping: how that mode's compact voice frame packs \((f_0, \{v_\ell\}, \{A_\ell\})\), and the FEC around it.
app/radio/ambe.py is step 2 for the whole AMBE family. It provides two frame formats that between them cover four modes:
| Frame | Bits | FEC channel frame | Used by |
|---|---|---|---|
| half-rate AMBE+2 | HR_BITS = 49 |
Golay24 + PN·Golay23 + 25 raw = 72 | DMR, NXDN EHR, YSF DN |
| full-rate AMBE | FR_BITS = 48 |
Golay24 + PN·Golay24 + 24 raw = 72 | D-STAR |
Both are the 20 ms voice frame of their modes (160 PCM samples at FS = 8000 Hz — see mbe.py). The synthesis is fully native: no DVSI, no mbelib.
2. Background — parameter frame vs channel frame
Two "frames" appear throughout and must not be confused:
- The parameter frame (49 or 48 bits) is the vocoder payload: the quantised pitch, voicing and spectral amplitudes. This is what
encode_frame_hr/decode_frame_hr(and the_frpair) produce and consume, and whatsynthesize_hr/synthesize_frturn into PCM. - The channel frame (
CHANNEL_BITS = 72) is what actually goes on the air: the parameter frame with forward-error-correction and scrambling wrapped around its most-sensitive bits.hr_to_channel/channel_to_hr(and_fr) are the boundary.
AMBE differs from IMBE only in the quantiser — the model underneath (MBE) is identical, one voiced/unvoiced decision per harmonic band rather than one per frame. The half-rate vs full-rate distinction is a bit budget: full-rate AMBE (D-STAR, ~2400 bps voice + 1200 bps FEC) predates and is looser than half-rate AMBE+2 (DMR/NXDN/YSF, ~2450 bps voice with stronger FEC and inter-frame prediction). Here both use the same parameter-field layout (pitch | voicing | gain + amplitudes); only the total bit count and the C1 Golay length differ.
3. The parameter frames — exact bit layout
Both frames share the field order — pitch (7) | band voicing (5) | gain (6) + spectral amplitudes (rest) — built by _encode_params / read back by _decode_params (ambe.py:83–98). The only difference is the total width (HR_BITS = 49 vs FR_BITS = 48, ambe.py:34), which changes the amplitude bit budget.
Half-rate AMBE+2 — 49 bits
bit 0 .. 6 b0 7-bit log-pitch index → (f0, L) _PITCH_BITS
bit 7 .. 11 V/UV 5 band-voicing bits (_VUV_BANDS = 5)
bit 12 .. 17 gain 6-bit mean log2 amplitude (imbe._GAIN_BITS)
bit 18 .. 48 amps per-harmonic differential-log residuals
L · rb bits, rb = max(1, (37-6)//L), zero-padded to fit
amplitude budget = 49 - 7 - 5 = 37 bits (gain + residuals)
Full-rate AMBE — 48 bits
bit 0 .. 6 b0 7-bit log-pitch index → (f0, L)
bit 7 .. 11 V/UV 5 band-voicing bits
bit 12 .. 17 gain 6-bit mean log2 amplitude
bit 18 .. 47 amps per-harmonic differential-log residuals
L · rb bits, rb = max(1, (36-6)//L), zero-padded to fit
amplitude budget = 48 - 7 - 5 = 36 bits (gain + residuals)
_encode_params(f0, voiced, amps, nbits) (ambe.py:83):
b0 = f0_to_b0(f0);_, L = b0_to_f0_L(b0)— L is fixed by b0 alone (§4), so the voicing and amplitude field sizes are derived from the pitch field and never signalled separately.amps/voicedare_fitto lengthL(ambe.py:78) — truncated, or padded by repeating the last value (amplitude fill1e-3, voicing fillFalse).- bits =
_tobits(b0, 7)+_encode_vuv(voiced, L)(5 bits) +encode_amps(amps, nbits − 7 − 5). - The result is zero-padded and truncated to exactly
nbits((bits + [0]*nbits)[:nbits]).
_decode_params(bits, nbits) (ambe.py:93) inverts it: f0, L from the first 7 bits, voiced from the next 5 (_decode_vuv), and the remainder handed to imbe.decode_amps(..., L, nbits − 12).
Amplitude quantiser (imported from imbe.py). encode_amps / decode_amps are shared with the P25 codec verbatim (ambe.py:25): a 6-bit gain = mean log₂ amplitude quantised over [_GAIN_LO, _GAIN_HI] = [−10, +6] (64 levels), then per harmonic a residual (log₂ amplitude minus gain) quantised over ±_RES_RANGE = ±3 with rb = max(1, (nbits − 6)//L) bits. The budget self-allocates by harmonic count: few harmonics → fine per-harmonic resolution, many → coarse. See imbe.md §3 for the quantiser detail; this module does not re-implement it.
4. Pitch, harmonic count and band voicing
Fundamental frequency ↔︎ 7-bit log index
Unlike IMBE's ω₀ = 4π/(b₀+39.5) relation, AMBE here uses a logarithmic pitch index over a fixed frequency window [_F0_LO, _F0_HI] = [60, 400] Hz (ambe.py:31), with _LOG_SPAN = ln(400/60) ≈ 1.897 (ambe.py:32):
f0_to_b0(f0) (ambe.py:49):
f0 = clip(f0, 60, 400)
b0 = round( 127 · ln(f0 / 60) / _LOG_SPAN ) → b0 ∈ [0, 127] (7 bits)
b0_to_f0_L(b0) (ambe.py:54):
b0 = clip(b0, 0, 127)
f0 = 60 · exp( b0 · _LOG_SPAN / 127 )
L = clip( 0.95 · (FS/2) / f0 , 1, 56 ) harmonic count (int-truncated)
The 7-bit index spans the whole window: b0 = 0 → 60 Hz, b0 = 127 → 400 Hz. L is the harmonic count — the number of amplitudes carried — set exactly as in the MBE model: harmonics up to 0.95·f_s/2 = 3800 Hz, clamped to [1, 56]. At 8 kHz that is ≈ 9 harmonics at 400 Hz up to the 56-cap below ~68 Hz. L is a pure function of b0, so the encoder and decoder agree on field sizes from the pitch field alone (test test_pitch_map_monotonic_and_recovers pins monotonicity + <2 % f₀ recovery).
Band voicing — 5 bands over L harmonics
The five V/UV bits (_VUV_BANDS = 5) are a majority-vote compression of the per-harmonic voiced flags. _band_of(l, L) (ambe.py:62) maps harmonic index l (0-based) to a band:
band(l) = min( l · 5 // L , 4 )
_encode_vuv(voiced, L) (ambe.py:66) tallies, per band, votes for voiced/unvoiced and emits 1 if voiced ≥ unvoiced (ties → voiced). _decode_vuv(bits5, L) (ambe.py:73) expands the 5 bits back to L per-harmonic flags by replicating each band bit to its harmonics. This is the same per-band voicing MBE synthesises from — sinusoids for voiced bands, shaped noise for unvoiced (see mbe.md §1).
5. The 72-bit channel frames
CHANNEL_BITS = 72. The channel frame protects the parameter frame's first 24 bits — the two most sensitive 12-bit words C0 (pitch + top voicing/gain) and C1 (next 12) — with Golay codes, scrambles C1 with a PN sequence, and carries the remaining low-sensitivity bits raw. Built by _to_channel (ambe.py:129), inverted by _from_channel (ambe.py:141).
Layout
Half-rate (Golay23 on C1, 25 raw bits):
bits 0 .. 23 C0 = Golay(24,12) of param bits 0..11 24 bits, unscrambled
bits 24 .. 46 C1 = Golay(23,12) of param bits 12..23, 23 bits, XOR PN(C0)
then PN-scrambled
bits 47 .. 71 param bits 24..48 carried raw 25 bits
total = 24+23+25 = 72
Full-rate (Golay24 on C1, 24 raw bits):
bits 0 .. 23 C0 = Golay(24,12) of param bits 0..11 24 bits, unscrambled
bits 24 .. 47 C1 = Golay(24,12) of param bits 12..23, 24 bits, XOR PN(C0)
then PN-scrambled
bits 48 .. 71 param bits 24..47 carried raw 24 bits
total = 24+24+24 = 72
The PN scrambler — seeded by C0
_pn(seed, n) (ambe.py:118) is a 15-bit LFSR, feedback taps at bits 14 and 13, identical to P25's p25/voice.py:_pn (confirmed against source):
reg = (seed & 0xFFF) or 0xFFF # 12-bit seed, never all-zero
for each of n output bits:
b = ((reg >> 14) ^ (reg >> 13)) & 1
reg = ((reg << 1) | b) & 0x7FFF
The seed is the decoded C0 data word, which is why ordering matters: C0 must be corrected first because it seeds the PN that de-scrambles C1.
Encode (_to_channel)
- Split the parameter frame:
c0d = bits[0:12],c1d = bits[12:24]. c1 = golay24_encode(c1d)(full-rate) orgolay23_encode(c1d)(half-rate);c1n= 24 or 23.pn = _pn(c0d, c1n); scramble:c1bits = golay_bits(c1) XOR pn.- Emit
Golay24(c0d)(24 bits, unscrambled — it is the PN seed) +c1bits- the untouched tail
bits[24:].
- the untouched tail
Decode (_from_channel)
c0d = golay24_decode(bits[0:24])— correct up to 3 errors; fail →(None, −1)(nothing can proceed without the PN seed).pn = _pn(c0d, c1n); de-scramblebits[24:24+c1n] XOR pn, thengolay23_decode/golay24_decode→c1d; fail →(None, −1).- Reassemble
out = tobits(c0d,12) + tobits(c1d,12) + tail. - Corrected-bit count: re-encode
outand compare to the received frame —nerr = Σ(_to_channel(out) ≠ bits72)— then returnout[:nbits], nerr.
The Golay(24,12,8)/(23,12,7) primitives come from p25/fec.py (golay24_encode, golay24_decode, golay23_encode, golay23_decode): a [23,12] cyclic generator 0xC75 with a full weight-≤3 syndrome→error table, extended to [24,12] by an overall even-parity bit. Each Golay word corrects up to 3 bit errors.
6. Synthesis via the MBE vocoder
synthesize_hr(bits49) / synthesize_fr(bits48) (ambe.py:178, 184) are the one-call parameter-frame → PCM path:
f0, voiced, amps = decode_frame_hr(bits49) # §3 / §4
pcm = mbe.synthesize(f0, voiced, amps, fs) # §see mbe.md
The synthesis itself — voiced-band sinusoids, unvoiced-band shaped noise, the 0.95·f_s/2 harmonic cut, zero-phase default — is entirely the MBE vocoder and is not re-documented here. The full native chain for a mode is:
DV voice burst ─▶ (mode framing/deinterleave) ─▶ 72-bit channel frame
─▶ channel_to_hr / channel_to_fr (Golay + PN, §5)
─▶ 49/48-bit parameter frame
─▶ decode_frame_hr / _fr (§3, §4)
─▶ (f0, voiced, amps) ─▶ mbe.synthesize ─▶ PCM
7. Constants & tables (verbatim from ambe.py)
| Name | Value | Meaning |
|---|---|---|
_PITCH_BITS |
7 | width of the b0 log-pitch index |
_VUV_BANDS |
5 | band-voicing bits per frame |
_F0_LO, _F0_HI |
60.0, 400.0 Hz | pitch window |
_LOG_SPAN |
ln(400/60) ≈ 1.897 |
log span of the pitch window |
HR_BITS |
49 | half-rate AMBE+2 parameter frame |
FR_BITS |
48 | full-rate AMBE parameter frame |
CHANNEL_BITS |
72 | on-air channel frame |
L cap |
[1, 56] |
harmonic count clamp (b0_to_f0_L) |
| harmonic band-limit | 0.95 · FS/2 = 3800 Hz |
highest harmonic used |
Imported (not redefined here):
| Name | Source | Meaning |
|---|---|---|
FS |
mbe.py |
8000 Hz sample rate; 160-sample (20 ms) frame |
encode_amps, decode_amps |
imbe.py |
gain(6) + differential-log residual quantiser |
_GAIN_BITS=6, _GAIN_LO/HI=−10/+6, _RES_RANGE=±3 |
imbe.py |
amplitude quantiser ranges |
golay24_encode/decode, golay23_encode/decode |
p25/fec.py |
Golay(24,12,8)/(23,12,7), corrects ≤3 |
PN LFSR 0x7FFF, taps 14/13, seed & 0xFFF or 0xFFF |
ambe.py (= p25/voice.py) |
C0-seeded scrambler |
8. Interoperability & validation
test_ambe.py exercises the codec end to end. It does not claim interop against real DVSI frames (see §9); it pins the round-trip self-consistency and the FEC behaviour.
| Test | Asserts |
|---|---|
test_pitch_map_monotonic_and_recovers |
f0_to_b0 monotone over 60–400 Hz; b0_to_f0_L recovers f₀ within 2 %, L ≥ 1 |
test_hr_parameter_roundtrip |
HR encode→decode: f₀ within 5 %, amplitude corr > 0.7, voicing agreement > 80 % |
test_fr_parameter_roundtrip |
same for the full-rate 48-bit frame |
test_hr_channel_frame_corrects_errors |
HR channel frame is 72 bits; 6 injected errors (3 in C0, 3 in C1) fully corrected, nerr ≥ 6 |
test_fr_channel_frame_corrects_errors |
same for full-rate (2× Golay24) |
test_pn_scrambling_active |
C1 on the wire ≠ plain Golay23 (PN really active); XOR-ing PN back recovers the plaintext (involutive) |
test_full_native_chain_analyze_to_pcm |
mbe.synthesize→noise→mbe.analyze→encode_frame_hr→hr_to_channel→ inject 2 errors →channel_to_hr→synthesize_hr: 160 finite, non-silent samples, no DVSI/mbelib |
The last test is the headline: a full analyse-to-PCM loop with channel bit errors, entirely in-repo.
9. Limitations — honest scope
This mirrors exactly the stance of imbe.md §3, and the ambe.py module docstring states it outright. What is spec-accurate:
- The frame sizes (49-bit half-rate, 48-bit full-rate, 72-bit channel).
- The field order — pitch, band voicing, gain, spectral amplitudes.
- The Golay/PN channel-frame structure — Golay(24,12) on C0, Golay(23,12) (half) / Golay(24,12) (full) on C1, PN-scrambled, remainder raw, C0 seeding the PN.
What is structurally-AMBE but not DVSI-bit-exact:
- The quantisation tables — the logarithmic pitch map, the 6-bit gain + differential-log amplitude residuals bit-allocated by harmonic count. These are the AMBE scheme and round-trip self-consistently (§8), but they are not DVSI's proprietary vector-quantisation codebooks.
- DVSI's exact VQ tables, the PN seed constant, and inter-frame prediction (AMBE+2 predicts spectral parameters across frames) are the named remaining transcription for bit-exact on-air audio against real DMR/NXDN/YSF/D-STAR radios.
So: this decodes and re-encodes its own frames faithfully and synthesises intelligible native speech, but it will not produce bit-exact audio from a frame captured off a real DVSI-equipped radio, nor a frame a DVSI decoder would reproduce bit-for-bit. Same honest position as IMBE; AMBE+2 also carries in-force patents (non-commercial UK use here).
Additionally, phases are not transmitted (an MBE property, see mbe.md §4) — synthesis defaults to zero phase, the usual slightly "buzzy" vocoder character.
10. References
- TIA-102.BABA-1 — the Half-Rate Vocoder Annex (AMBE+2) used by DMR / NXDN (EHR) / YSF DN: the 49-bit parameter frame and its 72-bit Golay+PN channel frame.
- JARL D-STAR specification — full-rate AMBE (2400 bps voice + 1200 bps FEC): the 48-bit parameter frame and its 2× Golay(24,12) channel frame.
- DVSI AMBE-2000 / AMBE-3000 — the reference hardware vocoders this replaces.
- In-repo:
app/radio/ambe.py(this codec),app/radio/mbe.py(MBE vocoder),app/radio/imbe.py(IMBE, the shared amplitude quantiser),app/radio/p25/fec.py(Golay),app/radio/p25/voice.py(the PN construction),test_ambe.py(validation).
Related: IMBE (the sibling codec — same layering for P25's 88-bit frame), MBE vocoder (the synthesis model beneath this), and the modes that carry these frames — DMR, NXDN, YSF, D-STAR. The amplitude quantiser theory is in the maths guide on source coding.