Skip to main content

opus_pure/
lib.rs

1//! Pure-Rust Opus audio codec (RFC 6716) with Ogg encapsulation (RFC 7845).
2//!
3//! Encoder and decoder for all three Opus coding modes — SILK for speech, CELT
4//! for music, and the hybrid of both — plus a real Ogg container layer, so this
5//! crate reads and writes `.opus` files rather than only raw packets.
6//!
7//! # Encoding to an `.opus` file
8//!
9//! ```
10//! use opus_pure::{Application, MAX_PACKET_BYTES, OggOpusWriter, OpusEncoder, OpusHead};
11//!
12//! let (rate, channels, frame) = (48_000, 2, 960); // 20 ms stereo
13//! let pcm = vec![0.0f32; frame * channels * 50];  // one second of silence
14//!
15//! let mut encoder = OpusEncoder::new(rate, channels, Application::Audio)?;
16//! encoder.bitrate_bps = 96_000;
17//!
18//! // The header takes its pre-skip from the encoder's own delay rather than a
19//! // constant, which is what makes it right for every `Application`.
20//! let head = OpusHead::for_encoder(&encoder, rate as u32);
21//! let mut writer = OggOpusWriter::new(Vec::new(), head)?;
22//! let mut packet = vec![0u8; MAX_PACKET_BYTES];
23//! for block in pcm.chunks_exact(frame * channels) {
24//!     let n = encoder.encode(block, frame, &mut packet)?;
25//!     writer.write_packet(&packet[..n])?;
26//! }
27//! let file: Vec<u8> = writer.finish()?;
28//! assert_eq!(&file[..4], b"OggS");
29//! # Ok::<(), opus_pure::Error>(())
30//! ```
31//!
32//! [`finish`](OggOpusWriter::finish) writes the end-of-stream page and must be
33//! called; dropping the writer flushes on a best-effort basis but cannot report
34//! an I/O failure.
35//!
36//! # Integer PCM
37//!
38//! Both directions have a 16-bit entry point, for the many callers whose audio
39//! is already `i16`. They are not wrappers over the float ones, any more than
40//! libopus's are: [`encode_s16`](OpusEncoder::encode_s16) declares 16 bits of
41//! input precision where [`encode`](OpusEncoder::encode) declares 24, and
42//! [`decode_s16`](OpusDecoder::decode_s16) soft-clips before converting, which
43//! [`decode`](OpusDecoder::decode) does not. See [`SoftClip`] for why that
44//! second one matters and how to get it on the float path.
45//!
46//! ```
47//! use opus_pure::{Application, MAX_PACKET_BYTES, OpusDecoder, OpusEncoder};
48//!
49//! let mut encoder = OpusEncoder::new(48_000, 2, Application::Audio)?;
50//! let mut decoder = OpusDecoder::new(48_000, 2)?;
51//!
52//! let pcm = vec![0i16; 960 * 2];                  // 20 ms of stereo at 48 kHz
53//! let mut packet = vec![0u8; MAX_PACKET_BYTES];
54//! let n = encoder.encode_s16(&pcm, 960, &mut packet)?;
55//!
56//! let mut out = vec![0i16; 960 * 2];
57//! let samples = decoder.decode_s16(&packet[..n], 960, &mut out)?;
58//! assert_eq!(samples, 960);
59//! # Ok::<(), opus_pure::Error>(())
60//! ```
61//!
62//! # Decoding one back
63//!
64//! ```
65//! use opus_pure::{Application, MAX_PACKET_BYTES, MAX_PACKET_SAMPLES, OggOpusReader,
66//!                OggOpusWriter, OpusEncoder, OpusHead, Trim};
67//! # let (rate, channels, frame) = (48_000, 2, 960);
68//! # let pcm = vec![0.0f32; frame * channels * 50];
69//! # let mut encoder = OpusEncoder::new(rate, channels, Application::Audio)?;
70//! # let mut writer = OggOpusWriter::new(Vec::new(), OpusHead::for_encoder(&encoder, 48_000))?;
71//! # let mut packet = vec![0u8; MAX_PACKET_BYTES];
72//! # for block in pcm.chunks_exact(frame * channels) {
73//! #     let n = encoder.encode(block, frame, &mut packet)?;
74//! #     writer.write_packet(&packet[..n])?;
75//! # }
76//! # let file: Vec<u8> = writer.finish()?;
77//! let mut reader = OggOpusReader::new(std::io::Cursor::new(&file))?;
78//! let head = reader.head().clone();
79//! let channels = head.channel_count as usize;
80//!
81//! // Carries the channel count and the header's output gain.
82//! let mut decoder = head.decoder(48_000)?;
83//! // Takes the encoder delay off the front and the end-trim off the back.
84//! let mut trim = Trim::new(&head, 48_000, channels)?;
85//!
86//! let mut block = vec![0.0f32; MAX_PACKET_SAMPLES * channels];
87//! let mut out = Vec::new();
88//! for packet in reader.packets() {
89//!     let packet = packet?;
90//!     let n = decoder.decode(&packet.data, MAX_PACKET_SAMPLES, &mut block)?;
91//!     out.extend_from_slice(trim.keep(&packet, &block[..n * channels]));
92//! }
93//! // One second in, one second back, less the encoder delay that the stream
94//! // above never flushed — see below.
95//! assert_eq!(trim.samples_emitted(), 48_000 - u64::from(head.pre_skip));
96//! # Ok::<(), opus_pure::Error>(())
97//! ```
98//!
99//! # Where a stream begins and ends
100//!
101//! A decoded Opus stream is longer than the audio that went into it at both
102//! ends, and RFC 7845 gives both corrections: the [`pre_skip`](OpusHead::pre_skip)
103//! at the front (§4.2, the encoder's algorithmic delay) and an end-trim at the
104//! back (§4.4, a final granule position deliberately short of what the packets
105//! decode to). [`Trim`] applies the pair, which is worth reaching for even
106//! though it is ten lines: the first correction is conspicuous when it is
107//! missing and the second is silent, and every file `opusenc` writes carries
108//! one.
109//!
110//! Writing them is the same job in reverse, and it is not automatic:
111//! [`OggOpusWriter`] documents the tail arithmetic, and
112//! [`write_packet_with_duration`](OggOpusWriter::write_packet_with_duration) is
113//! what states the end-trim. The example above writes a whole number of frames
114//! and no end-trim, so it comes back one encoder delay short — which is what
115//! that arithmetic exists to fix.
116//!
117//! Build the header with [`OpusHead::for_encoder`] and the pre-skip is measured
118//! from the encoder rather than assumed; [`OpusHead::new`] uses the conventional
119//! 312, which is four milliseconds too many for
120//! [`Application::RestrictedLowDelay`].
121//!
122//! # Working with raw packets
123//!
124//! [`OpusEncoder`] and [`OpusDecoder`] are usable on their own when the framing
125//! comes from elsewhere (RTP, a custom container). [`Repacketizer`] combines and
126//! splits packets, and [`encode_parallel`] encodes a clip across threads by
127//! splitting it into chunks — a different encode from the serial one, and
128//! [`parallel`] is explicit about how it differs.
129
130// The public surface is a published contract, so both of these are structural
131// rather than a convention CI happens to enforce. `missing_docs` because this
132// crate's private internals are heavily commented and its public API once was
133// not, which is exactly backwards for what docs.rs renders; `missing_debug_
134// implementations` because a public type without `Debug` cannot appear in
135// anyone else's derived one. Neither reaches private items.
136#![deny(missing_docs)]
137#![deny(missing_debug_implementations)]
138#![allow(unsafe_op_in_unsafe_fn)]
139#![allow(clippy::too_many_arguments)]
140#![allow(clippy::needless_range_loop)]
141
142// The README is the first thing anyone reads and the last thing anyone checks,
143// so its examples are compiled and run with the rest of the doctests. Nothing
144// is rendered from here: this only exists so a change to the API that the
145// README describes cannot pass CI while the README still shows the old one.
146#[cfg(doctest)]
147#[doc = include_str!("../README.md")]
148struct Readme;
149
150// ---- Public API ----
151mod config;
152mod decoder;
153mod encoder;
154mod error;
155pub mod multistream;
156pub mod ogg;
157pub mod packet;
158pub mod parallel;
159pub mod repacketizer;
160mod soft_clip;
161
162// ---- Codec internals (no semver contract) ----
163mod analysis;
164mod analysis_data;
165mod celt;
166mod hp_cutoff;
167mod range_coder;
168mod silk;
169mod toc;
170
171/// Internal measurement hooks for the harnesses in [`reference/`][ref]. Not
172/// public API: what this exposes can change or disappear without a version
173/// bump. Compiled only under the non-default `probe` feature.
174///
175/// [ref]: https://github.com/stephenberry/opus-pure/tree/main/reference
176#[cfg(feature = "probe")]
177pub mod probe {
178    /// CELT's band edges, in units of 200 Hz — RFC 6716 §4.3.1's `eband5ms`.
179    ///
180    /// A 2.5 ms MDCT at 48 kHz has 120 bins over 24 kHz, so one unit is 200 Hz
181    /// and the last edge, 100, is the 20 kHz top of fullband. A harness that
182    /// reports a result per band has to use the codec's own band layout rather
183    /// than a copy of it: three tools in `reference/` once carried private
184    /// copies of the test signal generators and spent months measuring audio no
185    /// test encoded.
186    pub const CELT_BAND_EDGES_200HZ: [i16; 22] = crate::celt::modes::EBAND_5MS;
187}
188
189pub use config::{Application, Bandwidth, OpusMode, RateControl, Signal};
190pub use decoder::OpusDecoder;
191pub use encoder::{MAX_PACKET_BYTES, OpusEncoder};
192pub use error::{Error, Result};
193pub use multistream::{ChannelLayout, OpusMSDecoder, OpusMSEncoder};
194pub use ogg::{OggOpusReader, OggOpusWriter, OggPacket, OpusHead, OpusTags, Trim};
195pub use packet::MAX_PACKET_SAMPLES;
196pub use parallel::{DEFAULT_WARMUP_MS, ParallelConfig, ParallelPlan, encode_parallel};
197pub use repacketizer::Repacketizer;
198pub use soft_clip::SoftClip;
199
200#[cfg(test)]
201mod integration_tests {
202    use crate::config::OpusMode;
203    use crate::toc::{
204        channels_from_toc, frame_duration_ms_from_toc, frame_rate_from_params, gen_toc,
205        mode_from_toc,
206    };
207    use crate::{Application, Bandwidth, OpusDecoder, OpusEncoder, RateControl};
208
209    fn frame_size_from_toc(toc: u8, sampling_rate: i32) -> Option<usize> {
210        let mode = mode_from_toc(toc);
211        match mode {
212            OpusMode::CeltOnly => {
213                let period = ((toc >> 3) & 0x03) as i32;
214                let frame_rate = 400 >> period;
215                if frame_rate == 0 || sampling_rate % frame_rate != 0 {
216                    return None;
217                }
218                Some((sampling_rate / frame_rate) as usize)
219            }
220            OpusMode::SilkOnly => {
221                let duration_ms = frame_duration_ms_from_toc(toc);
222                Some((sampling_rate as i64 * duration_ms as i64 / 1000) as usize)
223            }
224            OpusMode::Hybrid => {
225                let duration_ms = frame_duration_ms_from_toc(toc);
226                Some((sampling_rate as i64 * duration_ms as i64 / 1000) as usize)
227            }
228        }
229    }
230
231    #[test]
232    fn gen_toc_matches_celt_reference_values() {
233        let sampling_rate = 48_000;
234        let cases = [
235            (120usize, 0xE0u8),
236            (240usize, 0xE8u8),
237            (480usize, 0xF0u8),
238            (960usize, 0xF8u8),
239        ];
240
241        for (frame_size, expected_toc) in cases {
242            let frame_rate = frame_rate_from_params(sampling_rate, frame_size).unwrap();
243            let toc = gen_toc(OpusMode::CeltOnly, frame_rate, Bandwidth::Fullband, 1);
244            assert_eq!(
245                toc, expected_toc,
246                "frame_size {} expected TOC {:02X} got {:02X}",
247                frame_size, expected_toc, toc
248            );
249            let decoded_size = frame_size_from_toc(toc, sampling_rate).unwrap();
250            assert_eq!(decoded_size, frame_size);
251        }
252
253        let stereo_toc = gen_toc(
254            OpusMode::CeltOnly,
255            frame_rate_from_params(sampling_rate, 960).unwrap(),
256            Bandwidth::Fullband,
257            2,
258        );
259        assert_eq!(channels_from_toc(stereo_toc), 2);
260    }
261
262    /// The SILK TOC configurations, including the 60 ms one that no sample rate
263    /// divides evenly.
264    ///
265    /// `gen_toc` finds the duration by doubling the frame rate until it reaches
266    /// 400, which needs `frame_rate_from_params` to hand it the *truncated*
267    /// 48000/2880 = 16 rather than a rounded 17: 16 doubles to 512 in five
268    /// steps and lands on config 3, and 17 lands on the same config only by
269    /// accident at 48 kHz and on the wrong one at 8 kHz.
270    #[test]
271    fn gen_toc_covers_every_silk_duration() {
272        for &(rate, ms, frame_size) in &[
273            (48_000i32, 10i32, 480usize),
274            (48_000, 20, 960),
275            (48_000, 40, 1920),
276            (48_000, 60, 2880),
277            (8_000, 10, 80),
278            (8_000, 20, 160),
279            (8_000, 40, 320),
280            (8_000, 60, 480),
281        ] {
282            let frame_rate = frame_rate_from_params(rate, frame_size)
283                .unwrap_or_else(|| panic!("{rate} Hz / {ms} ms rejected"));
284            let toc = gen_toc(OpusMode::SilkOnly, frame_rate, Bandwidth::Wideband, 1);
285            assert_eq!(
286                frame_duration_ms_from_toc(toc),
287                ms,
288                "{rate} Hz / {ms} ms produced TOC {toc:02X}"
289            );
290            assert_eq!(mode_from_toc(toc), OpusMode::SilkOnly);
291            assert_eq!(frame_size_from_toc(toc, rate).unwrap(), frame_size);
292        }
293    }
294
295    /// `frame_rate_from_params` answers for one coded *frame*, so the durations
296    /// Opus can only express by packing several frames into one packet have no
297    /// answer here. The encoder reaches them through `PacketDuration::layout`,
298    /// which splits them into frames this function does recognise.
299    #[test]
300    fn frame_rate_rejects_durations_that_need_multi_frame_packets() {
301        for &(rate, frame_size) in &[
302            (48_000i32, 3840usize), // 80 ms
303            (48_000, 4800),         // 100 ms
304            (48_000, 5760),         // 120 ms
305            (16_000, 1280),         // 80 ms
306            (8_000, 640),           // 80 ms
307        ] {
308            assert!(
309                frame_rate_from_params(rate, frame_size).is_none(),
310                "{rate} Hz / {frame_size} samples was accepted"
311            );
312        }
313        // And nothing that is not a frame duration at all.
314        assert!(frame_rate_from_params(48_000, 0).is_none());
315        assert!(frame_rate_from_params(48_000, 333).is_none());
316        assert!(frame_rate_from_params(48_000, usize::MAX).is_none());
317    }
318
319    #[test]
320    fn test_celt_decoder_large_frame_sizes() {
321        let sampling_rate = 48000;
322        let channels = 1;
323
324        let mut decoder = OpusDecoder::new(sampling_rate, channels).unwrap();
325
326        let frame_sizes = [120, 240, 480, 960];
327
328        for frame_size in frame_sizes {
329            let toc = gen_toc(
330                OpusMode::CeltOnly,
331                frame_rate_from_params(sampling_rate, frame_size).unwrap(),
332                Bandwidth::Fullband,
333                channels,
334            );
335            let packet = [toc, 0, 0, 0, 0];
336
337            let mut output = vec![0.0f32; frame_size * channels];
338
339            let _ = decoder.decode(&packet, frame_size, &mut output);
340        }
341
342        let channels = 2;
343        let mut decoder = OpusDecoder::new(sampling_rate, channels).unwrap();
344
345        for frame_size in frame_sizes {
346            let toc = gen_toc(
347                OpusMode::CeltOnly,
348                frame_rate_from_params(sampling_rate, frame_size).unwrap(),
349                Bandwidth::Fullband,
350                channels,
351            );
352            let packet = [toc, 0, 0, 0, 0];
353
354            let mut output = vec![0.0f32; frame_size * channels];
355            let _ = decoder.decode(&packet, frame_size, &mut output);
356        }
357    }
358
359    #[test]
360    fn test_celt_decoder_edge_case_frame_sizes() {
361        let sampling_rate = 48000;
362        let channels = 1;
363        let mut decoder = OpusDecoder::new(sampling_rate, channels).unwrap();
364
365        let edge_sizes = [2048, 2167, 2168, 2169, 2880, 3072];
366
367        for frame_size in edge_sizes {
368            let mut output = vec![0.0f32; frame_size * channels];
369
370            let _ = decoder.decode(&[0x80, 0, 0, 0], frame_size, &mut output);
371        }
372    }
373
374    // Regression test for: "index out of bounds: the len is 48 but the index is 119"
375    // Root cause: frame_size=48 at 48kHz gives frame_rate=1000, which is not a valid
376    // Hybrid-mode frame rate but was not validated.  CELT's lm-search then silently
377    // fell back to lm=0, computed n2=120, and wrote output[119] into a 48-element
378    // slice.  Triggered via G.729-decoded PCM (8kHz) passed to a 48kHz Opus encoder
379    // without proper resampling, so the encoder received 48 samples instead of 480.
380    #[test]
381    fn test_invalid_small_frame_size_returns_error_not_panic() {
382        let mut enc = OpusEncoder::new(48000, 2, Application::Voip).unwrap();
383        enc.bitrate_bps = 64000;
384        enc.complexity = 5;
385        enc.rate_control = RateControl::Cbr;
386
387        // 48 samples at 48kHz = 1ms → frame_rate=1000, invalid for Hybrid mode.
388        let input = vec![0.0f32; 48 * 2]; // stereo interleaved
389        let mut output = vec![0u8; 256];
390
391        let result = enc.encode(&input, 48, &mut output);
392        assert!(
393            result.is_err(),
394            "encode with invalid frame_size=48 should return Err, not panic"
395        );
396    }
397
398    // Also verify that the Audio application path (always Hybrid at 48 kHz) rejects
399    // the same bad frame size.
400    #[test]
401    fn test_invalid_small_frame_size_audio_application_returns_error() {
402        let mut enc = OpusEncoder::new(48000, 1, Application::Audio).unwrap();
403        let input = vec![0.0f32; 48];
404        let mut output = vec![0u8; 256];
405
406        let result = enc.encode(&input, 48, &mut output);
407        assert!(
408            result.is_err(),
409            "Audio/48kHz encoder with frame_size=48 should return Err"
410        );
411    }
412}