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;
166#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
167mod cpu_features;
168mod hp_cutoff;
169mod range_coder;
170mod silk;
171mod toc;
172
173/// Internal measurement hooks for the harnesses in [`reference/`][ref]. Not
174/// public API: what this exposes can change or disappear without a version
175/// bump. Compiled only under the non-default `probe` feature.
176///
177/// [ref]: https://github.com/stephenberry/opus-pure/tree/main/reference
178#[cfg(feature = "probe")]
179pub mod probe {
180    /// CELT's band edges, in units of 200 Hz — RFC 6716 §4.3.1's `eband5ms`.
181    ///
182    /// A 2.5 ms MDCT at 48 kHz has 120 bins over 24 kHz, so one unit is 200 Hz
183    /// and the last edge, 100, is the 20 kHz top of fullband. A harness that
184    /// reports a result per band has to use the codec's own band layout rather
185    /// than a copy of it: three tools in `reference/` once carried private
186    /// copies of the test signal generators and spent months measuring audio no
187    /// test encoded.
188    pub const CELT_BAND_EDGES_200HZ: [i16; 22] = crate::celt::modes::EBAND_5MS;
189}
190
191pub use config::{Application, Bandwidth, OpusMode, RateControl, Signal};
192pub use decoder::OpusDecoder;
193pub use encoder::{MAX_PACKET_BYTES, OpusEncoder};
194pub use error::{Error, Result};
195pub use multistream::{ChannelLayout, OpusMSDecoder, OpusMSEncoder};
196pub use ogg::{OggOpusReader, OggOpusWriter, OggPacket, OpusHead, OpusTags, Trim};
197pub use packet::MAX_PACKET_SAMPLES;
198pub use parallel::{DEFAULT_WARMUP_MS, ParallelConfig, ParallelPlan, encode_parallel};
199pub use repacketizer::Repacketizer;
200pub use soft_clip::SoftClip;
201
202#[cfg(test)]
203mod integration_tests {
204    use crate::config::OpusMode;
205    use crate::toc::{
206        channels_from_toc, frame_duration_ms_from_toc, frame_rate_from_params, gen_toc,
207        mode_from_toc,
208    };
209    use crate::{Application, Bandwidth, OpusDecoder, OpusEncoder, RateControl};
210
211    fn frame_size_from_toc(toc: u8, sampling_rate: i32) -> Option<usize> {
212        let mode = mode_from_toc(toc);
213        match mode {
214            OpusMode::CeltOnly => {
215                let period = ((toc >> 3) & 0x03) as i32;
216                let frame_rate = 400 >> period;
217                if frame_rate == 0 || sampling_rate % frame_rate != 0 {
218                    return None;
219                }
220                Some((sampling_rate / frame_rate) as usize)
221            }
222            OpusMode::SilkOnly => {
223                let duration_ms = frame_duration_ms_from_toc(toc);
224                Some((sampling_rate as i64 * duration_ms as i64 / 1000) as usize)
225            }
226            OpusMode::Hybrid => {
227                let duration_ms = frame_duration_ms_from_toc(toc);
228                Some((sampling_rate as i64 * duration_ms as i64 / 1000) as usize)
229            }
230        }
231    }
232
233    #[test]
234    fn gen_toc_matches_celt_reference_values() {
235        let sampling_rate = 48_000;
236        let cases = [
237            (120usize, 0xE0u8),
238            (240usize, 0xE8u8),
239            (480usize, 0xF0u8),
240            (960usize, 0xF8u8),
241        ];
242
243        for (frame_size, expected_toc) in cases {
244            let frame_rate = frame_rate_from_params(sampling_rate, frame_size).unwrap();
245            let toc = gen_toc(OpusMode::CeltOnly, frame_rate, Bandwidth::Fullband, 1);
246            assert_eq!(
247                toc, expected_toc,
248                "frame_size {} expected TOC {:02X} got {:02X}",
249                frame_size, expected_toc, toc
250            );
251            let decoded_size = frame_size_from_toc(toc, sampling_rate).unwrap();
252            assert_eq!(decoded_size, frame_size);
253        }
254
255        let stereo_toc = gen_toc(
256            OpusMode::CeltOnly,
257            frame_rate_from_params(sampling_rate, 960).unwrap(),
258            Bandwidth::Fullband,
259            2,
260        );
261        assert_eq!(channels_from_toc(stereo_toc), 2);
262    }
263
264    /// The SILK TOC configurations, including the 60 ms one that no sample rate
265    /// divides evenly.
266    ///
267    /// `gen_toc` finds the duration by doubling the frame rate until it reaches
268    /// 400, which needs `frame_rate_from_params` to hand it the *truncated*
269    /// 48000/2880 = 16 rather than a rounded 17: 16 doubles to 512 in five
270    /// steps and lands on config 3, and 17 lands on the same config only by
271    /// accident at 48 kHz and on the wrong one at 8 kHz.
272    #[test]
273    fn gen_toc_covers_every_silk_duration() {
274        for &(rate, ms, frame_size) in &[
275            (48_000i32, 10i32, 480usize),
276            (48_000, 20, 960),
277            (48_000, 40, 1920),
278            (48_000, 60, 2880),
279            (8_000, 10, 80),
280            (8_000, 20, 160),
281            (8_000, 40, 320),
282            (8_000, 60, 480),
283        ] {
284            let frame_rate = frame_rate_from_params(rate, frame_size)
285                .unwrap_or_else(|| panic!("{rate} Hz / {ms} ms rejected"));
286            let toc = gen_toc(OpusMode::SilkOnly, frame_rate, Bandwidth::Wideband, 1);
287            assert_eq!(
288                frame_duration_ms_from_toc(toc),
289                ms,
290                "{rate} Hz / {ms} ms produced TOC {toc:02X}"
291            );
292            assert_eq!(mode_from_toc(toc), OpusMode::SilkOnly);
293            assert_eq!(frame_size_from_toc(toc, rate).unwrap(), frame_size);
294        }
295    }
296
297    /// `frame_rate_from_params` answers for one coded *frame*, so the durations
298    /// Opus can only express by packing several frames into one packet have no
299    /// answer here. The encoder reaches them through `PacketDuration::layout`,
300    /// which splits them into frames this function does recognise.
301    #[test]
302    fn frame_rate_rejects_durations_that_need_multi_frame_packets() {
303        for &(rate, frame_size) in &[
304            (48_000i32, 3840usize), // 80 ms
305            (48_000, 4800),         // 100 ms
306            (48_000, 5760),         // 120 ms
307            (16_000, 1280),         // 80 ms
308            (8_000, 640),           // 80 ms
309        ] {
310            assert!(
311                frame_rate_from_params(rate, frame_size).is_none(),
312                "{rate} Hz / {frame_size} samples was accepted"
313            );
314        }
315        // And nothing that is not a frame duration at all.
316        assert!(frame_rate_from_params(48_000, 0).is_none());
317        assert!(frame_rate_from_params(48_000, 333).is_none());
318        assert!(frame_rate_from_params(48_000, usize::MAX).is_none());
319    }
320
321    #[test]
322    fn test_celt_decoder_large_frame_sizes() {
323        let sampling_rate = 48000;
324        let channels = 1;
325
326        let mut decoder = OpusDecoder::new(sampling_rate, channels).unwrap();
327
328        let frame_sizes = [120, 240, 480, 960];
329
330        for frame_size in frame_sizes {
331            let toc = gen_toc(
332                OpusMode::CeltOnly,
333                frame_rate_from_params(sampling_rate, frame_size).unwrap(),
334                Bandwidth::Fullband,
335                channels,
336            );
337            let packet = [toc, 0, 0, 0, 0];
338
339            let mut output = vec![0.0f32; frame_size * channels];
340
341            let _ = decoder.decode(&packet, frame_size, &mut output);
342        }
343
344        let channels = 2;
345        let mut decoder = OpusDecoder::new(sampling_rate, channels).unwrap();
346
347        for frame_size in frame_sizes {
348            let toc = gen_toc(
349                OpusMode::CeltOnly,
350                frame_rate_from_params(sampling_rate, frame_size).unwrap(),
351                Bandwidth::Fullband,
352                channels,
353            );
354            let packet = [toc, 0, 0, 0, 0];
355
356            let mut output = vec![0.0f32; frame_size * channels];
357            let _ = decoder.decode(&packet, frame_size, &mut output);
358        }
359    }
360
361    #[test]
362    fn test_celt_decoder_edge_case_frame_sizes() {
363        let sampling_rate = 48000;
364        let channels = 1;
365        let mut decoder = OpusDecoder::new(sampling_rate, channels).unwrap();
366
367        let edge_sizes = [2048, 2167, 2168, 2169, 2880, 3072];
368
369        for frame_size in edge_sizes {
370            let mut output = vec![0.0f32; frame_size * channels];
371
372            let _ = decoder.decode(&[0x80, 0, 0, 0], frame_size, &mut output);
373        }
374    }
375
376    // Regression test for: "index out of bounds: the len is 48 but the index is 119"
377    // Root cause: frame_size=48 at 48kHz gives frame_rate=1000, which is not a valid
378    // Hybrid-mode frame rate but was not validated.  CELT's lm-search then silently
379    // fell back to lm=0, computed n2=120, and wrote output[119] into a 48-element
380    // slice.  Triggered via G.729-decoded PCM (8kHz) passed to a 48kHz Opus encoder
381    // without proper resampling, so the encoder received 48 samples instead of 480.
382    #[test]
383    fn test_invalid_small_frame_size_returns_error_not_panic() {
384        let mut enc = OpusEncoder::new(48000, 2, Application::Voip).unwrap();
385        enc.bitrate_bps = 64000;
386        enc.complexity = 5;
387        enc.rate_control = RateControl::Cbr;
388
389        // 48 samples at 48kHz = 1ms → frame_rate=1000, invalid for Hybrid mode.
390        let input = vec![0.0f32; 48 * 2]; // stereo interleaved
391        let mut output = vec![0u8; 256];
392
393        let result = enc.encode(&input, 48, &mut output);
394        assert!(
395            result.is_err(),
396            "encode with invalid frame_size=48 should return Err, not panic"
397        );
398    }
399
400    // Also verify that the Audio application path (always Hybrid at 48 kHz) rejects
401    // the same bad frame size.
402    #[test]
403    fn test_invalid_small_frame_size_audio_application_returns_error() {
404        let mut enc = OpusEncoder::new(48000, 1, Application::Audio).unwrap();
405        let input = vec![0.0f32; 48];
406        let mut output = vec![0u8; 256];
407
408        let result = enc.encode(&input, 48, &mut output);
409        assert!(
410            result.is_err(),
411            "Audio/48kHz encoder with frame_size=48 should return Err"
412        );
413    }
414}