opus-rs
A pure-Rust implementation of the Opus audio codec (RFC 6716), ported from the reference C implementation (libopus 1.6).
Production-ready
Features
- Pure Rust — no C dependencies
- High Performance: Competitive with C libopus on x64/aarch64
#![no_std]+ noalloc: fully heap-free — runs on bare metal, RTOS, and WebAssembly with no global allocator
Quick Start
use ;
// Encode
let mut encoder = new.unwrap;
encoder.bitrate_bps = 16000;
encoder.use_cbr = true;
let input = vec!; // 20ms frame at 16kHz
let mut output = vec!;
let bytes = encoder.encode.unwrap;
// Encode PCM16 directly (issue #28): skip the f32 round-trip in the
// SILK/VoIP path. Output is byte-for-byte identical to `encode()` called
// with the same audio converted via `sample as f32 / 32768.0`.
let input_i16 = vec!; // 20ms frame at 16kHz
let bytes = encoder.encode_i16.unwrap;
// Decode
let mut decoder = new.unwrap;
let mut pcm = vec!;
let samples = decoder.decode.unwrap;
WAV Roundtrip
# Rust encoder/decoder
#![no_std] + no-alloc Support
By default opus-rs runs on std with the heap feature: the large codec state
(OpusEncoder/OpusDecoder working buffers) is allocated with Box, so the public
structs are tiny (~1 KB each) and constructing them uses almost no stack.
For #![no_std], disable the default features. The crate then builds as
#![no_std] with no alloc — fully heap-free, no global allocator required —
and every working buffer is an inline, fixed-size array (sized to the Opus worst
case); growable state uses an internal FixedVec<T, N> (a tiny Vec-like type over
[MaybeUninit<T>; N]). In that configuration the structs are large, so place them
in a static (wrap init in your own Once-like guard) or a dedicated buffer.
[]
= "0.1"
= false
= ["libm"]
The public API (OpusEncoder/OpusDecoder with encode/decode writing into caller-provided buffers) is identical in both configurations.
Feature flags
| Feature | Default | Description |
|---|---|---|
std |
yes | Enables OS-backed runtime x86 SIMD detection (CPUID). Without it the crate is #![no_std]. |
heap |
yes | Allocates the large codec state on the heap (Box), shrinking OpusEncoder/OpusDecoder to ~1 KB so they can live on ordinary stacks. Requires std. |
libm |
no | Required for #![no_std] builds — provides float math via a pure-Rust port of musl libm. Not needed when std is on. |
portable-atomic |
no | Uses the portable-atomic crate for the lazily built mode table, so the crate builds on targets without compare-and-swap like thumbv6m-none-eabi (Cortex-M0/M0+) or riscv32imc-unknown-none-elf (ESP32-C3). There you also need its critical-section feature, or --cfg portable_atomic_unsafe_assume_single_core on single-core chips. |
Runtime dependency footprint:
stdbuild → 0 deps;no_stdbuild → 1 dep (libm, pure Rust).
Notes for no_std users
-
Large structs: without
heap, every working buffer is inline, soOpusEncoder/OpusDecoderare large (~250 KB / ~175 KB each). Place them in astatic(wrap init in your ownOnce-like guard) or a dedicated buffer rather than on the stack.OpusDecoder::newstill returns the decoder through the stack, so useOpusDecoder::initto build it directly in that memory:static mut DECODER: = uninit; let decoder = init?;Test threads use a 16 MB stack (see
.cargo/config.toml). -
x86 SIMD: without
std, AVX/AVX2 dispatch falls back to compile-time detection. Build withRUSTFLAGS="-C target-feature=+avx2"to enable it. aarch64 NEON is unconditional. -
Packets are capped at the RFC 6716 maximum of 1276 bytes (the heap-free range-coder buffer is sized accordingly); standalone
RangeCoder/CELT use supports up to 2048 bytes. -
Verified targets:
thumbv7em-none-eabi(Cortex-M),wasm32-unknown-unknown, and any Linux no_std target. Withportable-atomic, alsothumbv6m-none-eabiandriscv32imc-unknown-none-elf. Check withscripts/check_no_std.sh.
Performance
Criterion benchmark (cargo bench --bench opus_vs_c_bench) with 20 samples, 100 ms warm-up, 500 ms measurement, real speech input (fixtures/answer_16k.wav), mono encode-only. All numbers below are wall-clock time for the full frame set.
vs C Opus (libopus 1.6.1) on x86-64 (AVX2/FMA)
Measured on AMD Ryzen 7 5700X, compiled with --release (opt-level=3 + ThinLTO).
| Config | Pure Rust | C Opus | Ratio |
|---|---|---|---|
| 8 kHz / 20 ms VoIP | 39.9 ms | 40.6 ms | 0.98× (Rust 2% faster) |
| 16 kHz / 20 ms VoIP | 66.8 ms | 67.1 ms | 1.00× (Rust 0.5% faster) |
| 16 kHz / 10 ms VoIP | 73.2 ms | 72.5 ms | 1.01× (within noise) |
| 48 kHz / 20 ms Audio | 25.1 ms | 28.4 ms | 0.88× (Rust 12% faster) |
| 48 kHz / 10 ms Audio | 29.7 ms | 31.2 ms | 0.95× (Rust 5% faster) |
vs C Opus (libopus 1.6.1) on Apple Silicon
Measured on Apple Silicon M-series (aarch64), compiled with --release (opt-level=3 + ThinLTO), latest run on 2026-04-23.
| Config | Pure Rust | C Opus | Ratio |
|---|---|---|---|
| 8 kHz / 20 ms VoIP | 31.47 ms | 31.20 ms | 1.01× (C 0.9% faster) |
| 16 kHz / 20 ms VoIP | 51.19 ms | 52.81 ms | 0.97× (Rust 3.1% faster) |
| 16 kHz / 10 ms VoIP | 55.69 ms | 55.49 ms | 1.00× (within noise) |
| 48 kHz / 20 ms Audio | 13.97 ms | 19.39 ms | 0.72× (Rust 28% faster) |
| 48 kHz / 10 ms Audio | 16.19 ms | 20.28 ms | 0.80× (Rust 20% faster) |
Release Notes
Unreleased
0.1.39
- Fix: a SILK-only frame's trailing redundancy now decodes, like libopus
(issue #66). A SILK-only frame signals no redundancy flag — when bits are
left for it, redundancy is on,
celt_to_silkreads as one bit, and the frame's whole tail is a 5 ms redundant CELT frame (opus_decoder.c452-479). Going to CELT it decodes from a reset CELT state, which the next CELT frame carries on, and fades into the frame's last 2.5 ms (595-603); coming from CELT it opens the frame and fades into its first 5 ms (608-617). Only the SILK-to-CELT direction setsprev_redundancy(657), which now spares the next frame its transition bridge and CELT reset. The packet-start CELT reset narrows to switches into CELT, matching libopus's reset insidemode != MODE_SILK_ONLY(551-553). SILK-only↔CELT switches (NB/MB low-bitrate streams) decoded 4-5 dB from libopus at the switch; now 132 dB both ways.
0.1.38
- Fix: a Hybrid frame's mode-switch redundancy now decodes as libopus's
does. The 5 ms redundant CELT frame (RFC 6716 §4.5.1) used to be read
but never decoded, and the frame's own CELT layer was skipped: every
Hybrid↔CELT switch glitched for 40–60 ms. The main layer now decodes from
the bytes before the redundant frame, the redundant frame decodes with all
bands (from the old CELT state going to SILK, from a reset state going to
CELT) and crossfades in with the CELT window; only a SILK-to-CELT frame's
redundancy sets
prev_redundancy(PR #58). Worst switch window vs libopus: 2 dB → 129 dB. - Fix: CELT's energy history starts at −28 dB, as libopus's reset sets it, and the bands a frame doesn't code forget their energies (PR #59). Starting at 0 made anti-collapse inject extra noise into the first two transient frames: CELT's first packets decoded 25–45 dB from libopus. Now 119 dB at the onset.
- Fix: a super-wideband Hybrid frame's CELT layer ends at band 19, from the packet's bandwidth as in libopus (PR #60). It read two bands its encoder never coded and sat 10–15 dB from libopus throughout. Now 143 dB.
- Fix: switches out of CELT reset SILK and bridge with 5 ms of the old CELT state concealed forward, as libopus does (PR #61). opus-rs kept the SILK state (and resampler) from before the CELT run and bridged with the previous frame's last 5 ms, which had already played: a ~25 ms click at every CELT→SILK/Hybrid switch. Now 132 dB at the switch.
- New:
OpusDecoder::init(slot, sampling_rate, channels)builds the decoder in place (PR #65, issue #62 item 1). Withoutheap,OpusDecoder::newreturns ~175 KB by value through the stack, which no MCU has;initwrites it straight into astatic.newgoes throughinit, so the two cannot drift.init's stack frame is 44 bytes. - New: build on targets without compare-and-swap atomics via the
optional
portable-atomicfeature (PR #64, issue #62 item 2). Enablesthumbv6m-none-eabi(Cortex-M0/M0+) andriscv32imc-unknown-none-elf(ESP32-C3); there you also pick how portable-atomic emulates CAS (itscritical-sectionfeature, or--cfg portable_atomic_unsafe_assume_single_coreon single-core chips). Nothing changes when the feature is off. - Fix: SILK CBR with in-band FEC busted packets into concealed frames
(issue #51). SILK's frame target now leaves room for the packet's LBRR
section, as libopus's does. This ports
enc_API.c's per-frame target, for mono and stereo: the moving average of LBRR bits (nBitsUsedLBRR), the bit reservoir across packets (nBitsExceeded) and the balance within a multi-frame packet (bitsBalance). The frame cap no longer takes the LBRR bits off a second time, since the rate control already counts them. At 16-48 kb/s, 20 ms and 40% loss, mono busts 0-1 packets in 50 (was up to 8; libopus 0-2) and stereo 0-2 (was up to 15). Stereo carries LBRR in 22-31 packets (was 5-20; libopus 29-30). Mono's main frame codes as well as without FEC (worst window 4.6 dB, was 0.3). Hybrid's SILK layer also pays back its overshoot of the target rate, as libopus's does. - Stereo SILK/Hybrid codes the side, as libopus does (issue #48). L and R
are resampled separately and converted to mid/side at the internal rate by
a bit-exact port of libopus's
silk_stereo_LR_to_MS: predictor estimation and quantization, stereo-width smoothing, the panned-mono and reduced-width decisions, and the mid/side bit split. A second SILK channel codes the side, with its own VAD, LBRR (in-band FEC) and rate control, and libopus's side reset after mid-only frames. On the #42 signal each channel now measures within 0.3 dB of libopus (R: +3.8 → +5.1 dB, libopus +5.1). Anti-phase input, which decoded as silence, measures +7.0 dB per channel, as libopus does. Two unrelated voices go from +1.9/+2.8 to +6.0/+9.3 dB (libopus +5.4/+8.0). Hybrid splits its SILK rate per channel, as libopus'scompute_silk_rate_for_hybriddoes. Mono output is unchanged, byte for byte. Under CBR with in-band FEC the side's LBRR at first crowded LBRR out of many packets; #51 (above) fixes that. - Fix: stereo SILK/Hybrid decoded as L ≈ −0.63·mid, R ≈ 2.63·mid (issue
#42). The encoder codes stereo SILK as the mid only, but wrote stereo
predictor index 0, which dequantizes to
pred_Q13 = (0, −13364). The decoder applies the predictor to mid-only frames too, so the left channel came out phase-inverted and the right ~8 dB hot. It now writes the zero predictor, joint symbol 12, as libopus does for itstoMonoframes. Hybrid also fed SILK the interleaved L,R input as if it were mono; it is now downmixed like SILK-only, which lifts the 24/48 kHz Hybrid mid from −2.4 dB to +5.8 dB SNR (libopus +5.9 dB). Both channels now carry the mid (L = R ≈ 0.85·mid, SNR +5.2/+3.8 dB, libopus +5.1/+5.1 dB with real side coding), until #48 (above) adds the side. - Fix: the SILK decoder's stereo predictor levels match libopus. The
half-step constant was
(1 << 16) / 10= 6553 instead of libopus'sSILK_FIX_CONST(0.1, 16)= 6554, so 45 of the 75 levels came out a few Q13 units off and libopus stereo streams decoded at ~65 dB against libopus instead of bit-exactly. The stereo state is no longer cleared on frames whose predictor is zero (libopus clears it only on a mono→stereo switch), and a lost frame keeps the last mid-only flag and conceals the side only if the last good frame coded one (dec_API.c). - Fix: SILK-only VBR ignored
bitrate_bps(issue #43). The SILK target rate came from the packet budget, which under VBR is the caller's output buffer: a 1500-byte buffer asked SILK for ~510 kb/s, so every bitrate coded the same ~87-byte packets (16 kHz, 20 ms). The rate now follows libopus 1.6'sbits_target/total_bitRate(opus_encoder.c): the requested bitrate, capped by the buffer, less the TOC byte. The buffer stays the hard cap (maxBits). At 12-64 kb/s, 16 kHz VBR now codes 21-69 bytes a packet, against libopus's 27-73. CBR and Hybrid streams are byte-identical to before. Known gap: at wideband, opus-rs codes fewer excitation bits than libopus at the same gains (0.78x libopus's size at 12 kb/s, 1.01x at narrowband). That is a separate SILK encoder divergence, tracked for follow-up. - Fix: mediumband (12 kHz) SILK decoded to noise (issue #53). The decoder
gave 12 kHz the NB/MB NLSF codebook (order 10) but the wideband LPC order
(16), so it built a 16-tap synthesis filter from ten decoded NLSFs. Every
mediumband packet, from libopus or from opus-rs, decoded to loud noise at
every output rate (-14 to -18 dB against libopus). The order and the
codebook now come from one branch, as in libopus's
decoder_set_fs.c: mono mediumband decodes bit-exactly against libopus, and stereo matches it as far as the stereo predictor allows (issue #42). - Fix: a stereo decoder rejected mono packets, and a mono decoder rejected
stereo ones (issue #62, item 3).
OpusDecoder::decodereturned "Channel count mismatch between packet and decoder" whenever a packet's ToC disagreed with the decoder, and libopus stereo encoders send mono packets at low bitrates. Both now decode as in libopus. A stereo decoder plays a mono packet on both channels. A mono decoder plays a stereo packet's mid (SILK) or the average of its channels' spectra (CELT). Channel switches mid-stream followsilk/dec_API.c, which starts the side channel and the right resampler afresh when a stream turns stereo. SILK decodes bit-exactly against libopus in every case, switches included. CELT and Hybrid agree with libopus as closely as when the channel counts match (47-62 dB). Streams whose channel count matches the decoder's decode exactly as before. CELT's clearing of the bands a frame doesn't code (#59) now covers both halves of the energy state, as libopus's does. A mono stream keeps the second half, and stale energies there made a wider frame decode its new bands as noise (-0.9 dB against libopus).
0.1.37
- Fix: starved CBR budgets (issues #45, #46). Budgets below 3 bytes per
frame (and other starved budgets) now emit libopus's TOC-only "PLC" packet
— padded under CBR, with the 40/60 ms multiframe TOC rules — instead of
failing with a range-coder error (debug builds panicked in
RangeCoder::shrink). Budgets below ~6 kb/s (9 kb/s above 20 ms) switch to CELT-only mode and code small valid packets, as libopus does (opus_encoder.c:1226-1286, 1525-1527). Known gap: at 5-6 byte CELT packets the payload's tail bits differ from libopus's, costing some SNR — tracked for follow-up. - Fix: 60 ms SILK packets cap the first two frames at 2/5 and 3/4 of the packet (enc_API.c parity, from PR #44). Without the caps a 60 ms packet's early frames could leave the last one under its no-pulse fallback cost.
- CELT transients are analysed on unfiltered history (PR #47). The tone/transient analysis saw a prefiltered overlap head against an unfiltered body, reading a comb-filter step as a transient on steady pitched input; 48 kHz Audio at 20-24 kbps lost its first ~200 ms. Now lag 0, correlation 1.00 from the first window, and only the onset frame is coded with short blocks.
Unreleased
- Fix: CELT from 24 kHz input stuck at ~20 dB SNR (issue #54). When
run_prefiltercancelled the pitch pre-filter for a frame (libopus 1.6'scancel_pitch), it restored the unfiltered input over the whole frame. libopus still fades the previous frame's comb filter out over the overlap (celt_encoder.c:1572-1584), and the decoder's post-filter always applies that fade. So every cancel right after an "on" frame left 120 samples the decoder post-filtered but the encoder never pre-filtered. From 24 kHz the zero-stuffed input is dominated by its image above 12 kHz, so the filter was cancelled whenever the pitch period was odd, in 18 of 66 frames on the issue's chirp. The SNR was 20.1 / 20.9 / 21.1 dB at 32 / 64 / 128 kbps and is now 29.4 / 36.2 / 41.9 dB. libopus 1.6.1 scores 38.8 / 41.9 dB at 64 / 128 kbps (35.2 / 41.1 with its tonality analysis off, which opus-rs does not port). The 48 kHz chirp packets are byte-identical. - Fix: CELT flagged transients on steady pitched input (issue #38). The
encoder ran
tone_detectandtransient_analysison a buffer whose overlap head was the previous frame's prefiltered signal while the body was unfiltered; libopus 1.6 analyses the unfilteredprefilter_memtail (celt_encoder.c:2017). Wherever the comb filter was active, the step at the head read as a transient: on the issue's chirp, 30 of 65 frames were coded with short blocks at every bitrate (libopus: only the onset frame). With bits to spare this was inaudible, but 48 kHzAudioat 20-24 kbps (CELT-only) lost its first ~200 ms (correlation 0.56-0.66); it is now lag 0 and correlation 1.00 from the first window, like libopus's CELT.prefilter_memis also kept current while the comb filter is off (stereo, hybrid, complexity < 5), as libopus's always-runrun_prefilterdoes.
0.1.36
- Fix: in-band FEC emitted no LBRR at CBR (issue #36). The LBRR payload
reused the main frame's quantized pulses with only the first gain index
raised, so it cost as many bits as the main frame and never fit a CBR
packet.
silk_LBRR_encodeis now ported faithfully: the excitation is re-quantized through the noise-shaping quantizer at the raised LBRR gain with its own NSQ state and gain chain, gated on speech activity, withsilk_setup_LBRR(gain increase 7, thenmax(7 − 0.2·loss, 2)) and opus_encoder.c'sdecide_fec(rate thresholds + hysteresis + bandwidth reduction). CBR mono now recovers the previous packet through libopusdecode_fecat 3.8 dB median (previously 0.0 dB: no LBRR at all). - Fix: 48 kHz SILK/hybrid encoder delay and startup (issue #38). The
encoder input was resampled with the stateless
down2/down2_3decimators plus an internal-rate delay buffer, lagging libopus's pre-skip by up to 17 samples. The encoder now runs the libopussilk_resampler(encoder direction:delay_matrix_enc+ private down-FIR, ported with thesilk_Resampler_*_COEFStables) on the API-rate input, and prefills the SILK encoder on CELT→SILK restarts. Measured with the issue's harness: Voip 32 kbps lag −5..−2 (libopus's own encoder: identical), Audio 24 kbps startup correlation 0.56 → ≥ 0.77. - Fix: panics found by fuzzing. 2.5 ms CELT frames aborted in the encoder's lm search (off-by-one: lm=0 was excluded from the valid set); 2.5/5 ms frames reached the SILK encoder, whose VAD band split is only valid for ≥ 10 ms frames — SILK-only now rejects sub-10 ms frame sizes (they are CELT-only durations in libopus) and the VAD bails out on non-multiple-of-8 lengths.
- no_std: the crate builds and the full test suite runs with
--no-default-features --features libm(RUST_MIN_STACK=33554432, seescripts/check_no_std.sh). 13 fuzz targets run clean.
0.1.35
- Fix: SIGILL on AVX-without-FMA CPUs (issue #30, PR #31). Nine kernels
compiled with
#[target_feature(enable = "avx,fma")]/"avx2,fma"were dispatched after checking only AVX/AVX2. Newcompat::x86_has_avx_fma()/x86_has_avx2_fma()probes gate them, so Sandy/Ivy Bridge and VMs that mask FMA no longer fault on the firstvfmadd. - Fix: frame-loss concealment (issue #15, PR #32). Packets of 0 or 1 bytes
were decoded using the placeholder byte's ToC, which switched the decoder's
mode/bandwidth/SILK rate and could panic in the resampler. Losses now conceal
in the previous mode, as libopus does; an empty packet conceals
frame_sizesamples instead of erroring, and a mono marker no longer errors on a stereo decoder. - Fix: in-band FEC LBRR desync (issue #27, PR #33). LBRR frames were coded
with
CODE_INDEPENDENTLY_NO_LTP_SCALINGwhile every decoder readsCODE_INDEPENDENTLY, so a voiced LBRR frame desynced the main frame that followed it. The LBRR gain bump now raises only index 0 of independently coded frames, as libopus does. - Fix: SILK decoder alignment with libopus (issue #34). Mono SILK output
read from
w_silk_buf[0][2..]instead of libopus's[1..], running one internal-rate sample early, and equal API/SILK-internal rates bypassedsilk_resampler, dropping itsinputDelay. Mono SILK now matches libopus at lag 0 at 16 kHz and 48 kHz. - Fix: 40/60 ms frames (issue #35). Frame sizes were validated by
sampling_rate % frame_size == 0, which rejected 60 ms (16.67 frames/s) before encoding. Validation is now duration-based. SILK-only carries a 60 ms frame directly (3 internal SILK frames); CELT and Hybrid, which cap a frame at 20 ms, repacketize 20 ms sub-frames into one code 1/2/3 packet, as libopus does. This also fixes the code-3 VBR packet parser, which read frame lengths and frame data interleaved instead of all lengths first (libopus layout) — a latent bug that only surfaced once the encoder emitted code 3. - Fix: 24 kHz CELT decoded to garbage (issue #37). The CELT encoder always
coded all 21 bands, while the decoder derives its end band from the TOC
(SWB = 19 at 24 kHz); coding one band more than the decoder reads
desynchronised the range coder, turning every 24 kHz CELT packet into noise.
The encoder now takes
end_bandfrom the Opus bandwidth (CELT_SET_END_BAND). It also zero-stuffs the API-rate input byresampling_factor(Fs)and scales/zeroes the MDCT output for non-48 kHz rates, as libopuscelt_preemphasis/compute_mdctsdo. 24 kHzAudionow matches libopus (per-window correlation ~1.0, was ~0.04). - Tests: new libopus-oracle suites for SILK multi-frame/FEC (#27), frame-loss concealment (#15), mono alignment (#34), 40/60 ms frames (#35) and 24 kHz encoding (#37).
0.1.34
- New:
OpusEncoder::encode_i16()— native PCM16 entry point (issue #28). Callers holding PCM16 audio no longer need to convert to f32 before encoding. In the SILK/VoIP path the integer high-pass biquad (hp_cutoff_i16) is fed directly from the input — no f32 round-trip, no[i16; 11520]temporary conversion buffer. Output is byte-for-byte identical toencode()called with the same audio converted viasample as f32 / 32768.0(SILK-only, Hybrid and CELT-only modes; verified by a parity test matrix across 8/12/16/24/48 kHz, mono/stereo, complexity 0-10, CBR/VBR, in-band FEC). The floatencode()API is unchanged. - Fix:
no_stdbuilds (--no-default-features --features libm) compile again. Themultistreamdecode module (added in 0.1.33) usesVec-based plumbing and is now gated behind the defaultstdfeature; the core codec remains heap-freeno_std. - Zero warnings:
cargo checkandcargo clippy --all-targetsare clean for the default andno_stdfeature sets (dead CELT state fields removed, tests/examples/benches lint-clean).
0.1.29
- Default build is now heap-backed (issue #12). A new
heapfeature (on by default, requiresstd) allocates the large codec state withBox, shrinkingOpusEncoder/OpusDecoderfrom ~250 KB / ~175 KB to ~1 KB each. Constructing, encoding and decoding now fit on a 768 KiB stack (previouslyOpusEncoder::newalone needed ~850 KiB in release / ~2 MiB in debug, overflowing small-stack threads such as Windows' 1 MiB main thread). Disable default features for the heap-free#![no_std]build, which keeps the inline layout. - Tests: add
tests/stack_usage_test.rsasserting the structs stay <4 KiB withheap, >100 KiB without it, and that a full construct + encode + decode round-trip fits in a 768 KiB stack thread.
0.1.28
- Fix: high-bitrate bitstream interop with libopus (issue #11). The ported
BAND_ALLOCATIONtable was missing one200in its final row (7 entries instead of 8), shifting the whole row. That row is only selected by the allocator at high bitrates, so encoded stereo streams above ~160 kbps decoded to garbage in libopus (and the crate decoder mis-decoded libopus streams at the same rates). Table now byte-identical to libopus. - Fix:
celt_pvq_u/celt_pvq_vpanic fork >= 129.compute_u's fixed-size buffer is now domain-checked (k <= MAX_PVQ_K = 128); out-of-domain calls returnu32::MAXinstead of panicking or silently wrapping. - Fix: silent u32 overflow in PVQ codebook sizes.
compute_u/unext/celt_pvq_vuse saturating arithmetic, so an out-of-domain(n, k)can never produce a plausible-but-wrong codebook size. - Fix:
celt_pvq_u_lookuprow-extent aliasing. Lookups whosemax(n, k)exceeds a row's actual coverage now compute instead of reading the next row's block. - Fix:
OpusEncoder::encodenow returns an error when the range coder overflows its packet budget (previously bytes were silently dropped). - Tests: add full-domain PVQ table verification vs exact big-int recurrence, out-of-domain saturation checks, and a high-bitrate (128–192 kbps) stereo interop test that cross-checks against libopus.
0.1.27
#![no_std]with noalloc: the crate is now fully heap-free — no global allocator is required. AllVec/Boxworking buffers were replaced with an internalFixedVec<T, N>(inline[MaybeUninit<T>; N]);std::sync::LazyLockwas replaced with a heap-freeOnceCell; no_std float math (sin/cos/sqrt/…) is routed through an optionallibmfeature (pure-Rust musl port).- Verified on
thumbv7em-none-eabi(Cortex-M),wasm32-unknown-unknown, and Linux no_std.scripts/check_no_std.shreproduces. - Feature flags:
std(default) → 0 runtime deps;default-features = false, features = ["libm"]→ 1 dep (libm). - Encoder/decoder structs are inline and large (~250 KB / ~175 KB); place them in a
staticor dedicated buffer on constrained targets.
- Verified on
- Performance: unchanged vs 0.1.26 (measured within ±0.5% on
opus_real; no per-frame allocation overhead). - Conformance: RFC 6716 1276-byte packet cap enforced on the Opus path; the standalone
RangeCoder/CELT buffer supports up to 2048 bytes. wav_testexample now tags output files by build (_std/_nostd) for A/B listening across build modes.
License
See COPYING for the original Opus license (BSD-3-Clause).
Links
- RustPBX: https://github.com/restsend/rustpbx
- RustRTC: https://github.com/restsend/rustrtc
- SIP Stack: https://github.com/restsend/rsipstack
- Rust Voice Agent: https://github.com/restsend/active-call