← Back to News

AMR-NB and AMR-WB in Pure Go

codecamramr-wbamr-nbevssipvoltegolang

VoiceBlender can now encode and decode AMR-NB and AMR-WB as first-class SIP codecs. Both implementations are pure Go, built as standalone libraries:

Both are Apache-2.0, hand-ported from the fixed-point opencore-amr reference sources, and build with CGO_ENABLED=0.

Why these two codecs

AMR is not a quality-versus-bandwidth preference - on mobile networks it is the codec you have to speak to interoperate at all.

AMR-NB is the mandatory speech codec for GSM and UMTS circuit-switched voice. Its 12.2 kbit/s mode is bit-equivalent to GSM-EFR. Any path that touches a legacy mobile leg - a carrier gateway, a 2G/3G fallback, a handset that negotiates down - will land on AMR-NB. It runs at 8 kHz over 20 ms frames of 160 samples.

AMR-WB (marketed as HD Voice) is the wideband codec mandated for VoLTE by GSMA IR.92. It samples at 16 kHz - 320 samples per 20 ms frame - which roughly doubles the audio bandwidth over narrowband and is the difference between telephone-grade and wideband speech on the wire. It is also the codec used by WhatsApp Business Calling. For any deployment that bridges to a VoLTE core or a mobile carrier, AMR-WB is a hard requirement, not an optimisation.

Without native support, the only options are transcoding through an external C library over cgo, or not interoperating. cgo pulls in a C toolchain, breaks static cross-compilation, and adds a foreign-call boundary on every frame. Keeping the codecs in Go preserves VoiceBlender’s single static binary and its CGO_ENABLED=0 build.

What the libraries implement

Both cover the full encode and decode path - LP analysis, LSF quantization, open- and closed-loop pitch search, the per-mode algebraic codebooks, gain quantization, synthesis, and post-filter - across every standard mode:

CodecModesBitrates (kbit/s)Frame
AMR-NB84.75, 5.15, 5.90, 6.70, 7.40, 7.95, 10.2, 12.2160 samples @ 8 kHz
AMR-WB96.60, 8.85, 12.65, 14.25, 15.85, 18.25, 19.85, 23.05, 23.85320 samples @ 16 kHz

Both speak RFC 4867 RTP payload framing in both octet-aligned and bandwidth-efficient forms. On amd64 the hot FIR and correlation kernels use AVX2 assembly with pure-Go fallbacks on other architectures. The AMR-WB decode path is bit-exact against the C reference on the tone test vectors; the encode path has one known divergence, described next.

The amplitude divergence in AMR-WB

The first AMR-WB release validated the encoder with a tone-input test (TestEncDiffAgainstCReference), and it matched the vo-amrwbenc reference bit-for-bit. Real speech told a different story: decoded output was audibly loud and clipping.

The cause is a porting defect in the encoder’s fixed-point path. For non-tone content at input levels above roughly −12 dBFS, the produced bit stream diverges from the vo-amrwbenc reference, and the divergent frames decode to synthesised speech that overshoots into full-scale saturation - the “loud and clipping” symptom. The reference C encoder/decoder pair handles the same input cleanly up to 0 dBFS, so the fault is in the Go port, not the algorithm. The tone test never surfaced it because a steady tone does not exercise the code path that diverges; the likely site is a missing saturation or rounding step in the LPC analysis or the ACELP codebook search, and pinning the exact instruction needs a per-stage bisect against the C reference.

The shipped fix is a mitigation, not a root-cause correction: the encoder pre-attenuates its input by 6 dB (a one-bit right shift of every sample) before the fixed-point stage, which keeps the decoded envelope below full scale on realistic speech-band content. An end-to-end test (TestEncoderHeadroomPreventsDecoderSaturation) drives 5 s of speech-band noise at −3 dBFS peak through the encode/decode round trip and asserts the output does not clip. The exact divergence remains open.

Negotiation and configuration

