Skip to main content

sipx_audio/
g711.rs

1//! G.711 µ-law and A-law (ITU-T G.711).
2//!
3//! Two logarithmic 8-bit codecs, both ancient and both still the only thing every endpoint on
4//! earth agrees on. The compression is a piecewise-linear approximation of a logarithm: a sign
5//! bit, a 3-bit exponent and a 4-bit mantissa.
6//!
7//! What makes these worth testing carefully rather than round-tripping: the round trip is
8//! *lossy by design*, so "encode then decode gives back the input" is false for almost every
9//! input and cannot be the test. The reference values below come from the ITU algorithm, and
10//! what the round trip does guarantee is that a decoded value re-encodes to the same code —
11//! the codec is idempotent on its own output, which is the property that matters when audio
12//! passes through more than one hop.
13
14const ULAW_BIAS: i32 = 0x84;
15const ULAW_CLIP: i32 = 32_635;
16const ALAW_CLIP: i32 = 32_635;
17
18/// Encode one sample to µ-law.
19#[must_use]
20pub fn ulaw_encode(sample: i16) -> u8 {
21    let mut pcm = i32::from(sample);
22    let sign = if pcm < 0 { 0x80 } else { 0x00 };
23    if pcm < 0 {
24        pcm = -pcm;
25    }
26    pcm = pcm.min(ULAW_CLIP);
27    pcm += ULAW_BIAS;
28
29    let mut exponent = 7i32;
30    let mut mask = 0x4000i32;
31    while exponent > 0 && (pcm & mask) == 0 {
32        exponent -= 1;
33        mask >>= 1;
34    }
35    let mantissa = (pcm >> (exponent + 3)) & 0x0F;
36    let code = sign | (exponent << 4) | mantissa;
37    // The complement is not decoration: it puts the most common values (near silence) at codes
38    // with many 1 bits, which survive a lost bit better on the analogue lines this was
39    // designed for.
40    u8::try_from(!code & 0xFF).unwrap_or(0)
41}
42
43/// Decode one µ-law code.
44#[must_use]
45pub fn ulaw_decode(code: u8) -> i16 {
46    let code = i32::from(!code);
47    let sign = code & 0x80;
48    let exponent = (code >> 4) & 0x07;
49    let mantissa = code & 0x0F;
50    let mut sample = ((mantissa << 3) + ULAW_BIAS) << exponent;
51    sample -= ULAW_BIAS;
52    let sample = if sign != 0 { -sample } else { sample };
53    i16::try_from(sample.clamp(i32::from(i16::MIN), i32::from(i16::MAX))).unwrap_or(0)
54}
55
56/// Encode one sample to A-law.
57#[must_use]
58pub fn alaw_encode(sample: i16) -> u8 {
59    let mut pcm = i32::from(sample);
60    // A-law's sign convention is the opposite of µ-law's, and the negative branch subtracts
61    // one. Both are easy to "fix" into something that sounds almost right and is wrong.
62    let sign = if pcm >= 0 { 0x80 } else { 0x00 };
63    if pcm < 0 {
64        pcm = -pcm - 1;
65    }
66    pcm = pcm.min(ALAW_CLIP);
67
68    let code = if pcm < 256 {
69        pcm >> 4
70    } else {
71        let mut exponent = 7i32;
72        let mut mask = 0x4000i32;
73        while exponent > 0 && (pcm & mask) == 0 {
74            exponent -= 1;
75            mask >>= 1;
76        }
77        let mantissa = (pcm >> (exponent + 3)) & 0x0F;
78        (exponent << 4) | mantissa
79    };
80    // The 0x55 toggle spreads the alternating bit pattern that keeps a line's clock recovery
81    // happy during silence.
82    u8::try_from((code ^ sign ^ 0x55) & 0xFF).unwrap_or(0)
83}
84
85/// Decode one A-law code.
86#[must_use]
87pub fn alaw_decode(code: u8) -> i16 {
88    let code = i32::from(code ^ 0x55);
89    let sign = code & 0x80;
90    let exponent = (code >> 4) & 0x07;
91    let mantissa = code & 0x0F;
92    let sample = if exponent == 0 {
93        (mantissa << 4) + 8
94    } else {
95        ((mantissa << 4) + 0x108) << (exponent - 1)
96    };
97    let sample = if sign != 0 { sample } else { -sample };
98    i16::try_from(sample.clamp(i32::from(i16::MIN), i32::from(i16::MAX))).unwrap_or(0)
99}
100
101/// Encode a buffer of samples to µ-law.
102#[must_use]
103pub fn ulaw_encode_all(samples: &[i16]) -> Vec<u8> {
104    samples.iter().copied().map(ulaw_encode).collect()
105}
106
107/// Decode a buffer of µ-law.
108#[must_use]
109pub fn ulaw_decode_all(codes: &[u8]) -> Vec<i16> {
110    codes.iter().copied().map(ulaw_decode).collect()
111}
112
113/// Encode a buffer of samples to A-law.
114#[must_use]
115pub fn alaw_encode_all(samples: &[i16]) -> Vec<u8> {
116    samples.iter().copied().map(alaw_encode).collect()
117}
118
119/// Decode a buffer of A-law.
120#[must_use]
121pub fn alaw_decode_all(codes: &[u8]) -> Vec<i16> {
122    codes.iter().copied().map(alaw_decode).collect()
123}
124
125#[cfg(test)]
126#[allow(
127    clippy::unwrap_used,
128    clippy::expect_used,
129    clippy::panic,
130    clippy::indexing_slicing
131)]
132mod tests {
133    use super::*;
134
135    /// Values from the ITU-T G.711 algorithm, computed independently of this implementation.
136    /// A codec checked only by round-tripping proves its two halves agree with each other,
137    /// not that either is right — and two halves that are wrong in mirrored ways round-trip
138    /// perfectly while interoperating with nothing.
139    #[test]
140    fn ulaw_matches_the_itu_reference_table() {
141        const REFERENCE: &[(i16, u8)] = &[
142            (0, 255),
143            (1, 255),
144            (-1, 127),
145            (100, 242),
146            (-100, 114),
147            (1000, 206),
148            (-1000, 78),
149            (8000, 160),
150            (-8000, 32),
151            (32767, 128),
152            (-32768, 0),
153            (4096, 175),
154            (-4096, 47),
155        ];
156        for &(sample, expected) in REFERENCE {
157            assert_eq!(ulaw_encode(sample), expected, "µ-law encoding of {sample}");
158        }
159    }
160
161    #[test]
162    fn alaw_matches_the_itu_reference_table() {
163        const REFERENCE: &[(i16, u8)] = &[
164            (0, 213),
165            (1, 213),
166            (-1, 85),
167            (100, 211),
168            (-100, 83),
169            (1000, 250),
170            (-1000, 122),
171            (8000, 138),
172            (-8000, 10),
173            (32767, 170),
174            (-32768, 42),
175            (4096, 133),
176            (-4096, 26),
177        ];
178        for &(sample, expected) in REFERENCE {
179            assert_eq!(alaw_encode(sample), expected, "A-law encoding of {sample}");
180        }
181    }
182
183    #[test]
184    fn ulaw_decoding_matches_the_reference() {
185        assert_eq!(ulaw_decode(0xFF), 0);
186        assert_eq!(ulaw_decode(0x7F), 0);
187        assert_eq!(ulaw_decode(0x00), -32_124);
188        assert_eq!(ulaw_decode(0x80), 32_124);
189    }
190
191    #[test]
192    fn alaw_decoding_matches_the_reference() {
193        assert_eq!(alaw_decode(0xD5), 8);
194        assert_eq!(alaw_decode(0x55), -8);
195        assert_eq!(alaw_decode(0x2A), -32_256);
196        assert_eq!(alaw_decode(0xAA), 32_256);
197    }
198
199    /// Both codecs are symmetric about zero: the codes as a whole sum to nothing. A sign-handling
200    /// bug on one side shows up here even when the individual values look plausible.
201    #[test]
202    fn the_codec_is_symmetric_about_zero() {
203        let ulaw_sum: i64 = (0..=255u8).map(|c| i64::from(ulaw_decode(c))).sum();
204        let alaw_sum: i64 = (0..=255u8).map(|c| i64::from(alaw_decode(c))).sum();
205        assert_eq!(ulaw_sum, 0, "µ-law is not symmetric");
206        assert_eq!(alaw_sum, 0, "A-law is not symmetric");
207    }
208
209    /// The round trip is lossy by design, so this is the property that actually holds: a
210    /// decoded value re-encodes to the code it came from. Without it, audio passing through
211    /// two hops would degrade at every one.
212    ///
213    /// With exactly one exception, which is a property of µ-law rather than of this code.
214    /// µ-law has two representations of zero — code 255 is +0 and code 127 is −0 — and both
215    /// decode to the same sample, so the encoder has to pick one. Every other code, in both
216    /// codecs, is idempotent; A-law has no such pair.
217    #[test]
218    fn decoding_then_encoding_returns_the_same_code() {
219        const ULAW_NEGATIVE_ZERO: u8 = 127;
220
221        for code in 0..=255u8 {
222            if code == ULAW_NEGATIVE_ZERO {
223                assert_eq!(ulaw_decode(code), 0, "code 127 is µ-law's negative zero");
224                assert_eq!(
225                    ulaw_encode(ulaw_decode(code)),
226                    255,
227                    "and it normalises to positive zero"
228                );
229            } else {
230                assert_eq!(
231                    ulaw_encode(ulaw_decode(code)),
232                    code,
233                    "µ-law is not idempotent at code {code}"
234                );
235            }
236
237            assert_eq!(
238                alaw_encode(alaw_decode(code)),
239                code,
240                "A-law is not idempotent at code {code}"
241            );
242        }
243    }
244
245    /// Quantisation error must stay within the codec's step size. A wrong exponent produces
246    /// values that are close for small samples and wildly off for large ones, which this
247    /// catches and a spot check would not.
248    #[test]
249    fn quantisation_error_stays_within_the_step_size() {
250        for sample in (-32_768..=32_767).step_by(37) {
251            let sample = i16::try_from(sample).expect("in range");
252            let error = (i32::from(ulaw_decode(ulaw_encode(sample))) - i32::from(sample)).abs();
253            let allowed = (i32::from(sample).abs() >> 5).max(16);
254            assert!(
255                error <= allowed,
256                "µ-law error {error} at sample {sample} exceeds {allowed}"
257            );
258        }
259    }
260
261    /// Values beyond what the codec can represent must clip, not wrap. Wrapping turns a loud
262    /// sound into a loud sound of the opposite sign, which is heard as a click.
263    #[test]
264    fn loud_samples_clip_rather_than_wrapping() {
265        assert_eq!(ulaw_encode(32_767), ulaw_encode(32_000));
266        assert!(ulaw_decode(ulaw_encode(32_767)) > 30_000);
267        assert!(ulaw_decode(ulaw_encode(-32_768)) < -30_000);
268        assert!(alaw_decode(alaw_encode(32_767)) > 30_000);
269        assert!(alaw_decode(alaw_encode(-32_768)) < -30_000);
270    }
271
272    #[test]
273    fn buffers_encode_and_decode_as_wholes() {
274        let samples: Vec<i16> = (0..160).map(|i| i * 100 - 8000).collect();
275        let encoded = ulaw_encode_all(&samples);
276        assert_eq!(encoded.len(), samples.len());
277        let decoded = ulaw_decode_all(&encoded);
278        assert_eq!(decoded.len(), samples.len());
279        assert_eq!(
280            ulaw_encode_all(&decoded),
281            encoded,
282            "idempotent over a buffer"
283        );
284    }
285}