opus1 0.3.0

Pure-Rust implementation of the Opus audio codec (RFC 6716), following libopus 1.x
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
# opus1

A pure-Rust implementation of the [Opus audio codec](https://opus-codec.org/) (RFC 6716), ported from the reference C implementation (libopus 1.6). The name says what it follows: the Opus 1.x bitstream and the behaviour of libopus 1.x.

opus1 is a hard fork of [opus-rs](https://github.com/restsend/opus-rs), taken at version 0.1.37. It is developed separately at [github.com/RealEffect/opus1](https://github.com/RealEffect/opus1); opus-rs continues on its own. Code that used `opus_rs::` uses `opus1::` (see the [0.2.1 notes](#021-2026-10-27) for the API changes):

```toml
[dependencies]
opus1 = "0.3"
```

A systematic introduction to Opus and to this library is in [docs/](docs/README.md).

> **Production-ready**

## Features

- **Pure Rust** — no C dependencies
- **High performance:** on Apple M4 Pro, decoding takes 0.56-0.81x and encoding 0.48-0.71x the time of C libopus 1.6.1 built at `-O2`, the optimization level of libopus's own build, at every complexity, with the speech and music analysis in both from complexity 7 on. Against the Homebrew build of libopus 1.6.1, which behaves like a build at `-Os` and takes about 24 % more time to encode and 12 % more to decode, the ranges are 0.49-0.76x and 0.37-0.63x. opus1 is also faster than libopus on the big and the small cores of an Android phone, and on average over the bench set on an Intel Core i7-12700 (x86-64) (see [Performance](#performance))
- **Multichannel:** multistream encoder and decoder (mapping families 0, 1, 2 and 255), interoperable with libopus
- **libopus controls:** the encoder settings, their defaults and the values they refuse follow libopus's `OPUS_SET_*` requests (automatic bitrate, VBR and its constraint, complexity, in-band FEC, packet loss, forced mode, bandwidth and channels, signal type, input bit depth, DTX, frame duration from 2.5 to 120 ms), with one exception: a forced stereo count on a mono encoder is kept, with no effect, where libopus refuses it
- **Packets:** the `packet` module parses, writes and pads packets and has libopus's repacketizer. The `extensions` module reads and writes the padding extensions of libopus 1.5 and later
- **64-bit ARM and x86-64:** on 64-bit ARM the output is checked bit for bit against libopus 1.6.1. On x86-64 it is correct but not bit-exact with libopus: opus1's x86 vector kernels do not follow libopus's, and libopus's own x86 output depends on the C compiler (see [docs/guide/10](docs/guide/10-differences-from-libopus.md#platforms))
- **The speech and music analysis of libopus:** from complexity 7 on, the encoder estimates whether the input is speech or music and which bandwidth it uses, as libopus does, with the same results to the last bit on 64-bit ARM. CELT-only packets at complexity 7 to 10 are libopus's byte for byte
- **The neural decoder features of libopus 1.5 and 1.6 (optional):** deep packet loss concealment, the LACE and NoLACE speech enhancement of SILK, and the bandwidth extension of SILK wideband to fullband. Each is compiled only with its cargo feature and runs only when the decoder complexity is raised, as in libopus: `set_complexity(5)` for deep PLC (packet loss concealment), 6 for LACE, 7 for NoLACE, and 4 with `set_osce_bwe(true)` for the bandwidth extension. On 64-bit ARM the output is that of libopus 1.6.1 built with these features (without loop vectorization) to the last bit.
- **Deep redundancy (DRED, optional):** with the cargo feature `dred`, the encoder adds a compressed description of up to about one second of earlier speech to each packet (`set_dred_duration`, libopus `OPUS_SET_DRED_DURATION`), and the decoder rebuilds a burst of lost packets from the next packet that arrives (`OpusDredDecoder`, `OpusDred`, `OpusDecoder::dred_decode`). CELT-only packets with DRED, and the DRED data in SILK and Hybrid packets with the `Audio` application and VBR, are those of libopus 1.6.1 built with DRED, byte for byte. Decoding gives libopus's output to the last bit.
- **`#![no_std]` + no `alloc`:** fully heap-free — runs on bare metal, RTOS, and WebAssembly with **no global allocator**. With a global allocator, the feature `alloc` adds the multistream module to a `no_std` build

## Quick Start

```rust
use std::num::NonZeroU32;
use opus1::{Application, Bitrate, Channels, OpusDecoder, OpusEncoder, SampleRate};

// Encode. Without `set_bitrate` the encoder uses libopus's automatic
// bitrate (`Bitrate::Auto`, 19 kb/s for 16 kHz mono in 20 ms frames).
// The setters refuse what libopus refuses, for example a complexity of 11
// (the one exception is in the documentation of `set_force_channels`).
let mut encoder = OpusEncoder::new(SampleRate::Hz16000, Channels::Mono, Application::Voip).unwrap();
encoder.set_bitrate(Bitrate::BitsPerSecond(NonZeroU32::new(16000).unwrap()));
encoder.set_vbr(false); // constant bitrate: every packet has 40 bytes
assert!(encoder.set_complexity(11).is_err());

let input = vec![0.0f32; 320]; // 20ms frame at 16kHz
let mut output = vec![0u8; 256];
let bytes = encoder.encode(&input, 320, &mut output).unwrap();
assert_eq!(bytes, 40);

// Encode PCM16 directly. As libopus `opus_encode` does, this codes with an
// input precision of 16 bits: the output is byte-for-byte identical to
// `encode()` after `set_lsb_depth(16)`, called with the same audio
// converted via `sample as f32 / 32768.0`.
let input_i16 = vec![0i16; 320]; // 20ms frame at 16kHz
let bytes = encoder.encode_i16(&input_i16, 320, &mut output).unwrap();

// Decode. `decode` writes the samples it returns and nothing after them.
let mut decoder = OpusDecoder::new(SampleRate::Hz16000, Channels::Mono).unwrap();
let mut pcm = vec![0.0f32; 320];
let samples = decoder.decode(&output[..bytes], 320, &mut pcm).unwrap();
assert_eq!(samples, 320);
```

The public API is the crate root and the modules `packet`, `extensions` and
`multistream` (which needs the feature `alloc`, on by default through `std`).
The crate root holds the codecs `OpusEncoder` and `OpusDecoder`, the setting
types `SampleRate`, `Channels`, `Application`, `Bandwidth`, `Bitrate`,
`ForcedMode`, `FrameDuration`, `Signal` and `InbandFec`, the error types
`Error` and `ErrorKind`, the function `pcm_soft_clip` and the constant
`VERSION`. The neural network features add `DnnBlob` and
`DnnModels`, and `dred` adds `OpusDredDecoder`, `OpusDred`, `DredParse` and
`DredProcessing`. [docs/guide/06](docs/guide/06-using-the-library.md) shows the settings and
recipes for calls, voice agents and music.

Decoding to 16-bit PCM, and recovering a lost packet from the in-band
forward error correction (FEC) data of the next one:

```rust
use std::num::NonZeroU32;
use opus1::{Application, Bitrate, Channels, InbandFec, OpusDecoder, OpusEncoder, SampleRate};

// Three 20 ms packets of a 180 Hz tone at 16 kHz. Each packet after the
// first also carries a low-bitrate copy of the frame before it (in-band FEC).
let mut encoder = OpusEncoder::new(SampleRate::Hz16000, Channels::Mono, Application::Voip).unwrap();
encoder.set_bitrate(Bitrate::BitsPerSecond(NonZeroU32::new(24000).unwrap()));
encoder.set_inband_fec(InbandFec::On);
encoder.set_packet_loss_perc(10).unwrap();
let tone: Vec<f32> = (0..3 * 320)
    .map(|i| 0.3 * (2.0 * std::f32::consts::PI * 180.0 * i as f32 / 16000.0).sin())
    .collect();
let mut packets = Vec::new();
for frame in tone.chunks_exact(320) {
    let mut packet = vec![0u8; 256];
    let len = encoder.encode(frame, 320, &mut packet).unwrap();
    packet.truncate(len);
    packets.push(packet);
}

let mut decoder = OpusDecoder::new(SampleRate::Hz16000, Channels::Mono).unwrap();
let mut pcm16 = vec![0i16; 320];
decoder.decode_i16(&packets[0], 320, &mut pcm16).unwrap(); // soft-clipped like opus_decode
// packets[1] is lost. packets[2] has arrived and carries a copy of it.
let mut lost = vec![0.0f32; 320];
decoder.decode_fec(&packets[2], 320, &mut lost).unwrap(); // the frame before packets[2]
decoder.decode_i16(&packets[2], 320, &mut pcm16).unwrap();
```

Float output is not clipped to [-1, 1], as in libopus's `opus_decode_float`.

### Multichannel

```rust
use std::num::NonZeroU32;
use opus1::{Application, Bitrate, SampleRate};
use opus1::multistream::{MultistreamEncoder, MultistreamDecoder};

// 5.1 at 48 kHz in Vorbis channel order (mapping family 1).
let mut encoder = MultistreamEncoder::new_surround(SampleRate::Hz48000, 6, 1, Application::Audio).unwrap();
encoder.set_bitrate(Bitrate::BitsPerSecond(NonZeroU32::new(256_000).unwrap()));
let layout = encoder.layout().clone(); // streams, coupled streams and mapping for the Ogg header

let pcm = vec![0.0f32; 960 * 6]; // 20 ms, interleaved
let mut packet = vec![0u8; 4000];
let len = encoder.encode(&pcm, 960, &mut packet).unwrap();

let mut decoder = MultistreamDecoder::from_layout(SampleRate::Hz48000, &layout).unwrap();
let mut out = vec![0.0f32; 960 * 6];
decoder.decode(&packet[..len], 960, &mut out).unwrap();
// `out` holds the channels in the same Vorbis order. WAV files use another
// order; reordering and downmixing are left to the application.
```

`cargo run --release --example multistream` encodes and decodes a 5.1 test
signal and prints the signal-to-noise ratio of each channel.

### WAV Roundtrip

```bash
# Rust encoder/decoder
cargo run --example wav_test
```

## `#![no_std]` + no-`alloc` Support

By default opus1 runs on `std` with the **`heap` feature**: the large codec state
(`OpusEncoder`/`OpusDecoder` working buffers) is allocated with `Box`, so the public
structs are small (about 2.6 KB for `OpusEncoder`, 0.1 KB for `OpusDecoder`) 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.

```toml
[dependencies.opus1]
version = "0.3"
default-features = false
features = ["libm", "analysis"]
```

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]`. Turns on `alloc`. |
| `alloc`  | yes (through `std`) | Uses a global allocator without the rest of the standard library. Adds the `multistream` module, and lets the `packet` functions keep more than 32 extensions in one call. For `no_std` with an allocator: `default-features = false, features = ["alloc", "libm"]`. |
| `heap`   | yes     | Allocates the large codec state on the heap (`Box`), shrinking `OpusEncoder`/`OpusDecoder` to a few KB so they can live on ordinary stacks. Turns on `alloc`, so it also works in a `no_std` build with a global allocator: `default-features = false, features = ["heap", "libm"]`. |
| `analysis` | yes   | The speech and music analysis of the encoder (libopus `analysis.c`), which runs from complexity 7 on. Without it the encoder decides as libopus does below complexity 7, and its state is about 26 KB smaller. Works with and without `std`. |
| `deep-plc` | no    | Deep packet loss concealment (PitchDNN, the PLC network, FARGAN) with its built-in weights (1,432,768 bytes). Runs at decoder complexity 5 and above. |
| `osce`   | no      | LACE and NoLACE, the speech enhancement of SILK (decoder complexity 6 and 7; weights 1,573,440 bytes). Turns on `deep-plc`, as libopus's `--enable-osce` does. |
| `osce-bwe` | no    | The bandwidth extension of SILK wideband to 48 kHz fullband output, switched on with `set_osce_bwe` (weights 685,888 bytes). Turns on `osce`. |
| `dred`   | no      | Deep redundancy (DRED): the encoder writes DRED data (`OpusEncoder::set_dred_duration`), the decoder rebuilds lost packets from it (`OpusDredDecoder`, `OpusDred`, `OpusDecoder::dred_decode`). Embeds the RDOVAE encoder and decoder (weights 565,440 bytes). Turns on `deep-plc`, as libopus's `--enable-dred` does. |
| `dnn`    | no      | The code that the neural network features share (layer kernels, weight blob parser). `deep-plc`, `osce`, `osce-bwe` and `dred` turn it on. Alone it adds no public API: `DnnBlob` and `set_dnn_blob` come with `deep-plc`. |
| `test-libopus-dnn` | no | Test only: compiles the tests that compare with a libopus built with its neural network code (see [docs/guide/09](docs/guide/09-testing.md#tests-of-the-neural-network-code)). |
| `__internals` | no | Test only, outside semantic versioning: adds the hidden module `opus1::internals` for the tests, benchmarks, examples and fuzz targets of the repository (see [docs/guide/09](docs/guide/09-testing.md#how-the-tests-reach-internal-code)). Applications never enable it. |
| `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 | For targets without atomic compare-and-swap, such as `thumbv6m-none-eabi` (Cortex-M0/M0+) or `riscv32imc-unknown-none-elf` (ESP32-C3): the shared tables are built with the atomics of the [`portable-atomic`](https://crates.io/crates/portable-atomic) crate. There portable-atomic also needs its `critical-section` feature, or `--cfg portable_atomic_unsafe_assume_single_core` on a single-core chip (see [docs/guide/06](docs/guide/06-using-the-library.md#targets-without-compare-and-swap)). |

> Runtime dependency footprint: `std` build → **0 deps**; `no_std` build → **1 dep** (`libm`, pure Rust), 2 with `portable-atomic`.

### Notes for `no_std` users

- **Large structs:** without `heap`, every working buffer is inline, so
  `OpusEncoder` and `OpusDecoder` are large: on aarch64 the encoder takes
  about 154 KB with the `analysis` feature and 128 KB without it, and the
  decoder about 52 KB. Place them in a
  `static` (wrap init in your own `Once`-like guard) or a dedicated buffer rather
  than on the stack. `OpusEncoder::new` and `OpusDecoder::new` return the codec
  through the stack; `OpusEncoder::init` and `OpusDecoder::init` build it
  directly in memory you provide, with less than 16 KB of stack (see
  [docs/guide/06](docs/guide/06-using-the-library.md#building-the-encoder-and-the-decoder-in-place)):

  ```rust
  use core::mem::MaybeUninit;
  use opus1::{Application, Channels, OpusDecoder, OpusEncoder, SampleRate};

  static mut ENCODER: MaybeUninit<OpusEncoder> = MaybeUninit::uninit();
  static mut DECODER: MaybeUninit<OpusDecoder> = MaybeUninit::uninit();

  // SAFETY: run once, while nothing else uses `ENCODER` and `DECODER`.
  let encoder = OpusEncoder::init(unsafe { &mut *&raw mut ENCODER }, SampleRate::Hz48000, Channels::Stereo, Application::Audio)?;
  let decoder = OpusDecoder::init(unsafe { &mut *&raw mut DECODER }, SampleRate::Hz48000, Channels::Stereo)?;
  # Ok::<(), opus1::Error>(())
  ```

  The memory of such a codec is never freed, which is what a `static` wants.
  With `heap`, a second `init` on the same slot leaks the heap memory of the
  first codec (see the `init` documentation). The test suite of this build
  needs big test-thread stacks: `scripts/check_no_std.sh` sets
  `RUST_MIN_STACK=33554432` (32 MiB).
- **x86 SIMD:** without `std`, AVX/AVX2 dispatch falls back to compile-time detection. Build with `RUSTFLAGS="-C target-feature=+avx2,+fma"` to enable it (AVX2 includes AVX and SSE4.1; FMA is separate). aarch64 NEON is unconditional.
- **Frames are capped** at the RFC 6716 maximum of 1275 bytes (the heap-free range-coder buffer is sized accordingly). A packet of 40 to 120 ms holds up to six such frames.
- **Verified targets:** `thumbv7em-none-eabi` (Cortex-M), `wasm32-unknown-unknown`, and any Linux no_std target; with `portable-atomic`, also `thumbv6m-none-eabi` (Cortex-M0/M0+). Check with `scripts/check_no_std.sh`.

## Performance

Measured with `examples/perf_harness.rs` (`cargo run --release --example perf_harness -- bench`):
28 scenarios, 6 s clips of speech and of a synthetic full-band music signal,
15 repetitions per operation with opus1 and libopus interleaved. Both decoders
decode the same libopus-encoded packets. The libopus encoder is forced to the
same mode, bandwidth and channel count as opus1. The table gives the median
time per 20 ms frame (10 ms or 2.5 ms where named) and the ratio opus1 / libopus
(below 1 means opus1 is faster).

### vs C libopus on Apple M4 Pro (aarch64), default release profile

Measured on 2026-10-18 with the code of commit `f952b46` (the speech and music
analysis and DTX on `d541821`). Both C references are the release libopus
1.6.1. The main one is built at `-O2`, the optimization level of libopus's own
build: `scripts/android/build-libopus.sh --host --rtcd --src <1.6.1 source>`
builds it with Apple clang and the flags of libopus's `configure`, plus the
run-time CPU detection that `configure` uses on Android. With that detection
libopus's SILK encoder uses its NEON quantizer, as on a phone. The second ratio
column is the Homebrew libopus 1.6.1, which the tests link. It behaves like a
build at `-Os` and calls the C version of that quantizer. The code before and
after the analysis ran in turns, each linked with each libopus build: four
harness programs, three passes, while other programs loaded the machine.

| Scenario | opus1 encode µs | Ratio vs `-O2` | Ratio vs Homebrew | opus1 decode µs | Ratio vs `-O2` | Ratio vs Homebrew |
|---|---:|---:|---:|---:|---:|---:|
| SILK NB 8 kHz mono 12 kb/s, complexity 0 | 9.1 | 0.71 | 0.63 | 2.0 | 0.64 | 0.62 |
| SILK WB 16 kHz mono 20 kb/s, complexity 10 | 46.7 | 0.52 | 0.39 | 3.9 | 0.62 | 0.59 |
| SILK WB 16 kHz stereo 32 kb/s, complexity 9 | 90.7 | 0.55 | 0.39 | 7.0 | 0.61 | 0.57 |
| Hybrid SWB 24 kHz mono 24 kb/s, complexity 9 | 62.9 | 0.51 | 0.37 | 7.3 | 0.60 | 0.53 |
| Hybrid FB 48 kHz stereo 40 kb/s, complexity 9 | 130.3 | 0.56 | 0.40 | 13.9 | 0.56 | 0.49 |
| CELT FB 48 kHz mono 64 kb/s, complexity 0 | 11.8 | 0.62 | 0.57 | 8.3 | 0.75 | 0.70 |
| CELT FB 48 kHz mono 64 kb/s, complexity 10 | 24.8 | 0.50 | 0.43 | 9.8 | 0.73 | 0.61 |
| CELT FB 48 kHz stereo 128 kb/s, complexity 5 | 35.7 | 0.54 | 0.46 | 19.2 | 0.75 | 0.60 |
| CELT FB 48 kHz stereo 128 kb/s, complexity 10 | 58.0 | 0.58 | 0.46 | 19.2 | 0.75 | 0.60 |
| CELT FB 48 kHz stereo 128 kb/s, 2.5 ms, complexity 9 | 11.1 | 0.68 | 0.59 | 3.2 | 0.81 | 0.74 |

From complexity 7 on, for input at 16 kHz or more, both encoders run the speech
and music analysis, so every ratio compares the same work. opus1's analysis
takes 0.26 to 0.28 of the time of libopus's on this machine and adds 2.1 to
3.9 µs per 20 ms frame. The C SILK encoder in a float build of libopus is the
floating-point one; opus1 ports the fixed-point one.

Over all 28 scenarios of the bench set (geometric means), opus1 takes 0.572 of
the encode time and 0.669 of the decode time of libopus 1.6.1 at `-O2`, and
0.461 and 0.598 of the Homebrew build. Before the analysis, when only libopus
ran it, encoding took 0.546 and 0.443 in the same passes. The full table is in
[docs/guide/08-performance.md](docs/guide/08-performance.md#latest-results), and
[The C reference](docs/guide/08-performance.md#the-c-reference) explains the builds.

On an Android phone (MediaTek MT6833), against libopus 1.6.1 built with the
NDK's clang at `-O2` and the flags of libopus's `configure` for Android, opus1
is faster in all 30 scenarios on both kinds of core, also where both encoders
run the speech and music analysis (geometric means over the bench set, commit
`f952b46`): encoding 0.668 and decoding 0.768 of libopus's time on the
Cortex-A76, 0.698 and 0.824 on the in-order Cortex-A55. Before the analysis,
when only libopus ran it, encoding took 0.625 and 0.650. opus1's analysis
alone takes 0.36 to 0.40 of the time of libopus's on these cores. See
[docs/guide/08-performance.md](docs/guide/08-performance.md#on-an-android-phone).

The latest speed work ("round 8" in the performance document) made the
release libopus 1.6.1, built from Xiph's release file, the C reference on the
Mac and on the phone. It also went on with the work for in-order cores, in
these parts:

- the SILK delayed-decision quantizer;
- the PVQ search and the bit allocation of CELT;
- the pitch analysis of the CELT encoder;
- the FFT, the inverse MDCT and the de-emphasis of the CELT decoder.

The output did not change. Against the code before it, run in turns
(geometric means over the bench set):

| | Apple M4 Pro | Cortex-A76 | Cortex-A55 |
|---|---:|---:|---:|
| Encoding | 3.1-3.5 % less time | 1.5 % less | 1.5-1.8 % less |
| Decoding | 1.9-2.2 % less | 1.4 % less | 1.7-1.8 % less |

CELT encoding and decoding take 2.3-4.2 % less time on all three cores. Most
of that comes from the PVQ search and the allocation. On Apple M4 Pro, SILK and
Hybrid encoding at complexity 6 and 7 take 3.8-6.4 % less time. That removes
the slowdown that round 7 left there. See
[docs/guide/08-performance.md](docs/guide/08-performance.md#round-8-more-work-for-in-order-cores).

Round 7 before it was aimed at the in-order Cortex-A55. It rewrote the pitch
analysis of the CELT encoder, the
post-filter of the CELT decoder, the SILK noise-shaping quantizers, and the PVQ
search, the spreading rotation and the bit allocation of the CELT encoder. The
output did not change. Against the code before it, run in turns on the same
machine (geometric means over the bench set), encoding takes 10.8-11.1 % less
time on the Cortex-A55, 7.5 % less on the Cortex-A76 and 2.5-2.8 % less on
Apple M4 Pro. Decoding takes 1.9-2.0 % less on the Cortex-A55, 1.0 % less on
the Cortex-A76 and about the same time (within 0.4 %) on Apple M4 Pro. From
the Cortex-A76 to the Cortex-A55, opus1's encoding now grows 3.02 times slower
and libopus's 2.90 times; before this work opus1's grew 3.14 times. See
[docs/guide/08-performance.md](docs/guide/08-performance.md#round-7-work-for-in-order-cores).

Against opus-rs 0.1.37 on the same machine (change of the opus1 / libopus
ratio, Homebrew libopus), encoding takes 33-56 % less time for SILK and Hybrid
and 29-49 % less for mono CELT; decoding takes 35-62 % less in every scenario.
Stereo CELT encoding takes 24-30 % less time at complexity 0 and 5. From
complexity 5 it now runs libopus's stereo pitch pre-filter and, from complexity
8, its second quantization pass per stereo band (`theta_rdo`). So at complexity
9 and 10 it takes 2-22 % more time than in 0.1.37, while staying at 0.46-0.59
of the Homebrew libopus's time.

Build settings that help (measured, see
[docs/guide/06-using-the-library.md](docs/guide/06-using-the-library.md#build-settings-for-speed)):
`lto = "fat"`, `codegen-units = 1` and `panic = "abort"` in the application's
release profile take 1.7 % off decoding on Apple M4 Pro and 1.1-1.5 % off
encoding on the Cortex-A76. For Android, `-C target-cpu=cortex-a55` orders the
instructions for the in-order Cortex-A55. With the ARMv8.2-A extensions turned
off again (so the program runs on every 64-bit ARM phone) and with that
profile, it takes 4.4-4.5 % off encoding and 1.3-1.5 % off decoding on the
Cortex-A55, and 0.5-1.4 % off both on the Cortex-A76. Building for the
Cortex-A76 instead makes the Cortex-A55 15 % slower. `-C target-cpu=native`
changes nothing measurable on Apple silicon.

On aarch64 the FFT uses the FEAT_FCMA complex multiply-add instructions (part of
the Apple aarch64 baseline). Other aarch64 targets detect them at run time with
`std`. On aarch64 the FFT and MDCT give the same bits as the arm64 build of libopus.

### x86-64 (Intel Core i7-12700)

Measured on 2026-10-21 on an Intel Core i7-12700 (AVX2, FMA and AVX-VNNI, no
AVX-512) against libopus 1.6.1 at `-O2` (geometric means over the bench set):
opus1 takes 0.90 of libopus's time to encode and 0.81 to decode under Linux in
WSL2, and 0.88 and 0.80 natively on Windows. Low-complexity SILK encoding took
1.04 to 1.10 of libopus's time. On x86-64 the output is correct but not
bit-exact with libopus (see
[docs/guide/10](docs/guide/10-differences-from-libopus.md#platforms)).

### Earlier measurement: x86-64 (AVX2/FMA), opus-rs 0.1.x

Measured on AMD Ryzen 7 5700X with the earlier `opus_vs_c` benchmark (mono encode only,
complexity 0).

| 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**) |

## Release Notes

### 0.3.0 (2026-10-29)

One change of this release breaks the API (application programming
interface), so its version is 0.3.0.

**Breaking changes**
- The constant `opus1::LIBOPUS_VERSION` is removed. opus1 is compiled into the
  program, so each opus1 version follows one fixed libopus release. The release
  notes name that release: this version, like 0.2.1, follows libopus 1.6.1.

**Speed** ([details](docs/guide/08-performance.md#16-bit-and-multistream-output)).
The times are from an Apple M4 Pro, unless a line names the Intel Core
i7-12700.
- 16-bit decoding (`OpusDecoder::decode_i16`): the soft clipper first checks
  with one vector pass whether any sample is beyond ±1, as libopus does on
  64-bit ARM. When none is, it skips its search. 16-bit decoding takes 3.4 %
  less time.
- `MultistreamDecoder`: the copy of each channel into the output uses one
  loop counter and checks no index inside the loop. The rounding to 16 bits
  is one instruction on 64-bit ARM. Multistream 16-bit decoding takes 4.4 %
  less time. Multistream float decoding takes 2.2 % less. On the M4 Pro,
  moving the same code to other addresses alone has changed a time by up to
  2.3 %, so this gain may not be real
  ([code placement](docs/guide/08-performance.md#code-placement)).
- From the code before both changes to the code after them, 16-bit decoding
  takes about 3 % less time and multistream 16-bit decoding 7.9 % less.
- Concealment of lost packets
  ([details](docs/guide/08-performance.md#concealment-of-lost-packets)):
  - CELT (Constrained Energy Lapped Transform, the transform coder of Opus):
    `celt_iir` is the filter that turns the extrapolated excitation back
    into a signal. It now computes four output samples in one pass over its
    coefficients, as libopus does. A lost CELT packet after another lost one
    took 1.1 to 1.5 times libopus's time and now takes 0.63 to 0.69 times.
    The first lost packet of a run takes 0.37 to 0.46 times. On the
    i7-12700: 0.72 to 0.79 times for later losses, and 0.99 to 1.16 times
    for the first.
  - SILK: the synthesis filters of the comfort noise and of the concealment
    use the faster filter of the SILK decoder. A lost SILK packet takes 56 %
    (8 kHz) to 68 % (16 kHz) less time. It now takes 0.35 to 0.42 times
    libopus's time. A lost Hybrid packet takes 51 % less. On the i7-12700,
    with both concealment changes: 35 % (8 kHz) to 45 % (16 kHz) less for
    SILK, and 29 % less for Hybrid.
- 24-bit output (`decode24`): the rounding is one instruction on 64-bit ARM,
  in blocks of 4 samples. Multistream 24-bit decoding takes 6 % less time.
- On processors other than 64-bit ARM, the soft clipper also skips its
  search when the search would find nothing, so the output stays that of
  libopus's C version. On x86-64 the range check uses SSE2 (Streaming SIMD
  Extensions 2, where SIMD means single instruction, multiple data). On the
  i7-12700, 16-bit decoding takes 2.8 % less time, and
  multistream 16-bit decoding 5.6 % less, the multistream change above
  included.
- Smaller changes in the encoder, the SILK decoder and the packet functions
  ([details](docs/guide/08-performance.md#less-copying-and-zero-filling)):
  less clearing and copying of buffers. `multistream_unpad` of a packet of 2
  streams is 2.7 times as fast. Reading the extensions of a packet with one
  extension, padded to 400 or to 1,200 bytes, is 5.2 or 6.4 times as fast.
  `pad` allocates nothing when it keeps up to 32 extensions in a padding of
  up to 1,275 bytes.
- When a packet is lost, SILK now conceals straight into the output. libopus
  does the same, except for pieces shorter than 10 ms, which it decodes into
  a buffer of its own. The decoder no longer has the buffer that SILK wrote
  into before a copy: 7,688 bytes less per decoder (52,072 bytes without
  `heap`), and one heap allocation less with `heap`. Without `heap`,
  `OpusDecoder::new` needs 145 KB of stack instead of 177 KB in a release
  build for aarch64 (the smallest thread stack that works, measured on
  2026-10-29).

**Fixes**
- `pcm_soft_clip` on 64-bit ARM gives the bits of libopus also for input with
  a NaN (not a number) and for a `declip_mem` value beyond ±2. As in libopus
  there, both can then leave samples beyond ±1, and also beyond ±2.
- With the feature `dred` (DRED, deep redundancy): packets of several frames
  (CELT and Hybrid at 40 and 60 ms, every mode at 80 to 120 ms) at high
  bitrates now carry the DRED data that libopus puts there. In libopus the
  DRED bytes of such a packet are taken from the share of every frame and
  given to the first coded frame. Its room may then be more than 1,276
  bytes. The frame itself is coded in at most 1,276 bytes, and the DRED data
  (at most 1,000 bytes) goes into the rest of the room. opus1 cut the room
  to 1,276 bytes, which shortened or dropped the DRED data.
  - At a constant bitrate, CELT-only packets are now libopus's. In Hybrid
    and SILK-only packets the room for the DRED data changes. Their SILK
    layer is opus1's fixed-point one, so these packets are not libopus's
    anyway.
  - At a variable bitrate the packets change when the buffer holds more than
    about 2 × 1,276 bytes. At the largest bitrate (`Bitrate::Max`),
    CELT-only packets of 40 to 100 ms are now libopus's, and so are 120 ms
    packets in a buffer of at most 6 × 1,276 bytes. Other packets still
    differ on purpose: opus1 does not limit each frame to its share of the
    bitrate. With DRED a 120 ms packet can then be longer than 6 × 1,276
    bytes when the buffer allows it, by at most about 1,010 bytes. In a test,
    10 of 50 such packets were longer, by at most 26 bytes.
- Without `std`, opus1 needs `libm` 0.2.6 or later, the first version with
  `rint` and `rintf`. `Cargo.toml` asked only for 0.2, so a project that had
  an older `libm` locked failed to build.

### 0.2.1 (2026-10-27)

The first release of opus1. Version 0.2.0 was used only during development and
was never published.

The crate is renamed from `opus-rs` to `opus1`: code that used `opus_rs::` now
uses `opus1::`. This release follows libopus 1.6.1 and was checked against it
throughout: every fix has a test that compares opus1 with the C library under
the same settings. The full list of changes, with the measurements behind each
fix, is in [docs/changelog-0.2.1.md](docs/changelog-0.2.1.md).

**Breaking changes** (what code written for opus-rs 0.1.37 must change; API
added since 0.1.37 is under New features)
- The crate name: `opus1` instead of `opus-rs`.
- Calls that can fail return `Result<T, opus1::Error>` instead of
  `Result<T, &'static str>`. `Error::kind()` gives an `ErrorKind`, one per
  libopus error code (`BadArg`, `BufferTooSmall`, `InvalidPacket`, ...), and
  `message()` the text. Compare kinds, not messages. `Error` implements
  `core::error::Error`.
- Sampling rates and channel counts have their own types, `SampleRate`
  (`Hz8000` to `Hz48000`) and `Channels` (`Mono`, `Stereo`). The constructors
  of `OpusEncoder` and `OpusDecoder` take them. `SampleRate::try_from` and
  `Channels::try_from` check a number.
- The settings of `OpusEncoder` are methods instead of public fields:
  `set_bitrate(Bitrate)`, `set_complexity(u8)`, `set_vbr` (instead of
  `use_cbr`, with the opposite meaning), `set_inband_fec(InbandFec)` and
  `set_packet_loss_perc(u8)`, each with its getter. Each setter refuses the
  values that libopus refuses, and the setting then keeps its old value.
- The default bitrate is libopus's automatic one, `Bitrate::Auto` (for example
  19 kb/s for 16 kHz mono in 20 ms frames), instead of a fixed 64 kb/s.
- `OpusEncoder::enable_hybrid_mode` and the field `OpusDecoder::hybrid_skip_celt`
  are removed. To ask for Hybrid packets, call
  `set_force_mode(Some(ForcedMode::Hybrid))` and `set_bandwidth`.
- `Application` is `#[non_exhaustive]` and has the new values
  `RestrictedSilk` and `RestrictedCelt`. `Bandwidth` has no `Auto`; the
  automatic choice is `None` of an `Option<Bandwidth>`.
- Multistream: the decoder's `new(rate, table)` is
  `from_mapping_table(rate, &table)`, and `new` now takes the layout as
  numbers. `ChannelMappingTable` is `#[non_exhaustive]`;
  `ChannelMappingTable::new` makes one.
- The internal modules are private. The public API is the crate root and the
  modules `packet`, `extensions` and `multistream`. The hidden module
  `internals`, which the tests of the repository use, exists only with the
  test-only feature `__internals`, which is outside semantic versioning.
- `OpusDecoder::decode` writes only the samples it returns and no longer sets
  the rest of `output` to zero. Float output is no longer clamped to [-1, 1].
- The encoder now chooses mode, bandwidth and channel count as libopus does, so
  its packets change: for example a 16 kHz `Voip` encoder codes CELT at 64 kb/s
  and SILK at speech rates. `encode_i16` codes with an input precision of 16
  bits, as libopus `opus_encode` does.
- The feature `heap` turns on `alloc` instead of `std`, so
  `default-features = false, features = ["heap"]` is now a `no_std` build and
  needs `libm` too.

**New features**
- Multichannel: `multistream::MultistreamEncoder` (mapping families 0, 1, 2 and
  255, with libopus's rate allocation, LFE stream and surround masking) and
  `MultistreamDecoder` (any valid layout). Packets interoperate with libopus in
  both directions. Both have the constructors `new` (the layout as numbers),
  `from_mapping_table` (the table of an Ogg Opus header) and `from_layout` (a
  `ChannelLayout`), and the encoder also `new_surround`. The module needs the
  new feature `alloc` (on through `std` and `heap`), so it also works in a
  `no_std` build with a global allocator. Its constructors build every stream
  codec in place, so they need little stack also without `heap`.
- Every frame duration from 2.5 to 120 ms: frames of 80, 100 and 120 ms are new,
  and `set_expert_frame_duration` with the type `FrameDuration` sets one.
- The applications `RestrictedSilk` and `RestrictedCelt` of libopus 1.6:
  every packet with audio is SILK-only or CELT-only, for a decoder that has
  only one of the two codecs. They can only be chosen when the encoder is
  built.
- The speech and music analysis of libopus (feature `analysis`, on by default),
  which runs from complexity 7 on, and discontinuous transmission (DTX):
  `set_dtx`, `dtx` and `in_dtx`.
- The neural decoder features of libopus 1.5 and 1.6 (features `deep-plc`,
  `osce`, `osce-bwe`): deep packet loss concealment, the LACE and NoLACE speech
  enhancement of SILK, and the bandwidth extension (BWE) of SILK wideband to
  fullband.
- Deep redundancy (DRED, feature `dred`): `OpusEncoder::set_dred_duration`,
  and `OpusDredDecoder`, `OpusDred` and `OpusDecoder::dred_decode` (with
  `dred_decode_i16` and `dred_decode24`) on the decoder side.
  `OpusDredDecoder::parse` takes a `DredProcessing` (`Now` or `Deferred`).
- The module `packet` parses, writes and pads packets. The enums `Framing`
  and `Padding` stand for libopus's flags `self_delimited` and `pad`, and
  `ParsedPacket` has read-only methods. `packet::bandwidth` and
  `packet::nb_channels` read the TOC byte.
- Packet extensions of libopus 1.5 and later: the module `extensions`,
  `packet::Repacketizer`, `packet::pad_with_extensions`, `packet::unpad` and
  `packet::multistream_unpad`. `Extension::new` makes an extension, and
  `ExtensionIterator` is an `Iterator`. Bad arguments such as more than 48
  frames are errors instead of panics. `pad_with_extensions` writes the
  extensions it is given or fails with `BufferTooSmall`.
- Typed settings: `set_bandwidth` takes `Option<Bandwidth>` (`None` is the
  automatic choice), `set_force_channels` takes `Option<Channels>`, the LSB
  depth and the DRED duration are `u8`, the voice ratio is `Option<u8>`, and
  the decoder gain is an `i16`. The enums with a libopus number have
  `to_raw` and `TryFrom<i32>`. `set_energy_mask` refuses a mask of the wrong
  length, and `pcm_soft_clip(pcm, channels, memory)` returns a `Result`.
- New controls of libopus. Decoder: `decode_i16`, `decode_fec`, `final_range`,
  `reset_state`, `last_packet_duration`, `set_gain`, `pitch`,
  `set_phase_inversion_disabled`, `bandwidth` (`OPUS_GET_BANDWIDTH`) and
  `nb_samples` (`opus_decoder_get_nb_samples`). Encoder: `set_force_mode`,
  `set_bandwidth`, `set_max_bandwidth`, `set_force_channels`, `set_signal`,
  `set_lsb_depth`, `InbandFec::OnUnlessMusic`, `set_vbr_constraint`,
  `set_application`, `set_voice_ratio`, `set_phase_inversion_disabled`,
  `lookahead`, `bandwidth` (`OPUS_GET_BANDWIDTH`, the bandwidth of the last
  packet) and `max_bandwidth`. The multistream encoder and decoder forward the
  controls that libopus's forward, and answer `OPUS_GET_BANDWIDTH` and the
  decoder's `OPUS_GET_LAST_PACKET_DURATION` from the first stream, as
  libopus does. Both have `channels()`. `opus1::VERSION` and
  `opus1::LIBOPUS_VERSION` give the crate version and the libopus release it
  follows.
- The 24-bit integer calls of libopus 1.6: `OpusEncoder::encode24`,
  `OpusDecoder::decode24` and `decode_fec24`, and their multistream versions.
- `OpusDecoder::init` and `OpusEncoder::init` build a codec in memory the
  caller provides, such as a `static`, with little stack, as upstream opus-rs
  PR #65 does for the decoder. `reset_state` allocates nothing. The feature
  `portable-atomic` lets the crate build for targets without atomic
  compare-and-swap, as upstream opus-rs PR #64 (by Alix ANNERAUD) does.

**Quality fixes**
- 48 kHz `Voip`: SILK now reads the float high-pass filter's output, as the
  float build of libopus does. The integer filter added a constant of about -9
  to the 16-bit signal, which SILK coded as a burst of low-frequency energy in
  7.6 to 9.2 % of the 10 ms frames of speech. On 222 TSP speech files the error
  against the original, relative to libopus's, fell from between 1.21 and 3.28
  to between 0.98 and 1.09.
- 8 to 24 kHz `Voip`: the first SILK frame after CELT-only frames no longer
  codes energy below 200 Hz.
- 24 kHz: Hybrid super-wideband packets decoded with 21 CELT bands instead of
  19, so the 8-12 kHz band of every 24 kHz speech stream at the usual VoIP
  bitrates decoded to unrelated content.
- Stereo SILK and Hybrid: the decoded channels had the wrong sign and level
  (issue #42), and the side signal is now coded as libopus codes it (issue #48).
- SILK: VBR ignored the bitrate (issue #43); CBR with in-band FEC turned
  packets into concealed frames (issue #51); 12 kHz (mediumband) packets
  decoded to noise (issue #53); 10 ms SILK and Hybrid frames decoded to noise.
- CELT from 24 kHz input: a cancelled pitch pre-filter now fades out over the
  overlap as in libopus, which raises the SNR on the issue's chirp from about
  20 dB to 29-42 dB (issue #54).
- Closer to libopus on 64-bit ARM: the CELT encoder writes libopus's packets in
  all 1,080,000 packets of the 960 streams of `celtenc_gap_report` (up from
  91.2 %), and the CELT decoder gives libopus's samples in 51 of 51 CELT
  streams and 28 of 29 Hybrid streams of `perf_harness modes`.

**Speed** (opus1's time divided by libopus 1.6.1's; below 1 means opus1 is
faster; geometric means over the bench set; see [Performance](#performance))

| | Encoding | Decoding |
|---|---:|---:|
| Apple M4 Pro, libopus at `-O2` | 0.572 | 0.669 |
| Apple M4 Pro, Homebrew libopus | 0.461 | 0.598 |
| Cortex-A76 (Android phone, MediaTek MT6833) | 0.668 | 0.768 |
| Cortex-A55 (same phone) | 0.698 | 0.824 |
| Intel Core i7-12700, Linux in WSL2 | 0.90 | 0.81 |
| Intel Core i7-12700, Windows | 0.88 | 0.80 |

On x86-64, low-complexity SILK encoding took 1.04 to 1.10 of libopus's time.
Against opus-rs 0.1.37 on Apple M4 Pro, decoding takes 35-62 % less time, and
encoding 33-56 % less for SILK and Hybrid and 29-49 % less for mono CELT.
Stereo CELT encoding at complexity 9 and 10 takes 2-22 % more time, because it
now runs libopus's stereo pitch pre-filter and `theta_rdo`.

**Platforms**
- 64-bit ARM is the reference platform. There the output is checked bit for
  bit against libopus 1.6.1, and every test that requires libopus's bits
  passes. [docs/guide/10](docs/guide/10-differences-from-libopus.md) lists the remaining
  differences and their sizes.
- x86-64 gives correct output but is not bit-exact with libopus. opus1's x86
  vector kernels do not follow libopus's x86 kernels, and libopus's own x86
  output depends on the C compiler. 54 tests that require libopus's bits
  are ignored on targets other than 64-bit ARM. x86-64 was run on an Intel
  Core i7-12700 (AVX2, FMA and AVX-VNNI), under Linux in WSL2 and natively on
  Windows ([docs/guide/10](docs/guide/10-differences-from-libopus.md#platforms)).
- The minimum Rust version is 1.98: the 64-bit ARM dot-product intrinsics of
  the neural network code are stable from that version on.

**Known gaps** (see [docs/guide/10](docs/guide/10-differences-from-libopus.md#not-implemented))
- The scalable quality extension of libopus 1.6 (QEXT) is not ported.
- Mapping family 3 (ambisonics mixed through a matrix, libopus's
  `opus_projection_*` functions) is not implemented.
- The Opus Custom interface of libopus (`opus_custom_*`, the non-standard
  modes of `--enable-custom-modes`) is not implemented.
- The multistream encoder and decoder have no `init` that builds them in
  memory the caller provides (libopus `opus_multistream_encoder_init`,
  `opus_multistream_surround_encoder_init` and `opus_multistream_decoder_init`).
  They allocate their streams when they are built.
- One control has no equivalent: the decoder's `OPUS_SET_IGNORE_EXTENSIONS`.
  In libopus 1.6.1 only the quality extension, which is not ported, reads
  extensions in the decoder.
- SILK stereo with a constant bitrate at 12 kHz can lose its stereo image on
  tones, as the fixed-point build of libopus does.

### 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.

### 0.1.37 (earlier changes)

- **Fix: CELT flagged transients on steady pitched input (issue #38).** The
  encoder ran `tone_detect` and `transient_analysis` on a buffer whose
  overlap head was the previous frame's *prefiltered* signal while the body
  was unfiltered; libopus 1.6 analyses the unfiltered `prefilter_mem` tail
  (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 kHz `Audio` at 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_mem` is also kept current while the comb filter is off (stereo,
  hybrid, complexity < 5), as libopus's always-run `run_prefilter` does.

### 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_encode` is 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, with
  `silk_setup_LBRR` (gain increase 7, then `max(7 − 0.2·loss, 2)`) and
  opus_encoder.c's `decide_fec` (rate thresholds + hysteresis + bandwidth
  reduction). CBR mono now recovers the previous packet through libopus
  `decode_fec` at 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_3` decimators
  plus an internal-rate delay buffer, lagging libopus's pre-skip by up to 17
  samples. The encoder now runs the libopus `silk_resampler` (encoder
  direction: `delay_matrix_enc` + private down-FIR, ported with the
  `silk_Resampler_*_COEFS` tables) 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`, see
  `scripts/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. New `compat::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 first `vfmadd`.
- **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_size`
  samples 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_SCALING` while every decoder reads
  `CODE_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 bypassed
  `silk_resampler`, dropping its `inputDelay`. 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_band` from the Opus bandwidth
  (`CELT_SET_END_BAND`). It also zero-stuffs the API-rate input by
  `resampling_factor(Fs)` and scales/zeroes the MDCT output for non-48 kHz
  rates, as libopus `celt_preemphasis` / `compute_mdcts` do. 24 kHz `Audio`
  now 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 to `encode()` called
  with the same audio converted via `sample 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 float `encode()` API is unchanged.
- **Fix: `no_std` builds (`--no-default-features --features libm`) compile
  again.** The `multistream` decode module (added in 0.1.33) uses `Vec`-based
  plumbing and is now gated behind the default `std` feature; the core codec
  remains heap-free `no_std`.
- **Zero warnings:** `cargo check` and `cargo clippy --all-targets` are clean
  for the default and `no_std` feature sets (dead CELT state fields removed,
  tests/examples/benches lint-clean).

### 0.1.29

- **Default build is now heap-backed (issue #12).** A new `heap` feature (on by
  default, requires `std`) allocates the large codec state with `Box`, shrinking
  `OpusEncoder`/`OpusDecoder` from ~250 KB / ~175 KB to ~1 KB each. Constructing,
  encoding and decoding now fit on a 768 KiB stack (previously `OpusEncoder::new`
  alone 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.rs` asserting the structs stay <4 KiB
  with `heap`, >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_ALLOCATION` table was missing one `200` in 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_v` panic for `k >= 129`.** `compute_u`'s fixed-size buffer is now domain-checked (`k <= MAX_PVQ_K = 128`); out-of-domain calls return `u32::MAX` instead of panicking or silently wrapping.
- **Fix: silent u32 overflow in PVQ codebook sizes.** `compute_u`/`unext`/`celt_pvq_v` use saturating arithmetic, so an out-of-domain `(n, k)` can never produce a plausible-but-wrong codebook size.
- **Fix: `celt_pvq_u_lookup` row-extent aliasing.** Lookups whose `max(n, k)` exceeds a row's actual coverage now compute instead of reading the next row's block.
- **Fix: `OpusEncoder::encode` now 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 no `alloc`**: the crate is now fully heap-free — no global allocator is required. All `Vec`/`Box` working buffers were replaced with an internal `FixedVec<T, N>` (inline `[MaybeUninit<T>; N]`); `std::sync::LazyLock` was replaced with a heap-free `OnceCell`; no_std float math (`sin`/`cos`/`sqrt`/…) is routed through an optional `libm` feature (pure-Rust musl port).
  - Verified on `thumbv7em-none-eabi` (Cortex-M), `wasm32-unknown-unknown`, and Linux no_std. `scripts/check_no_std.sh` reproduces.
  - 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 `static` or dedicated buffer on constrained targets.
- **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_test` example now tags output files by build (`_std` / `_nostd`) for A/B listening across build modes.

## License

opus1 is released under the BSD-3-Clause license; the full text is in [COPYING](COPYING). It is a port of libopus, so the copyright of the libopus authors (Xiph.Org, Skype Limited, Octasic, Jean-Marc Valin, Timothy B. Terriberry, CSIRO, Gregory Maxwell, Mark Borgerding, Erik de Castro Lopo, Mozilla, Amazon, and for some of the files opus1 ports Koen Vos, Cisco Systems, Vidyo, Google, David Rowe and Arm Limited) applies together with that of restsend.com (opus-rs) and gaojiangjie (opus1). The neural network weights in `dnn-weights/` come from libopus 1.6.1 under the same license ([dnn-weights/COPYING](dnn-weights/COPYING)). The function `cos_fdlibm` in `src/pvq.rs` is translated from fdlibm, the math library of Sun Microsystems, whose notice is kept in [COPYING](COPYING).

## Links

- **Repository**: <https://github.com/RealEffect/opus1>
- **Documentation**: <https://docs.rs/opus1>
- **Forked from**: [opus-rs](https://github.com/restsend/opus-rs) by restsend

Other projects of the opus-rs authors:

- **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>