MBE vocoder — native Multi-Band Excitation analysis + synthesis
The vocoder model underneath P25's IMBE and the DMR/D-STAR/YSF/NXDN AMBE family: each 20 ms speech frame reduced to a fundamental frequency, one amplitude per harmonic, and a voiced/unvoiced decision per harmonic band — synthesised back as sinusoids for the voiced bands and shaped noise for the unvoiced ones. This module replaces the mbelib dependency for the parameters→speech step.
Rafe project · app/radio/mbe.py, test_mbe.py · analysis + synthesis · clean-room from the published MBE model (Griffin & Lim); the per-codec bit quantisation (P25's 88-bit IMBE frame) wires on top
Abstract
Every digital-voice mode in the catalogue transmits a model of the talker, not the waveform (maths guide §13.4). Multi-Band Excitation is the model family used by IMBE (P25 Phase 1) and AMBE (DMR, D-STAR, YSF, NXDN): where a classic LPC vocoder makes one voiced-or-unvoiced decision per frame, MBE makes one per harmonic band, which is why it survives mixed excitation (breathy vowels, fricative-vowel transitions) that makes single-band vocoders sound robotic.
Historically the open-source route to MBE speech was mbelib. This module is Rafe's native replacement for the model layer: analyze() extracts \((f_0,\ \{v_\ell\},\ \{A_\ell\})\) from PCM, synthesize() regenerates PCM from them. The codec-specific quantisation — how P25 packs those parameters into its 88-bit IMBE frame — is the IMBE parameter codec, which together with this module makes the P25 voice path fully native; the P25 spec documents the FEC around it. Patent status: the IMBE patents have expired, and this project is non-commercial (UK), so the model layer carries no licensing dependency.
1. The model
A frame of speech is represented by:
| Parameter | Symbol | Meaning |
|---|---|---|
| fundamental | \(f_0\) | pitch, Hz (search range 80–400 Hz) |
| harmonic amplitudes | \(A_\ell\), \(\ell = 1..L\) | magnitude of each harmonic \(\ell f_0\) |
| band voicing | \(v_\ell \in \{0,1\}\) | is band \(\ell\) periodic (voiced) or noise-like? |
with \(L\) set by the bandwidth: harmonics are used up to \(0.95 \cdot f_s/2\), i.e. \(L = \lfloor 0.95\,(f_s/2)/f_0 \rfloor\) (≈ 9–47 harmonics across the pitch range at 8 kHz).
Synthesis is a direct reading of the model — for each harmonic \(\ell\):
- voiced band: \(A_\ell \cos(2\pi \ell f_0 t + \varphi_\ell)\) (phases default to 0, or are supplied);
- unvoiced band: \(A_\ell\, w(t) \cos(2\pi \ell f_0 t)\) with \(w\) white Gaussian noise — i.e. a band of noise centred on the harmonic frequency with the same energy placement, from a seeded RNG so frames are reproducible.
Sum over harmonics; harmonics at or above \(0.95 f_s/2\) are dropped. Defaults: \(f_s = 8000\) Hz, frame \(n = 160\) samples (20 ms).
2. Analysis, step by step
analyze(pcm, fs=8000, fmin=80, fmax=400, nfft=4096):
- Pitch by autocorrelation. Remove the mean, autocorrelate, and take the peak lag within \([f_s/f_{\max},\ f_s/f_{\min}]\) = samples 20…100 at 8 kHz; \(f_0 = f_s/\text{lag}\).
- Harmonic count. \(L = \max(1, \lfloor 0.95\,(f_s/2)/f_0 \rfloor)\).
- Spectrum. Hann-window the frame, magnitude FFT at
nfft = 4096(bin width \(f_s/4096 \approx 2\) Hz); amplitude calibrationscale = 2 / sum(hann(n))so a unit-amplitude sinusoid measures ≈ 1. - Per harmonic \(\ell\): read the spectrum at bin \(\mathrm{round}(\ell f_0 / \text{binhz})\) → \(A_\ell\) (× scale).
- Voicing per band: compare the harmonic peak against the inter-harmonic bin at \((\ell + 0.5) f_0\) — a periodic band has a deep null between harmonics, a noise band doesn't. Decision: \(v_\ell = \big[\text{peak} > 4 \times \text{between}\big]\).
That factor-4 (12 dB) peak-to-null contrast is the entire voicing detector, and the tests pin its behaviour from both sides: an all-voiced synthetic frame comes back ≥ (L−1) voiced, pure noise comes back < 30 % voiced, and a half-voiced/half-unvoiced frame recovers the split exactly.
spectral_envelope(f0, amps) is a small helper that paints the harmonic amplitudes onto a 512-point spectrum grid, used to score reconstruction quality in the tests.
3. Validation
| Test | Asserts |
|---|---|
test_f0_recovery |
110/155/220 Hz recovered within 5 % |
test_amplitude_recovery |
correlation > 0.95 between true and analysed \(A_\ell\) |
test_voiced_detection |
all-voiced frame → ≥ L−1 bands voiced |
test_unvoiced_detection |
white noise → < 30 % bands voiced |
test_mixed_voicing |
5 voiced + 5 unvoiced bands recovered exactly |
test_envelope_reconstruction |
envelope correlation > 0.9 through analyse→synthesise |
test_synthesis_shape_finite |
output shape and finiteness |
(Summary figures also quoted in native-protocols.md: f0 within 5 %, amplitude correlation > 0.95, V/UV recovered including mixed, envelope correlation > 0.97 on the reference frames.)
4. Scope and the wiring step
What this module deliberately is and isn't:
- It is the model layer — the part
mbelibprovided: parameters in, speech out (and, for testing/round-trips, speech in, parameters out). - It is not the bit codec. IMBE's 88-bit P25 frame maps onto these parameters via the IMBE parameter codec — implemented, making P25 voice fully native (the exact IMBE amplitude tables remain for bit-exact on-air audio; see that spec's §3). The same layer pattern serves the AMBE modes (DMR/D-STAR/YSF/NXDN) whose framing is otherwise straightforward. AMBE+2 retains some in-force patents and stays out of scope.
- Phases are not transmitted (as in real MBE codecs): synthesis defaults to zero phase per harmonic, which is audible as a slightly "buzzy" character versus the original — the standard vocoder trade, improvable with phase continuation across frames at synthesis time.
Related: P25 (the IMBE frame this feeds), RVQ-Voice (Rafe's own vocoder — LPC front end + residual vector quantiser, a different answer to the same question), FreeDV/M17 (Codec2 — sinusoidal coding, MBE's close cousin), and the maths guide on source coding.