DMR voice quality (AMBE+2)
Conventional DMR now decodes end to end, but the decoded voice — female voices especially — can sound rough. This page records what the decode chain does, a mbelib reconciliation review (what was ruled out and the one genuine divergence found), the operator levers you can pull today, and the calibration-gated path to a verified fix.
The short version: GopherTrunk’s default DMR decoder
(internal/voice/ambe2)
is a clean-room Go re-implementation of mbelib’s ambe3600x2450 path. mbelib
is a reverse-engineered, approximate AMBE+2 decoder — it is known to sound
worse than DVSI silicon, and worst on high-pitched (female) voices. So a large
part of what you hear is inherent to the algorithm being mirrored, not a discrete
bug. That said, one genuine divergence from the reference was found (below) —
the prime suspect for the female-voice-specific roughness — and the corrected,
spec-faithful form now ships enabled by default behind the tri-state
recordings.spec_amplitude_enhance config key (unset = on; set false to get
the legacy envelope back). The calibration harness below remains the gate for
its final quality sign-off.
Why female voices are the hard case
A female voice has a higher fundamental ω₀, hence fewer harmonics L
(internal/voice/ambe2/tables2450.go: L ranges 9…56, and the low-L rows are the
high-pitch ones). Everything in the model that is indexed by harmonic position
l relative to L therefore behaves differently for small L — most of the
band sits in the “high band” (8l > L) where the spectral-amplitude enhancement
acts, and the voiced/unvoiced and phase-dispersion decisions cover proportionally
more of the voice. So a mis-tuned or mis-transcribed high-band step hits female
voices hardest.
Reconciliation review (mbelib ambe3600x2450 + TIA-102.BABA §6.2)
Ruled OUT — these faithfully match the reference
- Pitch / L / w₀ tables (
tables2450.go) — generated from mbelib’sambe3600x2450_const.h; the b0→(w₀, L) lookup matches. - Voiced/unvoiced decode (
params2450.go:60-72). Thejl = int(l·16·f0)index and thedmrVuv[b1][jl]lookup match mbelib. The clamp to[0,7]is a defined-behaviour guard; across the real f0 rangejlspans the full 0…7 of the table (e.g. at the highest f0≈0.05,jl = 0.8·lreaches 7 only at l≈9), so there is no pathological V/UV collapse for female frames. - §6.3 voiced-phase de-buzz (
decoder.go:synthFrame, ~627-664). mbelib doesPHIl = PSIlforl ≤ L/4andPHIl = PSIl + numUv·rand_phase()/Labove it, withrand_phase() ∈ [-π, π]. GT draws(-1..1)·π · (numUv/L)forl > L/4voiced harmonics — the same distribution and scaling. Faithful.
Found — the one genuine divergence (§6.2 spectral-amplitude enhancement), now fixed
internal/voice/mbe/enhance.go
EnhanceAmplitudes is on the default decode path (for both AMBE+2/DMR and
IMBE/P25). Its legacy per-harmonic weight ξ dropped a factor that is present in
both references the code cites (TIA-102.BABA §6.2 and mbelib’s
mbe_spectralAmpEnhance):
mbelib / spec: ξ = 0.96·π·num / ( ω0 · R_M0 · (R_M0² − R_M1²) )
legacy GT: ξ = 0.96·num / ( R_M0 · (R_M0² − R_M1²) )
└─ missing the π / ω0 factor ─┘
num = R_M0² + R_M1² − 2·R_M0·R_M1·cos(ω0·l), W_l = ξ^0.25, clamped [0.5, 1.2]
For DMR, ω₀ ∈ [~0.05, ~0.31] rad, so π/ω0 ∈ [~10, ~63]. The legacy ξ is
therefore 10–63× smaller than the spec’s; after the ^0.25, the high-band
weights are ~1.8–2.8× lower, so they land on the 0.5 (attenuate) clamp where
the spec would push toward the 1.2 (boost) clamp. Because the frame is then
energy-renormalised (γ = √(R_M0/Σ)), the relative spectral tilt shifts energy
out of the high band into the low band — a duller timbre, and, since small-L
(female) frames have most of their harmonics in that high band, the effect is
strongest on female voices. This matches the reported symptom.
Both missing factors are now restored and ship enabled by default.
EnhanceAmplitudes(p, M, specFaithful) carries a specFaithful flag: the
spec-faithful branch restores the π/ω₀ factor and mbelib’s √M_l
amplitude weight (which makes the clamp gate scale-invariant — without it a
pure gain change reshapes the enhanced envelope). The recorder/live path
enables it per call for both DMR AMBE+2 and P25 IMBE, governed by the
tri-state recordings.spec_amplitude_enhance key (unset = on; false
restores the legacy envelope, which stays byte-identical to prior releases so
unit-test goldens hold). See internal/voice/mbe/enhance.go and the
CHANGELOG entry that shipped it.
Per this repo’s #764/#771 discipline, the final quality sign-off is still
calibration-gated: matching the spec’s (or mbelib’s) formula is not by itself
proof of better perceived audio, and EnhanceAmplitudes is shared with the
P25 IMBE path. The calibrate harness below is that gate — with a real
reference WAV in place, the TestCompareAMBE2 cross-correlation will rise
if the spec-faithful form helps and fall if it hurts.
Levers you can pull today (no code change)
These are perceptual, not spec-exact, but cost nothing to try and can meaningfully warm up / brighten DMR audio while the calibration above is pending:
-
recordings.enhance— the opt-in output chain (EnhanceConfig). A high-shelf and a louder AGC target help intelligibility:recordings: write_raw: true # also needed for calibration below enhance: enabled: true hpf_hz: 200 # trim rumble lpf_hz: 3400 # telephone band shelf_hz: 1600 # gentle presence shelf_db: 2 # dB of cut above shelf_hz (positive; <=0 disables) agc_target: 22000 # louder playback -
ambe2-dmr-warm— the warm DMR vocoder variant (a high-shelfToneTilt), selected byrecordings.warm_dmr_audio: true(WarmDMRAudio). Softens the synthetic “edge” without touching the model decode.
Verified-fix path (calibration-first)
You already have the input side: every DMR call the daemon records with
recordings.write_raw: true writes a <UTC>_<id>.raw sidecar of the packed
AMBE+2 frames (7 bytes/frame) next to the .wav. Pick a female-voice call —
the hard case — and:
-
Reference-decode the same frames through an independent AMBE+2 decoder. DSD-FME’s
-rreads its own cookie-headed.ambcontainer, not the flat.raw— enablerecordings.mbe_files: trueso the daemon writes the.ambnext to the recording, then:dsd-fme -fs -w reference12k.wav -r <call>.amb # DSD-FME's -w stamps an 8 kHz header on 12 kHz synthesis (see # docs/vocoders.md) — fix the rate before comparing: tail -c +45 reference12k.wav > ref12k.pcm sox -t raw -r 12000 -e signed -b 16 -c 1 ref12k.pcm -r 8000 reference.wav -
Drop both files in and run the A/B (the test skips until the reference lands, so CI stays green today):
cp <call>.raw internal/voice/ambe2/testdata/dmr-voice.raw cp reference.wav internal/voice/ambe2/testdata/dmr-voice-dsdfme.wav go test ./internal/voice/calibrate/ -v -run TestCompareAMBE2Acceptance:
|RMSRatioDb| < 3.0(loudness) andPeakXcorr > 0.85(waveform similarity). A clean RMS but low xcorr is the synthesis signature — i.e. the §6.2 enhancement above, not a gain knob. -
A/B the §6.2 correction. The spec-faithful form is the default, so the A/B is a config toggle, not a code change: run once as-is, once with
recordings.spec_amplitude_enhance: false(the legacy envelope), and comparePeakXcorr. If the spec-faithful run scores higher (and DMR + P25 both hold or improve), the default is confirmed; if not, report it — that reference pair is exactly the evidence the sign-off needs.
See Voice calibration for the full harness reference
(the cmd/voice-calibrate CLI, the IMBE path, and the AGC TargetPeak knob for
the loudness half).
Do not commit real over-the-air public-safety recordings as fixtures. The calibrate testdata dirs are for local, operator-provided captures — the tests
t.Skipwhen the files are absent, which is the intended state in the repo.