The codecs plug into the existing SIP codec negotiation. Add them to SIP_CODECS in preference order (names are case-insensitive; a bare AMR resolves to AMR-NB per RFC 4867 §8.1):

SIP_CODECS=AMR-WB,AMR-NB,PCMU,PCMA

The encoder speech mode is a ceiling, not a fixed rate - VoiceBlender clamps it to the mode-set the peer negotiates in SDP, and on answers echoes whatever framing the peer offered:

AMRWB_MODE=2          # ceiling: 0..8 (2 = 12.65 kbit/s, the IR.92/VoLTE default)
AMRWB_OCTET_ALIGNED=false
AMRNB_MODE=7          # ceiling: 0..7 (7 = 12.2 kbit/s, GSM-EFR-equivalent)
AMRNB_OCTET_ALIGNED=false

See the configuration reference for the full variable set.

Performance against the C reference

The relevant bar is real time: a 20 ms frame must encode and decode in well under 20 ms of wall clock. Both codecs clear that by a wide margin on a single core. Measured against the fixed-point C reference on an AMD Ryzen 9 7900 (Go 1.26.3, linux/amd64):

  • AMR-WB - encode runs 1.32–1.63× the C reference time; decode runs 1.04–1.17×, effectively at parity.
  • AMR-NB - encode runs roughly 1.08–1.26× the C reference time; decode runs faster than C, at 0.74–0.88× of its time.

The gap is largest on the encode path, where the closed-loop search dominates, and closes to parity or better on decode. In both cases the absolute cost is a small fraction of the per-frame budget, so codec CPU is not the constraint on how many concurrent legs a node carries.

What about EVS

The obvious question for a VoLTE-adjacent stack is EVS - Enhanced Voice Services, marketed as HD Voice+. It is the 3GPP Release 12 codec (TS 26.441 series, 2014) positioned as the successor to AMR-WB for VoLTE, and it differs from the AMR family on two axes that matter here.

Technically, EVS is a wider and more capable codec:

  • Audio bandwidth. AMR-NB is narrowband (8 kHz) and AMR-WB is wideband (16 kHz), each fixed at one sample rate. EVS spans narrowband, wideband, super-wideband (32 kHz) and fullband (48 kHz) in a single codec, so it reaches well past the ~7 kHz audio bandwidth of AMR-WB.
  • Coding model. AMR-NB and AMR-WB are pure ACELP - a speech model that degrades on music and mixed content. EVS switches between an ACELP speech path and an MDCT transform path per frame, so it holds up on non-speech audio where ACELP does not.
  • Rate range and robustness. EVS runs from 5.9 up to 128 kbit/s and adds a channel-aware mode that carries partial redundancy for packet-loss recovery on LTE. Its stated quality at 13.2 kbit/s is comparable to AMR-WB near the top of its 6.6–23.85 kbit/s range.
  • Interoperability. EVS includes an AMR-WB interoperable (AMR-WB IO) mode that emits and consumes AMR-WB bit streams, so an EVS endpoint can bridge to an AMR-WB endpoint without transcoding. Framing for the primary modes is specified in 3GPP TS 26.445 Annex A, again over 20 ms frames.

Legally, EVS is why this post is about AMR. The essential patents on AMR-NB (1999) and AMR-WB (2002) have largely lapsed, which is what makes royalty-free implementations like opencore-amr - and these Go ports on top of them . EVS remains under an active royalty-bearing patent pool; the 3GPP reference C is published but license-encumbered, and a Go port would not change that. So EVS is out of scope until its patents expire.

Using them elsewhere

Both libraries stand alone and have no dependency on VoiceBlender. The API is a mode-configured encoder and decoder over 16-bit mono PCM:

enc, _ := amrwb.NewEncoder(amrwb.EncoderConfig{Mode: amrwb.Mode2385})
dec := amrwb.NewDecoder(amrwb.DecoderConfig{OctetAligned: true})

If you need AMR in a Go project without a cgo dependency, they are usable on their own under Apache-2.0.