wavekat_core/codec/mod.rs
1//! Telephony / streaming audio codecs.
2//!
3//! Each codec lives in a submodule so the public surface stays
4//! deliberately granular: a consumer that only needs G.711 imports
5//! `wavekat_core::codec::g711`. Future additions (iLBC, …) live beside
6//! them and stay independently selectable.
7//!
8//! Opus is gated behind the `opus` cargo feature because it pulls a C
9//! build dependency (vendored `libopus` via `audiopus_sys`); the crate
10//! stays pure-Rust for consumers that don't opt in. The feature also
11//! unlocks [`Encoder`] / [`Decoder`] — the codec-agnostic seam for
12//! consumers that negotiate their codec per call.
13
14pub mod g711;
15#[cfg(feature = "opus")]
16pub mod opus;
17
18#[cfg(feature = "opus")]
19pub use seam::{Decoder, Encoder};
20
21/// The per-call codec seam: one enum per direction, dispatching to the
22/// stateless G.711 tables or a stateful Opus coder. Split from the
23/// submodules so `g711`/`opus` stay usable standalone.
24#[cfg(feature = "opus")]
25mod seam {
26 use super::g711::{G711Codec, G711_FRAME_SAMPLES, G711_SAMPLE_RATE};
27 use super::opus::{OpusDecoder, OpusEncoder, OPUS_FRAME_SAMPLES, OPUS_PCM_SAMPLE_RATE};
28 use crate::CoreError;
29
30 /// The encode half of a negotiated call codec.
31 ///
32 /// G.711 is a stateless table lookup (the enum just carries the
33 /// μ-law/A-law choice); Opus owns a heap-backed encoder with
34 /// inter-frame state. Either way: feed one 20 ms frame at
35 /// [`pcm_sample_rate`](Self::pcm_sample_rate) per call, in order.
36 pub enum Encoder {
37 /// PCMU or PCMA.
38 G711(G711Codec),
39 /// Opus, wideband voice profile.
40 Opus(OpusEncoder),
41 }
42
43 impl Encoder {
44 /// The PCM rate this encoder consumes: 8 kHz for G.711, 16 kHz
45 /// for Opus. Resample the microphone to this, not to a global
46 /// constant.
47 pub fn pcm_sample_rate(&self) -> u32 {
48 match self {
49 Encoder::G711(_) => G711_SAMPLE_RATE,
50 Encoder::Opus(_) => OPUS_PCM_SAMPLE_RATE,
51 }
52 }
53
54 /// Samples in one 20 ms frame at [`pcm_sample_rate`](Self::pcm_sample_rate):
55 /// 160 for G.711, 320 for Opus.
56 pub fn frame_samples(&self) -> usize {
57 match self {
58 Encoder::G711(_) => G711_FRAME_SAMPLES,
59 Encoder::Opus(_) => OPUS_FRAME_SAMPLES,
60 }
61 }
62
63 /// Encode one 20 ms frame, appending the payload bytes to `out`
64 /// and returning the payload length. G.711 cannot fail; Opus
65 /// errors surface (wrong frame length, libopus failure).
66 pub fn encode(&mut self, pcm: &[i16], out: &mut Vec<u8>) -> Result<usize, CoreError> {
67 match self {
68 Encoder::G711(codec) => {
69 codec.encode(pcm, out);
70 Ok(pcm.len())
71 }
72 Encoder::Opus(enc) => enc.encode(pcm, out),
73 }
74 }
75 }
76
77 /// The decode half of a negotiated call codec.
78 ///
79 /// The loss-recovery calls are uniform so a receive loop can drive
80 /// them without codec branches: for G.711 they are no-ops (append
81 /// nothing, return 0) because G.711 carries no redundancy and its
82 /// historical loss behavior — silence by absence — is the correct
83 /// one to keep.
84 pub enum Decoder {
85 /// PCMU or PCMA.
86 G711(G711Codec),
87 /// Opus, with in-band FEC recovery and PLC.
88 Opus(OpusDecoder),
89 }
90
91 impl Decoder {
92 /// The PCM rate decoded frames come out at: 8 kHz for G.711,
93 /// 16 kHz for Opus.
94 pub fn pcm_sample_rate(&self) -> u32 {
95 match self {
96 Decoder::G711(_) => G711_SAMPLE_RATE,
97 Decoder::Opus(_) => OPUS_PCM_SAMPLE_RATE,
98 }
99 }
100
101 /// Decode one received payload, appending PCM to `out` and
102 /// returning the sample count. Use the returned length — Opus
103 /// peers choose their own frame size.
104 pub fn decode(&mut self, payload: &[u8], out: &mut Vec<i16>) -> Result<usize, CoreError> {
105 match self {
106 Decoder::G711(codec) => {
107 codec.decode(payload, out);
108 Ok(payload.len())
109 }
110 Decoder::Opus(dec) => dec.decode(payload, out),
111 }
112 }
113
114 /// Recover a single lost frame from the packet that followed it
115 /// (drive this on a one-packet RTP sequence gap). Opus decodes
116 /// the next packet's in-band FEC; G.711 has nothing to recover
117 /// and appends nothing.
118 pub fn recover_lost(
119 &mut self,
120 next_payload: &[u8],
121 out: &mut Vec<i16>,
122 ) -> Result<usize, CoreError> {
123 match self {
124 Decoder::G711(_) => Ok(0),
125 Decoder::Opus(dec) => dec.decode_fec(next_payload, out),
126 }
127 }
128
129 /// Produce one concealment frame for a packet that is not
130 /// coming back (multi-packet gap). Opus extrapolates via PLC;
131 /// G.711 appends nothing — absence already sounds like silence.
132 pub fn conceal(&mut self, out: &mut Vec<i16>) -> Result<usize, CoreError> {
133 match self {
134 Decoder::G711(_) => Ok(0),
135 Decoder::Opus(dec) => dec.conceal(out),
136 }
137 }
138 }
139
140 #[cfg(test)]
141 mod tests {
142 use super::*;
143
144 fn tone(n: usize, rate: u32) -> Vec<i16> {
145 (0..n)
146 .map(|i| {
147 let t = i as f32 / rate as f32;
148 ((t * 440.0 * 2.0 * std::f32::consts::PI).sin() * 8000.0) as i16
149 })
150 .collect()
151 }
152
153 #[test]
154 fn g711_seam_matches_direct_codec() {
155 // The enum must dispatch to the same tables as calling
156 // G711Codec directly — not a re-implementation.
157 let pcm = tone(G711_FRAME_SAMPLES, G711_SAMPLE_RATE);
158 let mut via_seam = Vec::new();
159 Encoder::G711(G711Codec::Pcmu)
160 .encode(&pcm, &mut via_seam)
161 .unwrap();
162 let mut direct = Vec::new();
163 G711Codec::Pcmu.encode(&pcm, &mut direct);
164 assert_eq!(via_seam, direct);
165
166 let mut decoded = Vec::new();
167 let n = Decoder::G711(G711Codec::Pcmu)
168 .decode(&via_seam, &mut decoded)
169 .unwrap();
170 assert_eq!(n, G711_FRAME_SAMPLES);
171 }
172
173 #[test]
174 fn seam_reports_per_codec_rates_and_frames() {
175 let g711 = Encoder::G711(G711Codec::Pcma);
176 assert_eq!(g711.pcm_sample_rate(), 8_000);
177 assert_eq!(g711.frame_samples(), 160);
178 let opus = Encoder::Opus(OpusEncoder::new().unwrap());
179 assert_eq!(opus.pcm_sample_rate(), 16_000);
180 assert_eq!(opus.frame_samples(), 320);
181 assert_eq!(Decoder::G711(G711Codec::Pcmu).pcm_sample_rate(), 8_000);
182 assert_eq!(
183 Decoder::Opus(OpusDecoder::new().unwrap()).pcm_sample_rate(),
184 16_000
185 );
186 }
187
188 #[test]
189 fn opus_seam_round_trips() {
190 let mut enc = Encoder::Opus(OpusEncoder::new().unwrap());
191 let mut dec = Decoder::Opus(OpusDecoder::new().unwrap());
192 let pcm = tone(OPUS_FRAME_SAMPLES, OPUS_PCM_SAMPLE_RATE);
193 let mut packet = Vec::new();
194 enc.encode(&pcm, &mut packet).unwrap();
195 let mut out = Vec::new();
196 let n = dec.decode(&packet, &mut out).unwrap();
197 assert_eq!(n, OPUS_FRAME_SAMPLES);
198 }
199
200 #[test]
201 fn g711_recovery_is_a_noop_by_design() {
202 // The receive loop drives recover_lost/conceal without
203 // codec branches; for G.711 both must keep today's
204 // behavior — nothing inserted, loss stays silence.
205 let mut dec = Decoder::G711(G711Codec::Pcmu);
206 let mut out = vec![1i16, 2, 3];
207 assert_eq!(dec.recover_lost(&[0xFF; 20], &mut out).unwrap(), 0);
208 assert_eq!(dec.conceal(&mut out).unwrap(), 0);
209 assert_eq!(out, vec![1, 2, 3], "G.711 recovery must not touch out");
210 }
211
212 #[test]
213 fn opus_seam_recovers_and_conceals() {
214 let mut enc = Encoder::Opus(OpusEncoder::new().unwrap());
215 let mut dec = Decoder::Opus(OpusDecoder::new().unwrap());
216 let mut packets = Vec::new();
217 for i in 0..4 {
218 let pcm: Vec<i16> = (0..OPUS_FRAME_SAMPLES)
219 .map(|j| {
220 let t = (i * OPUS_FRAME_SAMPLES + j) as f32 / OPUS_PCM_SAMPLE_RATE as f32;
221 ((t * 440.0 * 2.0 * std::f32::consts::PI).sin() * 8000.0) as i16
222 })
223 .collect();
224 let mut packet = Vec::new();
225 enc.encode(&pcm, &mut packet).unwrap();
226 packets.push(packet);
227 }
228 let mut out = Vec::new();
229 dec.decode(&packets[0], &mut out).unwrap();
230 out.clear();
231 dec.decode(&packets[1], &mut out).unwrap();
232 // Packet 2 "lost": recover from packet 3's FEC, then conceal
233 // one more as if a second gap followed.
234 out.clear();
235 assert_eq!(
236 dec.recover_lost(&packets[3], &mut out).unwrap(),
237 OPUS_FRAME_SAMPLES
238 );
239 assert_eq!(dec.conceal(&mut out).unwrap(), OPUS_FRAME_SAMPLES);
240 assert_eq!(out.len(), 2 * OPUS_FRAME_SAMPLES);
241 }
242 }
243}