Skip to main content

oxideav_g711/
lib.rs

1//! ITU-T G.711 µ-law / A-law PCM codec.
2//!
3//! G.711 is the classic PSTN audio codec — 8 kHz mono, 8-bit samples that
4//! round-trip to ~13-bit linear quality via a logarithmic companding curve.
5//! Two variants, selected by codec id:
6//!
7//! - **µ-law** (North America / Japan): codec ids `pcm_mulaw`, `ulaw`,
8//!   `g711u`.
9//! - **A-law** (rest of the world): codec ids `pcm_alaw`, `alaw`, `g711a`.
10//!
11//! Both variants are **byte-for-sample**: one encoded byte on input
12//! yields one S16 PCM sample on output, and vice versa. The spec defines
13//! G.711 at 8 kHz mono but the implementation is stateless per sample and
14//! therefore works at any sample rate and any interleaved channel count
15//! the caller provides.
16//!
17//! # Algorithm
18//!
19//! Decoding uses a compile-time 256-entry lookup table generated from the
20//! ITU-T G.711 bit layout (see [`tables`]). Encoding is arithmetic: sign
21//! extraction → bias + segment search → mantissa extraction → on-wire
22//! inversion. There is no signal processing state, so each byte /sample
23//! is independent and packets may be arbitrary-length.
24//!
25//! # Registration
26//!
27//! [`register`] wires up both laws under each of their aliases via
28//! `CodecRegistry::register_both` — i.e. `pcm_mulaw`, `ulaw`, and `g711u`
29//! all resolve to the same [`mulaw::UlawDecoder`] / [`mulaw::UlawEncoder`]
30//! pair, and likewise for A-law.
31
32#![deny(unsafe_code)]
33#![allow(clippy::needless_range_loop)]
34
35pub mod alaw;
36pub mod mulaw;
37pub mod tables;
38
39use oxideav_core::{CodecCapabilities, CodecId, CodecTag};
40use oxideav_core::{CodecInfo, CodecRegistry};
41
42/// Canonical codec id for µ-law (matches FFmpeg's `pcm_mulaw`).
43pub const CODEC_ID_MULAW: &str = "pcm_mulaw";
44
45/// Canonical codec id for A-law (matches FFmpeg's `pcm_alaw`).
46pub const CODEC_ID_ALAW: &str = "pcm_alaw";
47
48/// Aliases that resolve to the µ-law implementation.
49pub const MULAW_ALIASES: &[&str] = &["pcm_mulaw", "ulaw", "g711u"];
50
51/// Aliases that resolve to the A-law implementation.
52pub const ALAW_ALIASES: &[&str] = &["pcm_alaw", "alaw", "g711a"];
53
54/// Register every G.711 codec id + alias for both decode and encode.
55pub fn register(reg: &mut CodecRegistry) {
56    // µ-law: one registration per alias so calls with any of them resolve
57    // cleanly. The canonical alias carries the WAVEFORMATEX tag claim.
58    for (idx, alias) in MULAW_ALIASES.iter().enumerate() {
59        let caps = CodecCapabilities::audio("g711_mulaw_sw")
60            .with_lossy(true)
61            .with_intra_only(true);
62        let mut info = CodecInfo::new(CodecId::new(*alias))
63            .capabilities(caps)
64            .decoder(mulaw::make_decoder)
65            .encoder(mulaw::make_encoder);
66        if idx == 0 {
67            // WAVE_FORMAT_MULAW = 0x0007 — attach to the canonical alias.
68            info = info.tag(CodecTag::wave_format(0x0007));
69        }
70        reg.register(info);
71    }
72
73    // A-law: same story.
74    for (idx, alias) in ALAW_ALIASES.iter().enumerate() {
75        let caps = CodecCapabilities::audio("g711_alaw_sw")
76            .with_lossy(true)
77            .with_intra_only(true);
78        let mut info = CodecInfo::new(CodecId::new(*alias))
79            .capabilities(caps)
80            .decoder(alaw::make_decoder)
81            .encoder(alaw::make_encoder);
82        if idx == 0 {
83            // WAVE_FORMAT_ALAW = 0x0006 — attach to the canonical alias.
84            info = info.tag(CodecTag::wave_format(0x0006));
85        }
86        reg.register(info);
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use oxideav_core::{CodecParameters, Frame, Packet, SampleFormat, TimeBase};
94
95    fn params(id: &str) -> CodecParameters {
96        let mut p = CodecParameters::audio(CodecId::new(id));
97        p.sample_rate = Some(8_000);
98        p.channels = Some(1);
99        p.sample_format = Some(SampleFormat::S16);
100        p
101    }
102
103    #[test]
104    fn register_all_aliases() {
105        let mut reg = CodecRegistry::new();
106        register(&mut reg);
107        for alias in MULAW_ALIASES.iter().chain(ALAW_ALIASES.iter()) {
108            let id = CodecId::new(*alias);
109            assert!(reg.has_decoder(&id), "no decoder for alias {alias}");
110            assert!(reg.has_encoder(&id), "no encoder for alias {alias}");
111        }
112    }
113
114    #[test]
115    fn mulaw_aliases_resolve_to_same_impl() {
116        let mut reg = CodecRegistry::new();
117        register(&mut reg);
118        // Build a decoder for every alias and feed it the same byte. All
119        // must produce the same S16 result.
120        let input = vec![0x55u8, 0xAA, 0x80, 0x00];
121        let mut results = Vec::new();
122        for alias in MULAW_ALIASES {
123            let p = params(alias);
124            let mut dec = reg.make_decoder(&p).expect("make_decoder");
125            let pkt = Packet::new(0, TimeBase::new(1, 8_000), input.clone());
126            dec.send_packet(&pkt).unwrap();
127            let Frame::Audio(af) = dec.receive_frame().unwrap() else {
128                panic!("expected audio frame");
129            };
130            results.push(af.data[0].clone());
131        }
132        for r in &results[1..] {
133            assert_eq!(r, &results[0]);
134        }
135    }
136
137    #[test]
138    fn mulaw_roundtrip_samples() {
139        let mut reg = CodecRegistry::new();
140        register(&mut reg);
141        let p = params(CODEC_ID_MULAW);
142        let mut enc = reg.make_encoder(&p).expect("make_encoder");
143        let mut dec = reg.make_decoder(&p).expect("make_decoder");
144
145        let samples: Vec<i16> = vec![0, 1, -1, 100, -100, 10000, -10000, 32000, -32000];
146        let mut pcm_bytes = Vec::with_capacity(samples.len() * 2);
147        for &s in &samples {
148            pcm_bytes.extend_from_slice(&s.to_le_bytes());
149        }
150        let input = Frame::Audio(oxideav_core::AudioFrame {
151            format: SampleFormat::S16,
152            channels: 1,
153            sample_rate: 8_000,
154            samples: samples.len() as u32,
155            pts: Some(0),
156            time_base: TimeBase::new(1, 8_000),
157            data: vec![pcm_bytes],
158        });
159        enc.send_frame(&input).unwrap();
160        let pkt = enc.receive_packet().unwrap();
161        assert_eq!(pkt.data.len(), samples.len());
162        dec.send_packet(&pkt).unwrap();
163        let Frame::Audio(af) = dec.receive_frame().unwrap() else {
164            panic!("expected audio frame");
165        };
166        assert_eq!(af.samples as usize, samples.len());
167    }
168}