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}