@ndqp/bun-rusty-opus 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,61 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 — 2026-11-13
4
+
5
+ - Vendored rusty-opus 1.0.2 into `vendor/rusty-opus/` with
6
+ `[patch.crates-io]` and bit-preserving decode patches (see
7
+ `vendor/rusty-opus/PATCHES.md`): AVX2/SSE2 kernels for `exp_rotation1`
8
+ (the PVQ rotation had a NEON kernel but no x86 twin; no FMA, unit-
9
+ tested bit-exact vs scalar), `#[inline(always)]` on `dec_bits` /
10
+ `read_byte` / `unext`, and the SIG_SAT clamp loop slicing first.
11
+ Decode 28.6 → 27.8 ms in `bench.mjs` (0.78x → 0.80x vs
12
+ @discordjs/opus, overall 1.15x). Two leads were measured and dropped:
13
+ `compute_u` never fires on real content and an AVX `kf_bfly2`
14
+ extraction did not vectorise (reverted).
15
+ - Validation harness: `scripts/check-decode-parity.sh` (pristine 1.0.2
16
+ vs fork, bit-for-bit, 44 checks: 11 configs × normal/PLC/FEC decode
17
+ + encode bit-identity — all pass) and `test/decode-parity.test.mjs`
18
+ (JS interop vs libopus: RMS ~2e-5, max 1 s16 LSB; PLC asserted as
19
+ sanity, not libopus parity, because upstream PLC legitimately
20
+ diverges from libopus's concealment fade).
21
+ - `docs/DECODE_ESTIMATE.md` corrected: the 22–24 ms fork estimate was
22
+ too optimistic (PVQ scaffolding, already-optimised IMDCT/postfilter);
23
+ the fork's ~3% is real and the harness makes future patches cheap to
24
+ validate.
25
+
26
+ ## 0.1.1 — 2026-11-13
27
+
28
+ - Faster decode: the f32→i16 conversion on the decode path is now
29
+ runtime-dispatched SIMD (AVX2/SSE2 on x86_64, NEON on aarch64, scalar
30
+ tail + fallback) instead of a scalar loop that did not auto-vectorise.
31
+ Decode 33.1 → 28.7 ms in `bench.mjs` (0.66x → 0.77x vs
32
+ @discordjs/opus), `decodeInto` 31.7 → 27.7 ms. Rounding may differ by
33
+ 1 LSB on exact-half values (nearest-even vs half-away-from-zero).
34
+ - Investigation notes in `docs/PERFORMANCE.md`: the remaining decode
35
+ gap is inside rusty-opus's CELT/Hybrid decode (SILK decode is at
36
+ parity or faster); ruled out with measurements — per-frame allocation
37
+ (0), denormals/FTZ (1.00x), SIMD dispatch, native CPU flags, packet
38
+ content. Also noted: SILK encode at 16 kHz mono is ~3x slower than
39
+ libopus in the crate (not fixed here).
40
+
41
+ ## 0.1.0 — 2026-11-13
42
+
43
+ First release.
44
+
45
+ - napi-rs bindings to rusty-opus 1.0.2 (pure-Rust Opus codec, no libopus).
46
+ - `OpusEncoder` with the @discordjs/opus API shape (encode/decode on s16le
47
+ PCM, `setBitrate`/`setFEC`/`setPLP`, `applyEncoderCTL`, `finalRange`/
48
+ `lastRange`), plus `OpusDecoder` (decode-only).
49
+ - Zero-copy Buffers (`BufferSlice`) and zero-allocation hot path
50
+ (`encodeInto`/`decodeInto` with caller-provided output buffers; reused
51
+ f32 scratch; no heap allocation per frame after warm-up).
52
+ - Prebuilt napi binaries for 7 platforms via GitHub Actions; install fails
53
+ (`ERR_BUN_RUSTY_OPUS_NO_PREBUILT`) when no prebuilt fits — no
54
+ install-time source build anywhere.
55
+ - `encoder.preSkip` / `opusPreSkip(rate, application)`: the real
56
+ end-to-end delay (120 samples @48 kHz CELT/Hybrid, 312 scaled for
57
+ SILK-only) — not libopus's uniform 312.
58
+ - Interop verified both ways with @discordjs/opus 0.11.0 (libopus):
59
+ packets decode correctly in both directions at matched bitrate.
60
+ - 18 tests pass under Node (15 under Bun; the 3 interop tests skip there
61
+ because @discordjs/opus ships no Bun prebuild). Typecheck clean.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NĐQP
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,197 @@
1
- # Temporary Holding Version
1
+ # @ndqp/bun-rusty-opus
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > **Personal project — quick test, not production ready.** This is an
4
+ > experiment for trying out rusty-opus under Bun/Node. It is not
5
+ > hardened, not audited, and not supported for production use. If you
6
+ > need a production Opus binding, use
7
+ > [@discordjs/opus](https://www.npmjs.com/package/@discordjs/opus) —
8
+ > the battle-tested libopus binding whose API this project mirrors.
9
+
10
+ Bun/Node bindings to [rusty-opus](https://crates.io/crates/rusty-opus) — a
11
+ **pure-Rust Opus codec** (RFC 6716/8251, no C, no libopus, no FFI) by
12
+ jinti and the Remade-With-Rust team (see [Credits](#credits)).
13
+ Drop-in replacement for
14
+ [@discordjs/opus](https://www.npmjs.com/package/@discordjs/opus), with the
15
+ same `OpusEncoder` shape discord.js voice uses — but the codec underneath
16
+ is Rust, and encode is faster. This project is only the thin binding
17
+ between the two; the codec and the API it mirrors are other people's
18
+ work, credited below.
19
+
20
+ ## Install
21
+
22
+ ```sh
23
+ npm install @ndqp/bun-rusty-opus
24
+ # or
25
+ bun install @ndqp/bun-rusty-opus
26
+ ```
27
+
28
+ **Prebuilt binaries only.** The package ships napi prebuilts for 7
29
+ platforms (below). There is no install-time build and no fallback to
30
+ compiling from source: if no prebuilt fits your machine, install **fails**
31
+ with a clear error (`ERR_BUN_RUSTY_OPUS_NO_PREBUILT`) instead of silently
32
+ building — or silently succeeding and breaking at first use. If you are
33
+ developing this package itself, `npm run build` (or
34
+ `scripts/build-local.sh` for a `target-cpu=native` dev build) is the way.
35
+
36
+ | OS | x86_64 | arm64 |
37
+ | --- | --- | --- |
38
+ | macOS | — (Intel Mac unsupported) | `darwin-arm64` |
39
+ | Windows | `win32-x64-msvc` | `win32-arm64-msvc` |
40
+ | Linux glibc | `linux-x64-gnu` | `linux-arm64-gnu` |
41
+ | Linux musl (Alpine) | `linux-x64-musl` | `linux-arm64-musl` |
42
+
43
+ Requires Node >= 18 or Bun >= 1.3.
44
+
45
+ ## Usage
46
+
47
+ ```js
48
+ import { OpusEncoder, OpusDecoder } from '@ndqp/bun-rusty-opus';
49
+
50
+ const encoder = new OpusEncoder(48000, 2); // rate, channels; defaults 48000/2
51
+ const packet = encoder.encode(pcmBuffer); // 20 ms s16le frame (3840 bytes) -> Opus
52
+ const pcm = encoder.decode(packet); // Opus packet -> s16le PCM
53
+
54
+ const decoder = new OpusDecoder(48000, 2); // decode-only, same decoder
55
+ ```
56
+
57
+ `frameSize` (samples per channel) is inferred when omitted: from the PCM
58
+ buffer length for `encode`, from the packet TOC byte for `decode`. Valid
59
+ frame sizes are 2.5/5/10/20/40/60 ms (and up to 120 ms in the codec); rates
60
+ 8000/12000/16000/24000/48000; channels 1/2.
61
+
62
+ ### Zero-allocation hot path
63
+
64
+ For 50 frames/s/stream, `encode` allocates a Buffer per packet. Reuse one
65
+ output buffer instead — no allocation per frame, no GC pressure:
66
+
67
+ ```js
68
+ const outPacket = Buffer.alloc(1276); // max Opus packet size
69
+ const outPcm = Buffer.alloc(3840); // one 20 ms stereo frame
70
+ const n = encoder.encodeInto(pcmBuffer, outPacket, 960); // n = bytes written
71
+ const m = encoder.decodeInto(packet, outPcm, 960); // m = PCM bytes written
72
+ ```
73
+
74
+ The encoder also reuses a scratch f32 buffer internally and makes no heap
75
+ allocation per frame after warm-up.
76
+
77
+ ### Options
78
+
79
+ ```js
80
+ new OpusEncoder(48000, 2, {
81
+ application: 'audio', // 'voip' | 'audio' (default) | 'lowDelay'
82
+ bitrate: 64000, // default 64000 (see note below)
83
+ complexity: 9, // 0-10, default 9
84
+ fec: false, // in-band forward error correction
85
+ packetLoss: 0, // expected loss, 0-100%
86
+ cbr: false, dtx: false,
87
+ bandwidth: 'auto', maxBandwidth: 'fullband', signal: 'auto',
88
+ lsbDepth: 16, // 16 for s16 source (default)
89
+ });
90
+ ```
91
+
92
+ Also available, all @discordjs/opus-compatible: `setBitrate`/`getBitrate`,
93
+ `setFEC`, `setPLP` (alias `setPacketLoss`), `setComplexity`, `setCBR`,
94
+ `setDTX`, `setBandwidth`, `setMaxBandwidth`, `setSignal`, `setLsbDepth`,
95
+ `applyEncoderCTL` (libopus CTL codes 4002/4006/4008/4009/4010/4012/4014/
96
+ 4016/4024/4036; 4000 application is rejected — set it in the constructor),
97
+ `applyDecoderCTL` (always throws: rusty-opus exposes no decoder controls),
98
+ `decodeFec(packet, frameSize)`, `decodePacketLoss(frameSize)`,
99
+ `finalRange()` / `lastRange()`. Helpers: `packetFrameSize(packet, rate)`,
100
+ `opusPreSkip(rate, application)`.
101
+
102
+ ## Differences from @discordjs/opus
103
+
104
+ These are deliberate, measured, and tested (`test/interop.test.mjs`
105
+ decodes each codec's packets with the other):
106
+
107
+ - **Default bitrate is 64000 bps; @discordjs/opus defaults to 120000 bps**
108
+ (libopus `OPUS_AUTO`). If you switch and want identical output sizes,
109
+ set the bitrate explicitly. At matched bitrate, packet totals are within
110
+ a few percent.
111
+ - **Pre-skip is not 312.** libopus pads every stream to a 312-sample
112
+ delay; rusty-opus does not. Its real end-to-end delay is 120 samples at
113
+ 48 kHz for CELT/Hybrid modes and 312 (scaled by rate) for SILK-only
114
+ (voip/audio ≤ 16 kHz). Read it from `encoder.preSkip` and use it as the
115
+ Ogg OpusHead pre-skip — writing 312 for a 48 kHz CELT/Hybrid stream
116
+ trims 192 real samples (~4 ms) off the start. See `docs/PERFORMANCE.md`
117
+ for the measured table.
118
+ - **No separate native decoder module** — same as @discordjs/opus: one
119
+ class encodes and decodes; `OpusDecoder` is a decode-only convenience.
120
+ - **Pure Rust, no libopus.** rusty-opus is a from-scratch Rust port of
121
+ libopus 1.6, bit-exact on the RFC test vectors, and interoperates with
122
+ libopus both ways.
123
+
124
+ ## Performance
125
+
126
+ Measured on this VM (shared 1.5 CPU, portable build), 10 s of 48 kHz
127
+ stereo, 20 ms frames, bitrate matched at 64 kbps — see
128
+ `docs/PERFORMANCE.md` for method and full numbers:
129
+
130
+ - **encode ~1.3x faster** than @discordjs/opus (libopus)
131
+ - **decode ~0.77x** (slower than libopus; was 0.66x before v0.1.1's SIMD
132
+ f32→i16 conversion — see `docs/PERFORMANCE.md` for the breakdown)
133
+ - **overall ~1.15x**
134
+
135
+ The crate's "1.5x faster" claim is about **encode** (CELT), which our
136
+ measurements support (1.27–1.39x). It is not an overall figure, and this
137
+ README does not present it as one. Both codecs run ~90–100x realtime here.
138
+
139
+ ## Why no `target-cpu=native` in the published builds?
140
+
141
+ The project brief suggested compiling with native CPU flags. That would be
142
+ a bug in a published prebuilt: a binary built with `-C target-cpu=native`
143
+ on a CI runner can crash with an illegal instruction on a user's older
144
+ CPU. rusty-opus already dispatches SIMD at runtime (AVX2/AVX/FMA/SSE2 on
145
+ x86_64, NEON on arm64; `RUSTY_OPUS_ISA` can cap it), so the portable build
146
+ still uses the fast paths. The release profile uses opt-level 3, fat LTO,
147
+ and single codegen unit instead. `scripts/build-local.sh` offers
148
+ `target-cpu=native` for your own machine only.
149
+
150
+ ## Development
151
+
152
+ ```sh
153
+ npm ci
154
+ npm run build # napi build --platform --release
155
+ npm test # node:test, incl. interop tests vs @discordjs/opus
156
+ npm run test:bun # bun test (interop tests skip: @discordjs/opus has no Bun prebuild)
157
+ npm run bench # bench/bench.mjs
158
+ npm run typecheck # tsc --noEmit
159
+ cargo clippy --all-targets -- -D warnings
160
+ ```
161
+
162
+ Releasing: push a `v*` tag; `.github/workflows/release.yml` builds all 8
163
+ prebuilts and publishes the main package plus the platform packages to
164
+ npm (needs the `NPM_TOKEN` secret). `docs/DESIGN.md` documents the
165
+ binding layout, the loader, and the gotchas found while building this.
166
+
167
+ ## Credits
168
+
169
+ This project is a thin binding — almost all of the hard work is other
170
+ people's:
171
+
172
+ - **rusty-opus** — the codec itself: a pure-Rust port of libopus by
173
+ **jinti** (`shenjindi@fourz.cn`) and the **Remade-With-Rust** team
174
+ ([github.com/Remade-With-Rust/rusty-opus](https://github.com/Remade-With-Rust/rusty-opus),
175
+ BSD-3-Clause). The SILK/CELT/Hybrid implementation, the SIMD kernels,
176
+ the bit-exact RFC test vectors — all theirs. v0.2.0 vendors their
177
+ 1.0.2 release with a few small decode patches (see
178
+ `vendor/rusty-opus/PATCHES.md`); the patches are mine, the codec is
179
+ theirs.
180
+ - **The Opus codec** — rusty-opus is a port of libopus, whose authors
181
+ are Xiph.Org, Skype Limited, Octasic, Jean-Marc Valin, Timothy B.
182
+ Terriberry and contributors (full copyright notice in
183
+ `vendor/rusty-opus/COPYING`).
184
+ - **@discordjs/opus** — by **iCrawl** (`icrawltogo@gmail.com`) and the
185
+ discord.js team ([github.com/discordjs/opus](https://github.com/discordjs/opus),
186
+ MIT). This binding mirrors its `OpusEncoder` API and uses it as the
187
+ interop reference in every test and bench. If this project looks
188
+ familiar, that is why.
189
+ - **napi-rs** — the binding framework that makes the Rust↔JS boundary
190
+ work.
191
+
192
+ ## License
193
+
194
+ MIT for the binding code in this repo. rusty-opus (including the
195
+ vendored copy in `vendor/rusty-opus/`) is BSD-3-Clause; @discordjs/opus
196
+ is MIT. See `vendor/rusty-opus/COPYING` for the codec's copyright
197
+ notice.