Patrick Lidstone
Self-hosted

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:

  1. its framing (bursts, sync, slot structure) — the mode's own spec; and
  2. 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 _fr pair) produce and consume, and what synthesize_hr/synthesize_fr turn 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:8398). 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):

  1. 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.
  2. amps/voiced are _fit to length L (ambe.py:78) — truncated, or padded by repeating the last value (amplitude fill 1e-3, voicing fill False).
  3. bits = _tobits(b0, 7) + _encode_vuv(voiced, L) (5 bits) + encode_amps(amps, nbits − 7 − 5).
  4. 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)

  1. Split the parameter frame: c0d = bits[0:12], c1d = bits[12:24].
  2. c1 = golay24_encode(c1d) (full-rate) or golay23_encode(c1d) (half-rate); c1n = 24 or 23.
  3. pn = _pn(c0d, c1n); scramble: c1bits = golay_bits(c1) XOR pn.
  4. Emit Golay24(c0d) (24 bits, unscrambled — it is the PN seed) + c1bits
    • the untouched tail bits[24:].

Decode (_from_channel)

  1. c0d = golay24_decode(bits[0:24]) — correct up to 3 errors; fail → (None, −1) (nothing can proceed without the PN seed).
  2. pn = _pn(c0d, c1n); de-scramble bits[24:24+c1n] XOR pn, then golay23_decode / golay24_decodec1d; fail → (None, −1).
  3. Reassemble out = tobits(c0d,12) + tobits(c1d,12) + tail.
  4. Corrected-bit count: re-encode out and compare to the received frame — nerr = Σ(_to_channel(out) ≠ bits72) — then return out[: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.analyzeencode_frame_hrhr_to_channel→ inject 2 errors →channel_to_hrsynthesize_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.