Skip to main content

audio_codec/
opus.rs

1#[cfg(feature = "std")]
2use super::PcmBuf;
3use super::{CodecError, Decoder, Encoder, Sample};
4// `Box` lives in the std prelude; under `no_std` it comes from `alloc`.
5#[cfg(not(feature = "std"))]
6use alloc::boxed::Box;
7pub use opus_rs::Application as OpusApplication;
8use opus_rs::{Application, OpusDecoder as OpusDecoderRaw, OpusEncoder as OpusEncoderRaw};
9
10// Heap-free scratch-buffer caps (worst case: 48 kHz, stereo, 20 ms frame).
11const OPUS_MAX_FRAME: usize = 960; // 20 ms @ 48 kHz per channel
12const OPUS_MAX_CHANNELS: usize = 2;
13const OPUS_MAX_SAMPLES: usize = OPUS_MAX_FRAME * OPUS_MAX_CHANNELS; // 1920
14// RFC 6716: a single Opus packet carries at most 1276 bytes. Used by the
15// std-only `encode` helper's scratch packet buffer.
16#[cfg(feature = "std")]
17const OPUS_MAX_PACKET: usize = 1276;
18
19pub struct OpusDecoder {
20    decoder: Box<OpusDecoderRaw>,
21    sample_rate: u32,
22    channels: u16,
23    w_output_f32: [f32; OPUS_MAX_SAMPLES],
24    w_pcm_i16: [i16; OPUS_MAX_SAMPLES],
25}
26
27impl OpusDecoder {
28    /// Create a new Opus decoder instance
29    pub fn new(sample_rate: u32, channels: u16) -> Self {
30        let decoder = Box::new(
31            OpusDecoderRaw::new(sample_rate as i32, channels as usize)
32                .expect("Failed to create Opus decoder"),
33        );
34
35        Self {
36            decoder,
37            sample_rate,
38            channels,
39            w_output_f32: [0.0; OPUS_MAX_SAMPLES],
40            w_pcm_i16: [0; OPUS_MAX_SAMPLES],
41        }
42    }
43
44    /// Create a default Opus decoder (48kHz, stereo)
45    pub fn new_default() -> Self {
46        Self::new(48000, 2)
47    }
48
49    /// Lower-level decode that does not perform the stereo→mono downmix.
50    /// Used internally by both the trait impl and the back-comat `decode`.
51    pub fn decode_into_raw(&mut self, data: &[u8], output: &mut [i16]) -> usize {
52        if data.is_empty() {
53            return 0;
54        }
55
56        // Detect the actual channel count from the Opus packet's TOC byte
57        // (bit 2 is the stereo flag per RFC 6716). opus-rs 0.1.19+ rejects
58        // decoding when the packet channel count doesn't match the decoder's,
59        // so we adapt the decoder here to avoid returning empty PCM.
60        let packet_channels = if data[0] & 0x04 != 0 { 2usize } else { 1 };
61        if self.channels as usize != packet_channels {
62            self.channels = packet_channels as u16;
63            // Reuse the existing heap allocation (see `clippy::replace_box`).
64            *self.decoder = OpusDecoderRaw::new(self.sample_rate as i32, packet_channels)
65                .expect("Failed to create Opus decoder");
66        }
67
68        let channels = usize::from(self.channels);
69        let frame_size = (self.sample_rate as usize * 20) / 1000;
70        let max_samples = frame_size * channels;
71
72        match self
73            .decoder
74            .decode(data, frame_size, &mut self.w_output_f32[..max_samples])
75        {
76            Ok(len) => {
77                let total_samples = len * channels;
78                if total_samples == 0 {
79                    return 0;
80                }
81
82                for i in 0..total_samples {
83                    self.w_pcm_i16[i] =
84                        (self.w_output_f32[i] * 32768.0).clamp(-32768.0, 32767.0) as i16;
85                }
86
87                let n = total_samples.min(output.len());
88                output[..n].copy_from_slice(&self.w_pcm_i16[..n]);
89                n
90            }
91            Err(_) => 0,
92        }
93    }
94}
95
96impl Decoder for OpusDecoder {
97    fn decode_into(&mut self, data: &[u8], out: &mut [Sample]) -> Result<usize, CodecError> {
98        if data.is_empty() {
99            return Ok(0);
100        }
101        let n = self.decode_into_raw(data, out);
102        if n == 0 {
103            Err(CodecError::DecodeFailed)
104        } else {
105            Ok(n)
106        }
107    }
108
109    fn max_decode_samples(&self, n_bytes: usize) -> usize {
110        // One 20ms frame per packet is the typical case; size for stereo
111        // to cover the worst case (caller may downmix afterwards).
112        let frame_size = (self.sample_rate as usize * 20) / 1000;
113        let _ = n_bytes;
114        frame_size * 2
115    }
116
117    fn sample_rate(&self) -> u32 {
118        self.sample_rate
119    }
120
121    fn channels(&self) -> u16 {
122        self.channels
123    }
124
125    /// Override the default to preserve back-comat stereo→mono downmix
126    /// when the decoder was configured with `channels == 2`.
127    #[cfg(feature = "std")]
128    fn decode(&mut self, data: &[u8]) -> PcmBuf {
129        if data.is_empty() {
130            return Vec::new();
131        }
132        let packet_channels = if data[0] & 0x04 != 0 { 2usize } else { 1 };
133
134        let frame_size = (self.sample_rate as usize * 20) / 1000;
135        let max_samples = frame_size * packet_channels;
136        let mut pcm = vec![0i16; max_samples];
137        let n = self.decode_into_raw(data, &mut pcm);
138        pcm.truncate(n);
139        if usize::from(self.channels) == 2 {
140            pcm = pcm
141                .chunks_exact(2)
142                .map(|chunk| ((chunk[0] as i32 + chunk[1] as i32) / 2) as i16)
143                .collect();
144        }
145        pcm
146    }
147}
148
149pub struct OpusEncoder {
150    encoder: Box<OpusEncoderRaw>,
151    sample_rate: u32,
152    channels: u16,
153    w_input_f32: [f32; OPUS_MAX_SAMPLES],
154}
155
156impl OpusEncoder {
157    pub fn new_with_application(sample_rate: u32, channels: u16, application: Application) -> Self {
158        let encoder = Box::new(
159            OpusEncoderRaw::new(sample_rate as i32, channels as usize, application)
160                .expect("Failed to create Opus encoder"),
161        );
162
163        Self {
164            encoder,
165            sample_rate,
166            channels,
167            w_input_f32: [0.0; OPUS_MAX_SAMPLES],
168        }
169    }
170
171    /// Create a new Opus encoder instance.
172    ///
173    /// Keep backward-compatible defaults with pre-0.3.31 behavior:
174    /// - VoIP application
175    /// - caller can provide mono PCM even when encoder is configured as stereo;
176    ///   `encode()` duplicates mono samples to stereo.
177    pub fn new(sample_rate: u32, channels: u16) -> Self {
178        let mut enc = Self::new_with_application(sample_rate, channels, Application::Voip);
179        enc.encoder.bitrate_bps = if channels == 2 { 64000 } else { 48000 };
180        enc.encoder.complexity = 5;
181        enc.encoder.use_cbr = true;
182        enc
183    }
184
185    /// Create a default Opus encoder (48kHz, stereo)
186    pub fn new_default() -> Self {
187        Self::new(48000, 2)
188    }
189
190    /// Set the encoder bitrate in bits per second.
191    pub fn set_bitrate(&mut self, bitrate_bps: i32) {
192        self.encoder.bitrate_bps = bitrate_bps;
193    }
194
195    /// Set the encoder complexity (0-10).
196    pub fn set_complexity(&mut self, complexity: i32) {
197        self.encoder.complexity = complexity;
198    }
199
200    /// Enable or disable constant bitrate (CBR) mode.
201    pub fn set_cbr(&mut self, cbr: bool) {
202        self.encoder.use_cbr = cbr;
203    }
204
205    /// Encode into a caller-provided packet buffer.
206    ///
207    /// Returns `Some(bytes_written)` on success. Expects `samples` to match
208    /// the encoder's channel count (interleaved stereo when `channels == 2`).
209    pub fn encode_into_raw(&mut self, samples: &[Sample], output: &mut [u8]) -> Option<usize> {
210        let channels = usize::from(self.channels);
211        if samples.is_empty() || channels == 0 || !samples.len().is_multiple_of(channels) {
212            return None;
213        }
214        // Fixed scratch cap (heap-free build).
215        if samples.len() > OPUS_MAX_SAMPLES {
216            return None;
217        }
218
219        let frame_size = samples.len() / channels;
220
221        for (dst, &s) in self.w_input_f32[..samples.len()]
222            .iter_mut()
223            .zip(samples.iter())
224        {
225            *dst = s as f32 / 32768.0;
226        }
227
228        self.encoder
229            .encode(&self.w_input_f32[..samples.len()], frame_size, output)
230            .ok()
231    }
232
233    #[cfg(feature = "std")]
234    fn encode_raw(&mut self, samples: &[Sample]) -> Vec<u8> {
235        let channels = usize::from(self.channels);
236        if samples.is_empty()
237            || channels == 0
238            || !samples.len().is_multiple_of(channels)
239            || samples.len() > OPUS_MAX_SAMPLES
240        {
241            return Vec::new();
242        }
243
244        let frame_size = samples.len() / channels;
245
246        for (dst, &s) in self.w_input_f32[..samples.len()]
247            .iter_mut()
248            .zip(samples.iter())
249        {
250            *dst = s as f32 / 32768.0;
251        }
252
253        let mut packet = [0u8; OPUS_MAX_PACKET];
254        match self
255            .encoder
256            .encode(&self.w_input_f32[..samples.len()], frame_size, &mut packet)
257        {
258            Ok(len) => packet[..len].to_vec(),
259            Err(_) => Vec::new(),
260        }
261    }
262}
263
264impl Encoder for OpusEncoder {
265    fn encode_into(&mut self, samples: &[Sample], out: &mut [u8]) -> Result<usize, CodecError> {
266        if samples.is_empty() {
267            return Ok(0);
268        }
269        // Note: this expects `samples` to already match the encoder's channel
270        // count (interleaved stereo if `channels == 2`). For the legacy
271        // mono→stereo upmix behavior, use the std-only `encode` method.
272        match self.encode_into_raw(samples, out) {
273            Some(n) => Ok(n),
274            None => Err(CodecError::EncodeFailed),
275        }
276    }
277
278    fn max_encode_bytes(&self, _n_samples: usize) -> usize {
279        // Per RFC 6716 the maximum Opus packet size is 1275 bytes.
280        1275
281    }
282
283    fn sample_rate(&self) -> u32 {
284        self.sample_rate
285    }
286
287    fn channels(&self) -> u16 {
288        self.channels
289    }
290
291    /// Override the default to preserve back-comat mono→stereo upmix
292    /// when the encoder was configured with `channels == 2`.
293    #[cfg(feature = "std")]
294    fn encode(&mut self, samples: &[Sample]) -> Vec<u8> {
295        if self.channels == 2 {
296            // mono→stereo upmix into a local scratch buffer (no allocation).
297            if samples.len() > OPUS_MAX_FRAME {
298                return Vec::new();
299            }
300            let mut stereo = [0i16; OPUS_MAX_SAMPLES];
301            for (i, &sample) in samples.iter().enumerate() {
302                stereo[2 * i] = sample;
303                stereo[2 * i + 1] = sample;
304            }
305            return self.encode_raw(&stereo[..samples.len() * 2]);
306        }
307        self.encode_raw(samples)
308    }
309}
310
311// The tests below exercise the std-only allocating `encode`/`decode`
312// conveniences (including the stereo-downmix handling), so they only compile
313// with `std`. The `*_into` trait API is covered portably by
314// `test_opus_into_roundtrip` at the bottom, which also runs in `no_std`.
315#[cfg(all(test, feature = "std"))]
316mod tests {
317    use super::*;
318
319    fn make_mono_pcm_20ms() -> Vec<i16> {
320        (0..960).map(|i| ((i * 100) % 32767) as i16).collect()
321    }
322
323    #[test]
324    fn test_opus_encode_decode_20ms_produces_960_mono_samples() {
325        let mut enc = OpusEncoder::new_default();
326        let pcm = make_mono_pcm_20ms();
327        let opus_pkt = enc.encode(&pcm);
328        assert!(!opus_pkt.is_empty(), "encoder should produce output");
329
330        // TOC byte bit 2 should be set (stereo flag from mono→stereo upmix)
331        assert!(opus_pkt[0] & 0x04 != 0, "packet should be stereo");
332
333        let mut dec = OpusDecoder::new_default();
334        let decoded = dec.decode(&opus_pkt);
335
336        // 20ms at 48kHz mono = 960 samples
337        assert_eq!(
338            decoded.len(),
339            960,
340            "decoder should downmix stereo→mono and output 960 samples, got {}",
341            decoded.len()
342        );
343    }
344
345    #[test]
346    fn test_opus_consecutive_frames_all_produce_960() {
347        let mut enc = OpusEncoder::new_default();
348        let pcm = make_mono_pcm_20ms();
349        let opus_pkt = enc.encode(&pcm);
350
351        let mut dec = OpusDecoder::new_default();
352        for i in 0..5 {
353            let decoded = dec.decode(&opus_pkt);
354            assert_eq!(
355                decoded.len(),
356                960,
357                "frame {} should produce 960 mono samples, got {}",
358                i,
359                decoded.len()
360            );
361        }
362    }
363
364    #[test]
365    fn test_opus_decoder_output_has_reasonable_energy() {
366        let mut enc = OpusEncoder::new_default();
367        // 440Hz sine at 48kHz, 20ms
368        let pcm: Vec<i16> = (0..960)
369            .map(|i| {
370                let t = i as f64 / 48000.0;
371                (16384.0 * (2.0 * std::f64::consts::PI * 440.0 * t).sin()) as i16
372            })
373            .collect();
374        let opus_pkt = enc.encode(&pcm);
375
376        let mut dec = OpusDecoder::new_default();
377        let decoded = dec.decode(&opus_pkt);
378        assert_eq!(decoded.len(), 960);
379
380        let energy: f64 =
381            decoded.iter().map(|&s| (s as f64).powi(2)).sum::<f64>() / decoded.len() as f64;
382        let rms = energy.sqrt();
383        assert!(
384            rms > 100.0 && rms < 20000.0,
385            "decoded audio RMS {} should be in reasonable range",
386            rms
387        );
388    }
389
390    #[test]
391    fn test_opus_decoder_handles_mono_packet_gracefully() {
392        // Create a mono Opus encoder, encode PCM → mono Opus packet
393        let mut mono_enc = OpusEncoder::new(48000, 1);
394        let pcm = make_mono_pcm_20ms();
395        let mono_pkt = mono_enc.encode(&pcm);
396        assert!(!mono_pkt.is_empty());
397        // Mono packet TOC bit 2 should be 0
398        assert!(
399            mono_pkt[0] & 0x04 == 0,
400            "mono encoder should produce mono packet"
401        );
402
403        // Decode with default stereo decoder — should handle mono→stereo transition
404        let mut dec = OpusDecoder::new_default();
405        let decoded = dec.decode(&mono_pkt);
406        assert_eq!(
407            decoded.len(),
408            960,
409            "first mono packet should produce exactly 960 mono samples, got {}",
410            decoded.len()
411        );
412
413        // Second mono packet should also be 960
414        let decoded2 = dec.decode(&mono_pkt);
415        assert_eq!(
416            decoded2.len(),
417            960,
418            "second mono packet should also produce 960 mono samples, got {}",
419            decoded2.len()
420        );
421    }
422
423    #[test]
424    fn test_opus_stereo_packet_downmix_produces_960() {
425        let mut enc = OpusEncoder::new_default();
426        let pcm = make_mono_pcm_20ms();
427        let stereo_pkt = enc.encode(&pcm);
428        assert!(stereo_pkt[0] & 0x04 != 0);
429
430        let mut dec = OpusDecoder::new_default();
431        let decoded = dec.decode(&stereo_pkt);
432        assert_eq!(
433            decoded.len(),
434            960,
435            "stereo packet should downmix to 960 mono samples"
436        );
437    }
438}
439
440/// Portable roundtrip through the trait `encode_into`/`decode_into` API
441/// (compiles with or without `std`; the no_std opus path).
442#[cfg(all(test, feature = "opus"))]
443mod no_std_tests {
444    use super::*;
445    use alloc::vec;
446    use alloc::vec::Vec;
447
448    #[test]
449    fn test_opus_into_roundtrip() {
450        let mut enc = OpusEncoder::new_default();
451        let mut dec = OpusDecoder::new(48000, 1);
452        let pcm: Vec<i16> = (0..960).map(|i| ((i * 100) % 32767) as i16).collect();
453
454        let mut packet = alloc::vec![0u8; enc.max_encode_bytes(pcm.len())];
455        let n = enc
456            .encode_into(&pcm, &mut packet)
457            .expect("packet sized by max_encode_bytes");
458        packet.truncate(n);
459        assert!(!packet.is_empty(), "encoder should produce output");
460
461        let mut out = alloc::vec![0i16; dec.max_decode_samples(packet.len())];
462        let samples = dec
463            .decode_into(&packet, &mut out)
464            .expect("out sized by max_decode_samples");
465        assert_eq!(samples, 960, "20 ms at 48 kHz must decode to 960 samples");
466        let rms: f64 = out[..samples]
467            .iter()
468            .map(|&s| (s as f64) * (s as f64))
469            .sum::<f64>()
470            / samples as f64;
471        assert!(rms.sqrt() > 1000.0, "decoded signal should keep energy");
472    }
473}