@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 +61 -0
- package/LICENSE +21 -0
- package/README.md +196 -2
- package/binding.js +839 -0
- package/index.d.ts +160 -0
- package/index.js +13 -0
- package/lib/binding.d.ts +96 -0
- package/lib/loader.js +88 -0
- package/lib/wrapper.js +359 -0
- package/package.json +87 -4
- package/scripts/check-prebuilt.js +47 -0
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
|
-
#
|
|
1
|
+
# @ndqp/bun-rusty-opus
|
|
2
2
|
|
|
3
|
-
|
|
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.
|