Skip to main content

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}