gemini_adk_fluent_rs/telephony/
g711.rs

1//! G.711 μ-law and A-law codecs — the audio dialect of the public phone
2//! network.
3//!
4//! Every PSTN leg (Twilio Media Streams, SIP trunks, carrier gateways)
5//! delivers 8 kHz audio in one of these two companding formats: μ-law in
6//! North America and Japan, A-law nearly everywhere else. Both pack a
7//! 14/13-bit linear sample into one logarithmically-companded byte.
8//!
9//! The functions here are pure, allocation-explicit, and implement ITU-T
10//! G.711 directly (bit manipulation, no tables to drift): encode from mono
11//! PCM16, decode back to mono PCM16. Pair them with
12//! [`resample`](crate::voice::resample) to move between the 8 kHz telephone
13//! rate and the Live API's 16 kHz-in / 24 kHz-out contract.
14
15/// Encode one linear PCM16 sample as a μ-law byte (ITU-T G.711).
16pub fn linear_to_ulaw(sample: i16) -> u8 {
17    const BIAS: i32 = 0x84; // 132: shifts the segment boundaries per G.711
18    const CLIP: i32 = 32_635;
19
20    let sign: u8 = if sample < 0 { 0x80 } else { 0 };
21    let mut magnitude = (sample as i32).abs().min(CLIP) + BIAS;
22
23    // Segment: how far the magnitude's top bit sits above the base segment
24    // (segment 0 spans biased magnitudes up to 0xFF).
25    let mut segment: u8 = 0;
26    let mut probe = magnitude >> 8;
27    while probe > 0 && segment < 7 {
28        segment += 1;
29        probe >>= 1;
30    }
31
32    magnitude >>= segment + 3;
33    let mantissa = (magnitude & 0x0F) as u8;
34    // μ-law transmits the byte inverted (all-ones is silence on the wire).
35    !(sign | (segment << 4) | mantissa)
36}
37
38/// Decode one μ-law byte to a linear PCM16 sample (ITU-T G.711).
39pub fn ulaw_to_linear(byte: u8) -> i16 {
40    let byte = !byte;
41    let sign = byte & 0x80;
42    let segment = (byte >> 4) & 0x07;
43    let mantissa = byte & 0x0F;
44
45    let magnitude = ((((mantissa as i32) << 3) + 0x84) << segment) - 0x84;
46    if sign != 0 {
47        -magnitude as i16
48    } else {
49        magnitude as i16
50    }
51}
52
53/// Encode one linear PCM16 sample as an A-law byte (ITU-T G.711).
54pub fn linear_to_alaw(sample: i16) -> u8 {
55    const CLIP: i32 = 32_635;
56
57    let sign: u8 = if sample >= 0 { 0x80 } else { 0 };
58    let magnitude = (sample as i32).abs().min(CLIP);
59
60    let compressed = if magnitude >= 256 {
61        let mut segment: u8 = 1;
62        let mut probe = magnitude >> 9;
63        while probe > 0 && segment < 7 {
64            segment += 1;
65            probe >>= 1;
66        }
67        let mantissa = ((magnitude >> (segment + 3)) & 0x0F) as u8;
68        (segment << 4) | mantissa
69    } else {
70        (magnitude >> 4) as u8
71    };
72
73    // A-law XORs alternate bits on the wire.
74    (sign | compressed) ^ 0x55
75}
76
77/// Decode one A-law byte to a linear PCM16 sample (ITU-T G.711).
78pub fn alaw_to_linear(byte: u8) -> i16 {
79    let byte = byte ^ 0x55;
80    let sign = byte & 0x80;
81    let segment = (byte >> 4) & 0x07;
82    let mantissa = (byte & 0x0F) as i32;
83
84    let magnitude = match segment {
85        0 => (mantissa << 4) + 8,
86        _ => ((mantissa << 4) + 0x108) << (segment - 1),
87    };
88    if sign != 0 {
89        magnitude as i16
90    } else {
91        -magnitude as i16
92    }
93}
94
95/// Decode a μ-law byte stream to mono PCM16 samples.
96pub fn decode_ulaw(bytes: &[u8]) -> Vec<i16> {
97    bytes.iter().map(|&b| ulaw_to_linear(b)).collect()
98}
99
100/// Encode mono PCM16 samples as a μ-law byte stream.
101pub fn encode_ulaw(samples: &[i16]) -> Vec<u8> {
102    samples.iter().map(|&s| linear_to_ulaw(s)).collect()
103}
104
105/// Decode an A-law byte stream to mono PCM16 samples.
106pub fn decode_alaw(bytes: &[u8]) -> Vec<i16> {
107    bytes.iter().map(|&b| alaw_to_linear(b)).collect()
108}
109
110/// Encode mono PCM16 samples as an A-law byte stream.
111pub fn encode_alaw(samples: &[i16]) -> Vec<u8> {
112    samples.iter().map(|&s| linear_to_alaw(s)).collect()
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn ulaw_silence_is_all_ones_on_the_wire() {
121        // The inverted encoding makes digital silence 0xFF — the classic
122        // "μ-law idle pattern".
123        assert_eq!(linear_to_ulaw(0), 0xFF);
124        assert_eq!(ulaw_to_linear(0xFF), 0);
125    }
126
127    #[test]
128    fn ulaw_known_extremes() {
129        // Full-scale positive clips to the largest positive code.
130        assert_eq!(ulaw_to_linear(linear_to_ulaw(32_767)), 32_124);
131        assert_eq!(ulaw_to_linear(linear_to_ulaw(-32_768)), -32_124);
132    }
133
134    #[test]
135    fn ulaw_round_trip_is_within_segment_quantisation() {
136        // Companding is lossy but monotone: error bounded by the segment's
137        // step size (≤ 1/16 of the magnitude, plus the smallest step).
138        for &s in &[0i16, 1, -1, 100, -100, 1000, -1000, 8000, -8000, 30000] {
139            let rt = ulaw_to_linear(linear_to_ulaw(s));
140            let tolerance = (s.unsigned_abs() as i32 / 16).max(16);
141            assert!(
142                ((rt as i32) - (s as i32)).abs() <= tolerance,
143                "sample {s} round-tripped to {rt}"
144            );
145        }
146    }
147
148    #[test]
149    fn ulaw_is_monotone() {
150        // Decoded values must never decrease as input increases — a codec
151        // that reorders amplitudes garbles speech even if errors are small.
152        let mut last = i16::MIN;
153        for s in (-32_768i32..=32_767).step_by(257) {
154            let rt = ulaw_to_linear(linear_to_ulaw(s as i16));
155            assert!(rt >= last, "non-monotone at input {s}");
156            last = rt;
157        }
158    }
159
160    #[test]
161    fn alaw_round_trip_is_within_segment_quantisation() {
162        for &s in &[0i16, 8, -8, 100, -100, 1000, -1000, 8000, -8000, 30000] {
163            let rt = alaw_to_linear(linear_to_alaw(s));
164            let tolerance = (s.unsigned_abs() as i32 / 16).max(24);
165            assert!(
166                ((rt as i32) - (s as i32)).abs() <= tolerance,
167                "sample {s} round-tripped to {rt}"
168            );
169        }
170    }
171
172    #[test]
173    fn alaw_is_monotone() {
174        let mut last = i16::MIN;
175        for s in (-32_768i32..=32_767).step_by(257) {
176            let rt = alaw_to_linear(linear_to_alaw(s as i16));
177            assert!(rt >= last, "non-monotone at input {s}");
178            last = rt;
179        }
180    }
181
182    #[test]
183    fn stream_helpers_round_trip_shape() {
184        let samples = vec![0i16, 500, -500, 12_000, -12_000];
185        assert_eq!(decode_ulaw(&encode_ulaw(&samples)).len(), samples.len());
186        assert_eq!(decode_alaw(&encode_alaw(&samples)).len(), samples.len());
187    }
188
189    #[test]
190    fn ulaw_segment_boundaries() {
191        // Test round-trip accuracy at segment transitions (where step size changes)
192        // Segment boundaries in μ-law are at magnitudes where probe changes (256, 512, 1024, ...)
193        for &boundary in &[256i16, 512, 1024, 2048, 4096, 8192, 16384] {
194            for offset in &[-1i32, 0, 1] {
195                let s = (boundary as i32 + offset) as i16;
196                let rt = ulaw_to_linear(linear_to_ulaw(s));
197                let tolerance = (s.unsigned_abs() as i32 / 16).max(16);
198                assert!(
199                    ((rt as i32) - (s as i32)).abs() <= tolerance,
200                    "sample {s} at boundary round-tripped to {rt}, error exceeds tolerance"
201                );
202            }
203        }
204    }
205
206    #[test]
207    fn alaw_segment_boundaries() {
208        // Test round-trip accuracy at segment transitions in A-law
209        // A-law segment 0 spans magnitude 0-255, then transitions to segment 1
210        for &boundary in &[256i16, 512, 1024, 2048, 4096, 8192, 16384] {
211            for offset in &[-1i32, 0, 1] {
212                let s = (boundary as i32 + offset) as i16;
213                let rt = alaw_to_linear(linear_to_alaw(s));
214                let tolerance = (s.unsigned_abs() as i32 / 16).max(24);
215                assert!(
216                    ((rt as i32) - (s as i32)).abs() <= tolerance,
217                    "alaw sample {s} at boundary round-tripped to {rt}, error exceeds tolerance"
218                );
219            }
220        }
221    }
222
223    #[test]
224    fn ulaw_all_mantissas_per_segment() {
225        // Verify each mantissa value (0-15) in each segment (0-7) codes/decodes correctly
226        for segment in 0u8..=7 {
227            for mantissa in 0u8..=15 {
228                // Reconstruct the encoded byte: !(sign | segment<<4 | mantissa)
229                let encoded = !((segment << 4) | mantissa);
230                let decoded = ulaw_to_linear(encoded);
231                // Re-encode to verify round-trip consistency
232                let re_encoded = linear_to_ulaw(decoded);
233                assert_eq!(
234                    re_encoded, encoded,
235                    "mantissa {mantissa} in segment {segment}: round-trip mismatch"
236                );
237            }
238        }
239    }
240
241    #[test]
242    fn alaw_all_mantissas_per_segment() {
243        // Verify A-law mantissas (0-15) in each segment (0-7) code/decode correctly
244        for segment in 0u8..=7 {
245            for mantissa in 0u8..=15 {
246                // Reconstruct the encoded byte per A-law spec
247                let compressed = (segment << 4) | mantissa;
248                let encoded = (0x80 | compressed) ^ 0x55; // with sign bit + XOR
249                let decoded = alaw_to_linear(encoded);
250                // Re-encode to verify consistency
251                let re_encoded = linear_to_alaw(decoded);
252                assert_eq!(
253                    re_encoded, encoded,
254                    "alaw mantissa {mantissa} in segment {segment}: round-trip mismatch"
255                );
256            }
257        }
258    }
259
260    #[test]
261    fn ulaw_negative_values_symmetric() {
262        // Negative and positive values should have symmetric round-trip errors
263        for &abs_val in &[100i16, 256, 1000, 8000, 20000] {
264            let pos_rt = ulaw_to_linear(linear_to_ulaw(abs_val));
265            let neg_rt = ulaw_to_linear(linear_to_ulaw(-abs_val));
266            assert_eq!(
267                pos_rt as i32 + neg_rt as i32,
268                0,
269                "μ-law should preserve sign symmetry: {pos_rt} + {neg_rt} != 0"
270            );
271        }
272    }
273
274    #[test]
275    fn alaw_negative_values_symmetric() {
276        // A-law should also preserve sign symmetry
277        for &abs_val in &[100i16, 256, 1000, 8000, 20000] {
278            let pos_rt = alaw_to_linear(linear_to_alaw(abs_val));
279            let neg_rt = alaw_to_linear(linear_to_alaw(-abs_val));
280            assert_eq!(
281                pos_rt as i32 + neg_rt as i32,
282                0,
283                "A-law should preserve sign symmetry: {pos_rt} + {neg_rt} != 0"
284            );
285        }
286    }
287
288    #[test]
289    fn ulaw_clipping_at_extremes() {
290        // Values beyond CLIP (32635) should clip to the same rounded value
291        let large_pos = ulaw_to_linear(linear_to_ulaw(32_767));
292        let large_pos2 = ulaw_to_linear(linear_to_ulaw(32_700));
293        assert_eq!(
294            large_pos, large_pos2,
295            "μ-law should clip large positive values to same result"
296        );
297
298        let large_neg = ulaw_to_linear(linear_to_ulaw(-32_768));
299        let large_neg2 = ulaw_to_linear(linear_to_ulaw(-32_700));
300        assert_eq!(
301            large_neg, large_neg2,
302            "μ-law should clip large negative values to same result"
303        );
304    }
305
306    #[test]
307    fn alaw_clipping_at_extremes() {
308        // A-law should also clip consistently
309        let large_pos = alaw_to_linear(linear_to_alaw(32_767));
310        let large_pos2 = alaw_to_linear(linear_to_alaw(32_700));
311        assert_eq!(
312            large_pos, large_pos2,
313            "A-law should clip large positive values to same result"
314        );
315
316        let large_neg = alaw_to_linear(linear_to_alaw(-32_768));
317        let large_neg2 = alaw_to_linear(linear_to_alaw(-32_700));
318        assert_eq!(
319            large_neg, large_neg2,
320            "A-law should clip large negative values to same result"
321        );
322    }
323}