Skip to main content

wavekat_core/codec/
g711.rs

1//! G.711 μ-law (PCMU) and A-law (PCMA) codecs.
2//!
3//! All four functions are byte-for-byte conversions: one 16-bit PCM
4//! sample ↔ one 8-bit codeword. A 20 ms RTP frame at 8 kHz is therefore
5//! 160 samples / 160 bytes — no length surprises.
6//!
7//! The tables follow ITU-T G.711; see
8//! <https://www.itu.int/rec/T-REC-G.711> for the recommendation and
9//! <https://en.wikipedia.org/wiki/G.711> for a readable summary.
10//! Implementations are cross-checked against the reference vectors in
11//! Sun Microsystems' `g711.c` and SpanDSP's reference.
12//!
13//! G.711 lives in `wavekat-core` (not `wavekat-sip`) because codecs are
14//! a consumer-layer choice — `wavekat-sip` deliberately stays
15//! codec-agnostic; SDP advertises both PCMU and PCMA and the consumer
16//! picks one after answering.
17
18/// SDP / RTP static payload type for μ-law (G.711U).
19pub const PCMU_PAYLOAD_TYPE: u8 = 0;
20/// SDP / RTP static payload type for A-law (G.711A).
21pub const PCMA_PAYLOAD_TYPE: u8 = 8;
22
23/// Sample rate of every static G.711 stream. The wire format does not
24/// carry the rate; both endpoints just know.
25pub const G711_SAMPLE_RATE: u32 = 8000;
26/// Samples in a 20 ms G.711 frame (the standard RTP packetization
27/// interval).
28pub const G711_FRAME_SAMPLES: usize = 160;
29
30const CLIP: i32 = 32635;
31const BIAS: i32 = 0x84;
32const SIGN_BIT: u8 = 0x80;
33const QUANT_MASK: u8 = 0x0F;
34const SEG_SHIFT: u8 = 4;
35const SEG_MASK: u8 = 0x70;
36
37// G.711 segment index for `pcm` in `[0, 0x7FFF]`. The segment is the
38// position of the highest set bit above bit 7, clamped to 0 for
39// `pcm < 0x100`. Callers in this file bound their inputs to `≤ 0x7FFF`
40// (μ-law clips to CLIP+BIAS=0x7FFF; A-law masks with 0x7FFF), so we
41// pick the bit-math form that needs no out-of-range fallback.
42#[inline]
43fn seg_for(pcm: i32) -> u32 {
44    if pcm < 0x100 {
45        0
46    } else {
47        31 - (pcm as u32).leading_zeros() - 7
48    }
49}
50
51/// Encode one 16-bit PCM sample to a μ-law byte (G.711U).
52pub fn linear_to_ulaw(pcm: i16) -> u8 {
53    let mut pcm = pcm as i32;
54    let sign = if pcm < 0 {
55        pcm = -pcm;
56        0x7F
57    } else {
58        0xFF
59    };
60    if pcm > CLIP {
61        pcm = CLIP;
62    }
63    pcm += BIAS;
64
65    let seg = seg_for(pcm);
66    let mantissa = ((pcm >> (seg + 3)) & 0x0F) as u8;
67    let coded = ((seg as u8) << 4) | mantissa;
68    coded ^ sign
69}
70
71/// Decode one μ-law byte to a 16-bit PCM sample.
72pub fn ulaw_to_linear(ulaw: u8) -> i16 {
73    let ulaw = !ulaw;
74    let sign = (ulaw & SIGN_BIT) != 0;
75    let exponent = (ulaw & SEG_MASK) >> SEG_SHIFT;
76    let mantissa = ulaw & QUANT_MASK;
77    let mut sample = (((mantissa as i32) << 3) + BIAS) << exponent;
78    sample -= BIAS;
79    if sign {
80        -sample as i16
81    } else {
82        sample as i16
83    }
84}
85
86/// Encode one 16-bit PCM sample to an A-law byte (G.711A).
87pub fn linear_to_alaw(pcm: i16) -> u8 {
88    let (pcm, mask) = if pcm >= 0 {
89        (pcm as i32, 0xD5u8)
90    } else {
91        (((!pcm) as i32) & 0x7FFF, 0x55u8)
92    };
93
94    let seg = seg_for(pcm);
95    let mantissa = if seg < 1 {
96        ((pcm >> 4) & 0x0F) as u8
97    } else {
98        ((pcm >> (seg + 3)) & 0x0F) as u8
99    };
100    let coded = ((seg as u8) << 4) | mantissa;
101    coded ^ mask
102}
103
104/// Decode one A-law byte to a 16-bit PCM sample.
105///
106/// A-law's sign-bit convention is opposite to μ-law's: after XOR with
107/// `0x55`, sign bit set means *positive* (see ITU-T G.711 §2.3, or
108/// SpanDSP's reference implementation).
109pub fn alaw_to_linear(alaw: u8) -> i16 {
110    let alaw = alaw ^ 0x55;
111    let sign_set = (alaw & SIGN_BIT) != 0;
112    let exponent = (alaw & SEG_MASK) >> SEG_SHIFT;
113    let mantissa = alaw & QUANT_MASK;
114    let mut sample = ((mantissa as i32) << 4) + 8;
115    if exponent != 0 {
116        sample = (sample + 0x100) << (exponent - 1);
117    }
118    if sign_set {
119        sample as i16
120    } else {
121        -sample as i16
122    }
123}
124
125/// Codec selection for a session. The wire payload-type number
126/// (`0`/`8`) is the canonical identifier; this enum is the typed
127/// version we pass around in code.
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum G711Codec {
130    /// μ-law (G.711U) — North America / Japan default, RTP payload type `0`.
131    Pcmu,
132    /// A-law (G.711A) — Europe / rest-of-world default, RTP payload type `8`.
133    Pcma,
134}
135
136impl G711Codec {
137    /// The static RTP payload-type number for this codec — `0` for PCMU,
138    /// `8` for PCMA (RFC 3551 §6).
139    pub fn payload_type(self) -> u8 {
140        match self {
141            G711Codec::Pcmu => PCMU_PAYLOAD_TYPE,
142            G711Codec::Pcma => PCMA_PAYLOAD_TYPE,
143        }
144    }
145
146    /// Resolve from a SIP/RTP payload type number. Returns `None` for
147    /// any non-G.711 payload type — the caller decides whether to fall
148    /// through (e.g. accept anyway, ask for re-INVITE, reject).
149    pub fn from_payload_type(pt: u8) -> Option<Self> {
150        match pt {
151            PCMU_PAYLOAD_TYPE => Some(G711Codec::Pcmu),
152            PCMA_PAYLOAD_TYPE => Some(G711Codec::Pcma),
153            _ => None,
154        }
155    }
156
157    /// Encode a slice of 16-bit PCM samples into G.711 bytes, one byte
158    /// per sample. Appends to `out`.
159    pub fn encode(self, pcm: &[i16], out: &mut Vec<u8>) {
160        out.reserve(pcm.len());
161        match self {
162            G711Codec::Pcmu => out.extend(pcm.iter().map(|&s| linear_to_ulaw(s))),
163            G711Codec::Pcma => out.extend(pcm.iter().map(|&s| linear_to_alaw(s))),
164        }
165    }
166
167    /// Decode G.711 bytes into 16-bit PCM samples, one sample per byte.
168    /// Appends to `out`.
169    pub fn decode(self, encoded: &[u8], out: &mut Vec<i16>) {
170        out.reserve(encoded.len());
171        match self {
172            G711Codec::Pcmu => out.extend(encoded.iter().map(|&b| ulaw_to_linear(b))),
173            G711Codec::Pcma => out.extend(encoded.iter().map(|&b| alaw_to_linear(b))),
174        }
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn ulaw_round_trip_silence() {
184        assert_eq!(linear_to_ulaw(0), 0xFF);
185        // μ-law silence (0xFF) decodes to a small non-zero residue — the
186        // codec is not loss-free near zero. The residue should round back
187        // to 0xFF on re-encode, which is the property that matters for
188        // end-to-end stability.
189        let s = ulaw_to_linear(0xFF);
190        assert_eq!(linear_to_ulaw(s), 0xFF);
191    }
192
193    #[test]
194    fn alaw_round_trip_silence() {
195        let encoded = linear_to_alaw(0);
196        let s = alaw_to_linear(encoded);
197        assert_eq!(linear_to_alaw(s), encoded);
198    }
199
200    #[test]
201    fn ulaw_handles_full_scale() {
202        assert_eq!(linear_to_ulaw(i16::MAX), 0x80);
203        assert_eq!(linear_to_ulaw(i16::MIN), 0x00);
204    }
205
206    #[test]
207    fn alaw_handles_full_scale() {
208        assert_eq!(linear_to_alaw(i16::MAX), 0xD5 ^ 0x7F);
209        assert_eq!(linear_to_alaw(i16::MIN), 0x55 ^ 0x7F);
210    }
211
212    #[test]
213    fn codec_encode_decode_length_matches_samples() {
214        let pcm: Vec<i16> = (0..160).map(|i| (i * 200) as i16).collect();
215        let mut encoded = Vec::new();
216        G711Codec::Pcmu.encode(&pcm, &mut encoded);
217        assert_eq!(encoded.len(), pcm.len());
218        let mut decoded = Vec::new();
219        G711Codec::Pcmu.decode(&encoded, &mut decoded);
220        assert_eq!(decoded.len(), encoded.len());
221    }
222
223    #[test]
224    fn payload_type_round_trips() {
225        assert_eq!(G711Codec::from_payload_type(0), Some(G711Codec::Pcmu));
226        assert_eq!(G711Codec::from_payload_type(8), Some(G711Codec::Pcma));
227        assert_eq!(G711Codec::from_payload_type(127), None);
228        assert_eq!(G711Codec::Pcmu.payload_type(), 0);
229        assert_eq!(G711Codec::Pcma.payload_type(), 8);
230    }
231
232    #[test]
233    fn ulaw_round_trip_preserves_loud_samples_within_codec_step() {
234        let inputs: &[i16] = &[1000, -1000, 8000, -8000, 16000, -16000];
235        for &s in inputs {
236            let encoded = linear_to_ulaw(s);
237            let decoded = ulaw_to_linear(encoded);
238            let diff = (s as i32 - decoded as i32).abs();
239            assert!(
240                diff < 400,
241                "μ-law round-trip drift too large: {s} → {decoded} (Δ={diff})"
242            );
243        }
244    }
245
246    #[test]
247    fn alaw_round_trip_preserves_loud_samples_within_codec_step() {
248        let inputs: &[i16] = &[1000, -1000, 8000, -8000, 16000, -16000];
249        for &s in inputs {
250            let encoded = linear_to_alaw(s);
251            let decoded = alaw_to_linear(encoded);
252            let diff = (s as i32 - decoded as i32).abs();
253            assert!(
254                diff < 400,
255                "A-law round-trip drift too large: {s} → {decoded} (Δ={diff})"
256            );
257        }
258    }
259
260    #[test]
261    fn ulaw_decode_is_a_fixed_point_for_every_codeword() {
262        // The right invariant: a decoded sample is the canonical form
263        // of its codeword, so decode(encode(decode(b))) must equal
264        // decode(b) for every codeword. The weaker variant
265        // (encode→decode→encode == encode for every i16) fails near
266        // zero because μ-law has separate +0/-0 codewords that
267        // collapse to the same decoded sample; that's a property of
268        // the codec, not a bug.
269        for b in 0u8..=255 {
270            let mid = ulaw_to_linear(b);
271            let again = ulaw_to_linear(linear_to_ulaw(mid));
272            assert_eq!(again, mid, "μ-law decode not fixed-point at {b:#x}");
273        }
274    }
275
276    #[test]
277    fn alaw_decode_is_a_fixed_point_for_every_codeword() {
278        for b in 0u8..=255 {
279            let mid = alaw_to_linear(b);
280            let again = alaw_to_linear(linear_to_alaw(mid));
281            assert_eq!(again, mid, "A-law decode not fixed-point at {b:#x}");
282        }
283    }
284
285    #[test]
286    fn ulaw_decode_covers_full_codeword_space_without_panic() {
287        // 256 possible codewords. Decoding all of them must not panic
288        // and must stay in i16 range.
289        for b in 0u8..=255 {
290            let _ = ulaw_to_linear(b);
291        }
292    }
293
294    #[test]
295    fn alaw_decode_covers_full_codeword_space_without_panic() {
296        for b in 0u8..=255 {
297            let _ = alaw_to_linear(b);
298        }
299    }
300
301    #[test]
302    fn ulaw_zero_is_distinct_from_full_scale() {
303        // Sanity: a non-trivial codec maps 0 and i16::MAX to different
304        // bytes. Guards against a stub impl that returns a constant.
305        assert_ne!(linear_to_ulaw(0), linear_to_ulaw(i16::MAX));
306        assert_ne!(linear_to_ulaw(0), linear_to_ulaw(i16::MIN));
307    }
308
309    #[test]
310    fn alaw_zero_is_distinct_from_full_scale() {
311        assert_ne!(linear_to_alaw(0), linear_to_alaw(i16::MAX));
312        assert_ne!(linear_to_alaw(0), linear_to_alaw(i16::MIN));
313    }
314
315    #[test]
316    fn pcmu_and_pcma_produce_different_bytes_for_the_same_input() {
317        // Guards against accidentally aliasing the two paths (e.g. a
318        // typo wiring Pcma to linear_to_ulaw). PCMU and PCMA share the
319        // shape but pick different quantisation tables and silence
320        // codewords; they should not match on a non-trivial sample.
321        let s = 12345i16;
322        assert_ne!(linear_to_ulaw(s), linear_to_alaw(s));
323    }
324
325    #[test]
326    fn codec_enum_dispatches_to_the_right_path() {
327        // Crossing the enum boundary must end up at the matching
328        // function — not swapped, not aliased.
329        let pcm = vec![1000i16, -2000, 3000];
330
331        let mut a = Vec::new();
332        G711Codec::Pcmu.encode(&pcm, &mut a);
333        let mut b = Vec::new();
334        for &s in &pcm {
335            b.push(linear_to_ulaw(s));
336        }
337        assert_eq!(a, b);
338
339        let mut c = Vec::new();
340        G711Codec::Pcma.encode(&pcm, &mut c);
341        let mut d = Vec::new();
342        for &s in &pcm {
343            d.push(linear_to_alaw(s));
344        }
345        assert_eq!(c, d);
346    }
347
348    #[test]
349    fn slice_encode_then_decode_recovers_signal_within_codec_drift() {
350        // Twenty-millisecond G.711 frame of a small sine — encode,
351        // decode, and compare against the input. The codec is lossy
352        // (log-PCM quantisation), so we allow a per-sample drift, but
353        // the average error must be small for an "audible" path.
354        let samples: Vec<i16> = (0..G711_FRAME_SAMPLES)
355            .map(|i| {
356                let t = i as f32 / G711_SAMPLE_RATE as f32;
357                ((t * 440.0 * 2.0 * std::f32::consts::PI).sin() * 8000.0) as i16
358            })
359            .collect();
360
361        for codec in [G711Codec::Pcmu, G711Codec::Pcma] {
362            let mut encoded = Vec::new();
363            codec.encode(&samples, &mut encoded);
364            assert_eq!(encoded.len(), G711_FRAME_SAMPLES);
365
366            let mut decoded = Vec::new();
367            codec.decode(&encoded, &mut decoded);
368            assert_eq!(decoded.len(), G711_FRAME_SAMPLES);
369
370            let mean_abs_error: f64 = samples
371                .iter()
372                .zip(decoded.iter())
373                .map(|(a, b)| (*a as i32 - *b as i32).abs() as f64)
374                .sum::<f64>()
375                / samples.len() as f64;
376            // 200 i16 units ≈ 0.6% of full scale — comfortably below
377            // perceptible degradation for telephony.
378            assert!(
379                mean_abs_error < 200.0,
380                "{codec:?}: mean abs error {mean_abs_error} too high"
381            );
382        }
383    }
384
385    #[test]
386    fn encode_appends_rather_than_replacing() {
387        // The slice-level encode/decode take `&mut Vec<…>` and append.
388        // Verifying that explicitly so callers can reuse buffers
389        // across RTP packets without per-packet alloc.
390        let mut buf = vec![0xFFu8, 0xFEu8];
391        let pcm = vec![0i16; 3];
392        G711Codec::Pcmu.encode(&pcm, &mut buf);
393        assert_eq!(buf.len(), 5);
394        assert_eq!(&buf[..2], &[0xFF, 0xFE]);
395    }
396
397    #[test]
398    fn payload_type_constants_match_rfc3551() {
399        // RFC 3551 §6 pins PCMU=0 and PCMA=8. Hard-coding these
400        // numbers in tests protects against a casual rename that would
401        // silently break SDP negotiation against any real PBX.
402        assert_eq!(PCMU_PAYLOAD_TYPE, 0);
403        assert_eq!(PCMA_PAYLOAD_TYPE, 8);
404        assert_eq!(G711_SAMPLE_RATE, 8000);
405        assert_eq!(G711_FRAME_SAMPLES, 160);
406    }
407}