@ndqp/bun-rusty-opus 0.0.0-stage → 0.2.1
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 +68 -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/lib/wrapper.js
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* JS wrapper — the @discordjs/opus-compatible surface over the native
|
|
5
|
+
* rusty-opus binding.
|
|
6
|
+
*
|
|
7
|
+
* `OpusEncoder` mirrors @discordjs/opus exactly (one class that encodes
|
|
8
|
+
* AND decodes, `new OpusEncoder(rate, channels)`, `encode`/`decode` on
|
|
9
|
+
* s16le PCM Buffers), so it is a drop-in replacement for prism-media /
|
|
10
|
+
* discord.js voice. Extra rusty-opus features are additive:
|
|
11
|
+
* an `options` argument on the constructor, `encodeInto`/`decodeInto`
|
|
12
|
+
* (zero-alloc reuse), `OpusDecoder`, FEC and packet-loss concealment.
|
|
13
|
+
*
|
|
14
|
+
* All PCM here is signed 16-bit little-endian, interleaved, exactly like
|
|
15
|
+
* @discordjs/opus. The i16 <-> f32 conversion happens inside Rust.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const { loadNative } = require('./loader');
|
|
19
|
+
|
|
20
|
+
const native = loadNative();
|
|
21
|
+
const { createOpus } = native;
|
|
22
|
+
|
|
23
|
+
const SUPPORTED_SAMPLE_RATES = [8000, 12000, 16000, 24000, 48000];
|
|
24
|
+
const FRAME_DURATIONS_MS = [2.5, 5, 10, 20, 40, 60, 80, 100, 120];
|
|
25
|
+
|
|
26
|
+
const OPUS_APPLICATIONS = Object.freeze({ voip: 2048, audio: 2049, lowDelay: 2051 });
|
|
27
|
+
const OPUS_BANDWIDTHS = Object.freeze({
|
|
28
|
+
auto: -1000, narrowband: 1101, mediumband: 1102, wideband: 1103, superwideband: 1104, fullband: 1105,
|
|
29
|
+
});
|
|
30
|
+
const OPUS_SIGNALS = Object.freeze({ auto: 0, voice: 1, music: 2 });
|
|
31
|
+
/** libopus `opus_encoder_ctl` codes the native layer understands. */
|
|
32
|
+
const OPUS_CTL = Object.freeze({
|
|
33
|
+
SET_APPLICATION: 4000,
|
|
34
|
+
SET_BITRATE: 4002,
|
|
35
|
+
SET_VBR: 4006,
|
|
36
|
+
SET_BANDWIDTH: 4008,
|
|
37
|
+
SET_MAX_BANDWIDTH: 4009,
|
|
38
|
+
SET_COMPLEXITY: 4010,
|
|
39
|
+
SET_INBAND_FEC: 4012,
|
|
40
|
+
SET_PACKET_LOSS_PERC: 4014,
|
|
41
|
+
SET_DTX: 4016,
|
|
42
|
+
SET_SIGNAL: 4024,
|
|
43
|
+
SET_LSB_DEPTH: 4036,
|
|
44
|
+
// libopus signal values for OPUS_SET_SIGNAL (4024):
|
|
45
|
+
SIGNAL_AUTO: -1000,
|
|
46
|
+
SIGNAL_VOICE: 3001,
|
|
47
|
+
SIGNAL_MUSIC: 3002,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/** Frame duration (ms) per Opus TOC config 0-31 (RFC 6716 §3.1). */
|
|
51
|
+
const TOC_DURATIONS_MS = [
|
|
52
|
+
10, 20, 40, 60, // SILK NB
|
|
53
|
+
10, 20, 40, 60, // SILK MB
|
|
54
|
+
10, 20, 40, 60, // SILK WB
|
|
55
|
+
10, 20, // Hybrid SWB
|
|
56
|
+
10, 20, // Hybrid FB
|
|
57
|
+
2.5, 5, 10, 20, // CELT NB
|
|
58
|
+
2.5, 5, 10, 20, // CELT WB
|
|
59
|
+
2.5, 5, 10, 20, // CELT SWB
|
|
60
|
+
2.5, 5, 10, 20, // CELT FB
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
function toBuffer(view, name) {
|
|
64
|
+
if (Buffer.isBuffer(view)) return view;
|
|
65
|
+
if (view instanceof Uint8Array) {
|
|
66
|
+
// View over the same memory — no copy.
|
|
67
|
+
return Buffer.from(view.buffer, view.byteOffset, view.byteLength);
|
|
68
|
+
}
|
|
69
|
+
throw new TypeError(`${name} must be a Buffer or Uint8Array`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function validFrameSizes(rate) {
|
|
73
|
+
return FRAME_DURATIONS_MS.map((ms) => (rate * ms) / 1000);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function assertRate(rate) {
|
|
77
|
+
if (!SUPPORTED_SAMPLE_RATES.includes(rate)) {
|
|
78
|
+
throw new RangeError(
|
|
79
|
+
`unsupported sample rate ${rate}: Opus supports only ${SUPPORTED_SAMPLE_RATES.join(', ')} Hz`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function assertChannels(channels) {
|
|
85
|
+
if (channels !== 1 && channels !== 2) {
|
|
86
|
+
throw new RangeError(`unsupported channel count ${channels}: Opus supports only 1 or 2 channels`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function inferEncodeFrameSize(pcm, rate, channels) {
|
|
91
|
+
if (pcm.length % (2 * channels) !== 0) {
|
|
92
|
+
throw new RangeError(
|
|
93
|
+
`PCM buffer length ${pcm.length} is not a whole number of frames for ${channels} channel(s)`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const frameSize = pcm.length / (2 * channels);
|
|
97
|
+
if (!validFrameSizes(rate).includes(frameSize)) {
|
|
98
|
+
throw new RangeError(
|
|
99
|
+
`invalid frame size ${frameSize} samples at ${rate} Hz: valid sizes are ${validFrameSizes(rate).join(', ')} ` +
|
|
100
|
+
`(2.5/5/10/20/40/60/80/100/120 ms)`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return frameSize;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Infer the total output frame size (samples per channel) of an Opus
|
|
108
|
+
* packet from its TOC byte — the same information libopus reads in
|
|
109
|
+
* `opus_packet_get_nb_frames` / `opus_packet_get_samples_per_frame`.
|
|
110
|
+
*/
|
|
111
|
+
function packetFrameSize(packet, rate) {
|
|
112
|
+
if (packet.length < 1) throw new RangeError('cannot infer frame size from an empty Opus packet');
|
|
113
|
+
const toc = packet[0];
|
|
114
|
+
const config = (toc >> 3) & 0x1f;
|
|
115
|
+
const frameCountCode = toc & 0x03;
|
|
116
|
+
let frames;
|
|
117
|
+
if (frameCountCode === 0) frames = 1;
|
|
118
|
+
else if (frameCountCode === 1 || frameCountCode === 2) frames = 2;
|
|
119
|
+
else {
|
|
120
|
+
if (packet.length < 2) throw new RangeError('malformed Opus packet: frame-count byte missing');
|
|
121
|
+
frames = packet[1] & 0x3f;
|
|
122
|
+
}
|
|
123
|
+
const perFrame = (rate * TOC_DURATIONS_MS[config]) / 1000;
|
|
124
|
+
return perFrame * frames;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* End-to-end encoder delay (pre-skip), in samples per channel.
|
|
129
|
+
*
|
|
130
|
+
* Measured by cross-correlating a chirp through encode -> decode (see
|
|
131
|
+
* docs/PERFORMANCE.md): rusty-opus does NOT pad its output to libopus's
|
|
132
|
+
* uniform 312 samples. Its delay follows the coding mode, exactly as the
|
|
133
|
+
* mode decision in rusty-opus `OpusEncoder::new`:
|
|
134
|
+
* - SILK-only (voip/audio at <= 16 kHz): 312 samples at 48 kHz, scaled by rate.
|
|
135
|
+
* - CELT-only / Hybrid (lowDelay at any rate; voip/audio above 16 kHz):
|
|
136
|
+
* 120 samples at 48 kHz, scaled by rate.
|
|
137
|
+
*
|
|
138
|
+
* This is the value an Ogg OpusHead must carry as pre-skip for streams
|
|
139
|
+
* encoded here — writing libopus's 312 for a 48 kHz CELT/Hybrid stream
|
|
140
|
+
* would trim 192 real samples (~4 ms) off the start of the audio.
|
|
141
|
+
* Forcing a SILK-only bandwidth at a high rate can change the mode and
|
|
142
|
+
* therefore the delay; the value here is for the default mode decision.
|
|
143
|
+
*/
|
|
144
|
+
function opusPreSkip(rate, application) {
|
|
145
|
+
const silkOnly = rate <= 16000 && application !== 'lowDelay';
|
|
146
|
+
return Math.round((rate * (silkOnly ? 312 : 120)) / 48000);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function resolveApplication(application) {
|
|
150
|
+
if (application === undefined || application === null) return { name: 'audio', code: 2049 };
|
|
151
|
+
if (typeof application === 'string') {
|
|
152
|
+
const key = application === 'low-delay' || application === 'lowdelay' ? 'lowDelay' : application;
|
|
153
|
+
if (key in OPUS_APPLICATIONS) return { name: key, code: OPUS_APPLICATIONS[key] };
|
|
154
|
+
throw new RangeError(`unknown application ${JSON.stringify(application)}: use 'voip', 'audio' or 'lowDelay'`);
|
|
155
|
+
}
|
|
156
|
+
throw new TypeError(`application must be a string ('voip' | 'audio' | 'lowDelay'), got ${typeof application}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function resolveBandwidth(name, label) {
|
|
160
|
+
if (name === undefined || name === null) return null;
|
|
161
|
+
if (typeof name === 'string' && name in OPUS_BANDWIDTHS) return OPUS_BANDWIDTHS[name];
|
|
162
|
+
if (typeof name === 'number' && Object.values(OPUS_BANDWIDTHS).includes(name)) return name;
|
|
163
|
+
throw new RangeError(`unknown ${label} ${JSON.stringify(name)}: use ${Object.keys(OPUS_BANDWIDTHS).join(', ')}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function resolveSignal(name) {
|
|
167
|
+
if (name === undefined || name === null) return 0;
|
|
168
|
+
if (typeof name === 'string' && name in OPUS_SIGNALS) return OPUS_SIGNALS[name];
|
|
169
|
+
throw new RangeError(`unknown signal ${JSON.stringify(name)}: use 'auto', 'voice' or 'music'`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function createNative(rate, channels, options = {}) {
|
|
173
|
+
const opts = typeof options === 'string' ? { application: options } : options;
|
|
174
|
+
if (opts === null || typeof opts !== 'object') throw new TypeError('options must be an object');
|
|
175
|
+
const app = resolveApplication(opts.application);
|
|
176
|
+
const bitrate = opts.bitrate ?? 64000;
|
|
177
|
+
const complexity = opts.complexity ?? 9;
|
|
178
|
+
const useCbr = opts.cbr ?? opts.useCbr ?? false;
|
|
179
|
+
const useFec = opts.fec ?? opts.useFec ?? false;
|
|
180
|
+
const useDtx = opts.dtx ?? opts.useDtx ?? false;
|
|
181
|
+
const packetLoss = opts.packetLoss ?? opts.packetLossPerc ?? 0;
|
|
182
|
+
// s16-sourced content: rusty-opus's float default is 24, but 16 matches
|
|
183
|
+
// opus_demo parity for s16 input (see docs/research-rusty-opus.md).
|
|
184
|
+
const lsbDepth = opts.lsbDepth ?? 16;
|
|
185
|
+
// createOpus is a free napi function: construction is fallible, and napi
|
|
186
|
+
// constructors must return Self directly, so creation lives there.
|
|
187
|
+
const codec = createOpus({
|
|
188
|
+
rate, channels, application: app.code, bitrate, complexity, useCbr, useFec, useDtx, packetLoss, lsbDepth,
|
|
189
|
+
});
|
|
190
|
+
const bandwidth = resolveBandwidth(opts.bandwidth, 'bandwidth');
|
|
191
|
+
if (bandwidth !== null && bandwidth !== -1000) codec.setBandwidth(bandwidth);
|
|
192
|
+
const maxBandwidth = resolveBandwidth(opts.maxBandwidth, 'maxBandwidth');
|
|
193
|
+
if (maxBandwidth !== null) codec.setMaxBandwidth(maxBandwidth);
|
|
194
|
+
codec.setSignal(resolveSignal(opts.signal));
|
|
195
|
+
return { codec, application: app.name };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* @discordjs/opus-compatible encoder/decoder.
|
|
200
|
+
*
|
|
201
|
+
* ```js
|
|
202
|
+
* const { OpusEncoder } = require('@ndqp/bun-rusty-opus');
|
|
203
|
+
* const encoder = new OpusEncoder(48000, 2); // rate, channels
|
|
204
|
+
* const packet = encoder.encode(pcmBuffer); // 20 ms s16le frame -> Opus
|
|
205
|
+
* const pcm = encoder.decode(packet); // Opus packet -> s16le PCM
|
|
206
|
+
* ```
|
|
207
|
+
*/
|
|
208
|
+
class OpusEncoder {
|
|
209
|
+
constructor(rate = 48000, channels = 2, options = {}) {
|
|
210
|
+
assertRate(rate);
|
|
211
|
+
assertChannels(channels);
|
|
212
|
+
const { codec, application } = createNative(rate, channels, options);
|
|
213
|
+
this._native = codec;
|
|
214
|
+
this._rate = rate;
|
|
215
|
+
this._channels = channels;
|
|
216
|
+
this._application = application;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
get rate() { return this._rate; }
|
|
220
|
+
get channels() { return this._channels; }
|
|
221
|
+
get application() { return this._application; }
|
|
222
|
+
/** End-to-end encoder delay in samples per channel — the Ogg pre-skip for streams encoded here. See `opusPreSkip`. */
|
|
223
|
+
get preSkip() { return opusPreSkip(this._rate, this._application); }
|
|
224
|
+
|
|
225
|
+
/** Encode one s16le PCM frame -> one Opus packet. `frameSize` in samples per channel; inferred from the buffer length when omitted. */
|
|
226
|
+
encode(pcm, frameSize) {
|
|
227
|
+
const buf = toBuffer(pcm, 'pcm');
|
|
228
|
+
const frame = frameSize ?? inferEncodeFrameSize(buf, this._rate, this._channels);
|
|
229
|
+
return this._native.encode(buf, frame);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Encode into a caller-provided output buffer. Returns bytes written.
|
|
234
|
+
* No Buffer is allocated per frame — the hot path for 50 frames/sec.
|
|
235
|
+
* `out` must hold at least 1276 bytes (max Opus packet) to be safe.
|
|
236
|
+
*/
|
|
237
|
+
encodeInto(pcm, out, frameSize) {
|
|
238
|
+
const buf = toBuffer(pcm, 'pcm');
|
|
239
|
+
const outBuf = toBuffer(out, 'out');
|
|
240
|
+
const frame = frameSize ?? inferEncodeFrameSize(buf, this._rate, this._channels);
|
|
241
|
+
return this._native.encodeInto(buf, outBuf, frame);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Decode one Opus packet -> s16le PCM. `frameSize` inferred from the packet TOC when omitted. */
|
|
245
|
+
decode(packet, frameSize) {
|
|
246
|
+
const buf = toBuffer(packet, 'packet');
|
|
247
|
+
const frame = frameSize ?? packetFrameSize(buf, this._rate);
|
|
248
|
+
return this._native.decode(buf, frame);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Decode into a caller-provided PCM buffer. Returns PCM bytes written. */
|
|
252
|
+
decodeInto(packet, out, frameSize) {
|
|
253
|
+
const buf = toBuffer(packet, 'packet');
|
|
254
|
+
const outBuf = toBuffer(out, 'out');
|
|
255
|
+
const frame = frameSize ?? packetFrameSize(buf, this._rate);
|
|
256
|
+
return this._native.decodeInto(buf, outBuf, frame);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Reconstruct a lost frame from the in-band FEC (LBRR) in the *next* packet. `frameSize` is required. */
|
|
260
|
+
decodeFec(packet, frameSize) {
|
|
261
|
+
if (frameSize === undefined || frameSize === null) {
|
|
262
|
+
throw new RangeError('decodeFec requires frameSize (samples per channel of the lost frame)');
|
|
263
|
+
}
|
|
264
|
+
return this._native.decodeFec(toBuffer(packet, 'packet'), frameSize);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Packet-loss concealment: synthesise `frameSize` samples per channel for a lost frame. */
|
|
268
|
+
decodePacketLoss(frameSize) {
|
|
269
|
+
if (frameSize === undefined || frameSize === null) {
|
|
270
|
+
throw new RangeError('decodePacketLoss requires frameSize (samples per channel)');
|
|
271
|
+
}
|
|
272
|
+
return this._native.decodePlc(frameSize);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
setBitrate(bitrate) { this._native.setBitrate(bitrate); }
|
|
276
|
+
getBitrate() { return this._native.getBitrate(); }
|
|
277
|
+
setFEC(enabled) { this._native.setFec(Boolean(enabled)); }
|
|
278
|
+
/** Expected packet loss, 0-100 (%). Alias of setPacketLoss. */
|
|
279
|
+
setPLP(percentage) { this._native.setPacketLoss(percentage); }
|
|
280
|
+
setPacketLoss(percentage) { this._native.setPacketLoss(percentage); }
|
|
281
|
+
setComplexity(complexity) { this._native.setComplexity(complexity); }
|
|
282
|
+
setCBR(enabled) { this._native.setCbr(Boolean(enabled)); }
|
|
283
|
+
setDTX(enabled) { this._native.setDtx(Boolean(enabled)); }
|
|
284
|
+
setBandwidth(bandwidth) {
|
|
285
|
+
const code = resolveBandwidth(bandwidth, 'bandwidth');
|
|
286
|
+
if (code === -1000 || code === null) this._native.clearBandwidth();
|
|
287
|
+
else this._native.setBandwidth(code);
|
|
288
|
+
}
|
|
289
|
+
setMaxBandwidth(bandwidth) { this._native.setMaxBandwidth(resolveBandwidth(bandwidth, 'maxBandwidth')); }
|
|
290
|
+
setSignal(signal) { this._native.setSignal(resolveSignal(signal)); }
|
|
291
|
+
setLsbDepth(depth) { this._native.setLsbDepth(depth); }
|
|
292
|
+
|
|
293
|
+
/** Raw `opus_encoder_ctl` passthrough for the codes in `OPUS_CTL`. Unsupported codes throw. */
|
|
294
|
+
applyEncoderCTL(ctl, value) { this._native.applyEncoderCtl(ctl, value); }
|
|
295
|
+
/** rusty-opus exposes no decoder controls — this always throws. Kept for @discordjs/opus API parity. */
|
|
296
|
+
applyDecoderCTL(ctl, value) { this._native.applyDecoderCtl(ctl, value); }
|
|
297
|
+
|
|
298
|
+
/** Final range-coder state of the last encoded packet (OPUS_GET_FINAL_RANGE). */
|
|
299
|
+
finalRange() { return this._native.finalRange(); }
|
|
300
|
+
/** Final range-decoder state of the last decoded packet. */
|
|
301
|
+
lastRange() { return this._native.lastRange(); }
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Decode-only codec. Same decoder as `OpusEncoder`, without an encoder. */
|
|
305
|
+
class OpusDecoder {
|
|
306
|
+
constructor(rate = 48000, channels = 2) {
|
|
307
|
+
assertRate(rate);
|
|
308
|
+
assertChannels(channels);
|
|
309
|
+
const { codec } = createNative(rate, channels, {});
|
|
310
|
+
this._native = codec;
|
|
311
|
+
this._rate = rate;
|
|
312
|
+
this._channels = channels;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
get rate() { return this._rate; }
|
|
316
|
+
get channels() { return this._channels; }
|
|
317
|
+
|
|
318
|
+
decode(packet, frameSize) {
|
|
319
|
+
const buf = toBuffer(packet, 'packet');
|
|
320
|
+
const frame = frameSize ?? packetFrameSize(buf, this._rate);
|
|
321
|
+
return this._native.decode(buf, frame);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
decodeInto(packet, out, frameSize) {
|
|
325
|
+
const buf = toBuffer(packet, 'packet');
|
|
326
|
+
const outBuf = toBuffer(out, 'out');
|
|
327
|
+
const frame = frameSize ?? packetFrameSize(buf, this._rate);
|
|
328
|
+
return this._native.decodeInto(buf, outBuf, frame);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
decodeFec(packet, frameSize) {
|
|
332
|
+
if (frameSize === undefined || frameSize === null) {
|
|
333
|
+
throw new RangeError('decodeFec requires frameSize (samples per channel of the lost frame)');
|
|
334
|
+
}
|
|
335
|
+
return this._native.decodeFec(toBuffer(packet, 'packet'), frameSize);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
decodePacketLoss(frameSize) {
|
|
339
|
+
if (frameSize === undefined || frameSize === null) {
|
|
340
|
+
throw new RangeError('decodePacketLoss requires frameSize (samples per channel)');
|
|
341
|
+
}
|
|
342
|
+
return this._native.decodePlc(frameSize);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
lastRange() { return this._native.lastRange(); }
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
module.exports = {
|
|
349
|
+
OpusEncoder,
|
|
350
|
+
OpusDecoder,
|
|
351
|
+
SUPPORTED_SAMPLE_RATES,
|
|
352
|
+
FRAME_DURATIONS_MS,
|
|
353
|
+
OPUS_APPLICATIONS,
|
|
354
|
+
OPUS_BANDWIDTHS,
|
|
355
|
+
OPUS_SIGNALS,
|
|
356
|
+
OPUS_CTL,
|
|
357
|
+
packetFrameSize,
|
|
358
|
+
opusPreSkip,
|
|
359
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,89 @@
|
|
|
1
1
|
{
|
|
2
|
+
"author": "Phuong Nhu DQ",
|
|
3
|
+
"bugs": {
|
|
4
|
+
"url": "https://github.com/nemesisqp/bun-rusty-opus/issues"
|
|
5
|
+
},
|
|
6
|
+
"description": "Personal test project (not production ready): Bun/Node bindings to rusty-opus — a pure-Rust Opus codec by jinti and the Remade-With-Rust team, faster to encode than libopus. Drop-in replacement for @discordjs/opus (by iCrawl and the discord.js team).",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@discordjs/opus": "^0.11.0",
|
|
9
|
+
"@napi-rs/cli": "^3.10.12",
|
|
10
|
+
"@types/node": "^26.6.6",
|
|
11
|
+
"typescript": "^5.9.0"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"bun": ">=1.3",
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./index.d.ts",
|
|
20
|
+
"default": "./index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"index.js",
|
|
25
|
+
"index.d.ts",
|
|
26
|
+
"binding.js",
|
|
27
|
+
"lib/",
|
|
28
|
+
"scripts/check-prebuilt.js",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"README.md",
|
|
31
|
+
"CHANGELOG.md"
|
|
32
|
+
],
|
|
33
|
+
"homepage": "https://github.com/nemesisqp/bun-rusty-opus",
|
|
34
|
+
"keywords": [
|
|
35
|
+
"opus",
|
|
36
|
+
"audio",
|
|
37
|
+
"codec",
|
|
38
|
+
"bun",
|
|
39
|
+
"napi",
|
|
40
|
+
"rust",
|
|
41
|
+
"discord",
|
|
42
|
+
"voice"
|
|
43
|
+
],
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"main": "index.js",
|
|
2
46
|
"name": "@ndqp/bun-rusty-opus",
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
47
|
+
"napi": {
|
|
48
|
+
"binaryName": "bun-rusty-opus",
|
|
49
|
+
"packageName": "@ndqp/bun-rusty-opus",
|
|
50
|
+
"targets": [
|
|
51
|
+
"aarch64-apple-darwin",
|
|
52
|
+
"x86_64-pc-windows-msvc",
|
|
53
|
+
"aarch64-pc-windows-msvc",
|
|
54
|
+
"x86_64-unknown-linux-gnu",
|
|
55
|
+
"x86_64-unknown-linux-musl",
|
|
56
|
+
"aarch64-unknown-linux-gnu",
|
|
57
|
+
"aarch64-unknown-linux-musl"
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
"repository": {
|
|
61
|
+
"type": "git",
|
|
62
|
+
"url": "git+https://github.com/nemesisqp/bun-rusty-opus.git"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"bench": "bun bench/bench.mjs",
|
|
66
|
+
"build": "napi build --platform --release --js binding.js --dts lib/binding.d.ts",
|
|
67
|
+
"build:debug": "napi build --platform --js binding.js --dts lib/binding.d.ts",
|
|
68
|
+
"build:local": "bash scripts/build-local.sh",
|
|
69
|
+
"clippy": "cargo clippy --all-targets -- -D warnings",
|
|
70
|
+
"fmt": "cargo fmt --check",
|
|
71
|
+
"fmt:fix": "cargo fmt",
|
|
72
|
+
"postinstall": "node scripts/check-prebuilt.js",
|
|
73
|
+
"release:prepare": "napi create-npm-dirs && napi prepublish",
|
|
74
|
+
"test": "node --test test/*.test.mjs",
|
|
75
|
+
"test:bun": "bun test test/",
|
|
76
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
77
|
+
},
|
|
78
|
+
"types": "index.d.ts",
|
|
79
|
+
"version": "0.2.1",
|
|
80
|
+
"optionalDependencies": {
|
|
81
|
+
"@ndqp/bun-rusty-opus-darwin-arm64": "0.2.1",
|
|
82
|
+
"@ndqp/bun-rusty-opus-win32-x64-msvc": "0.2.1",
|
|
83
|
+
"@ndqp/bun-rusty-opus-win32-arm64-msvc": "0.2.1",
|
|
84
|
+
"@ndqp/bun-rusty-opus-linux-x64-gnu": "0.2.1",
|
|
85
|
+
"@ndqp/bun-rusty-opus-linux-x64-musl": "0.2.1",
|
|
86
|
+
"@ndqp/bun-rusty-opus-linux-arm64-gnu": "0.2.1",
|
|
87
|
+
"@ndqp/bun-rusty-opus-linux-arm64-musl": "0.2.1"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* postinstall guard: fail the install when no prebuilt native binary fits
|
|
6
|
+
* this machine — instead of silently succeeding and failing at first use,
|
|
7
|
+
* and instead of falling back to a source build (there is none).
|
|
8
|
+
*
|
|
9
|
+
* Runs as the package's `postinstall` script. It only *loads* the binding;
|
|
10
|
+
* it never invokes cargo / node-gyp / any compiler.
|
|
11
|
+
*
|
|
12
|
+
* Skipped (warning only) in a source checkout — where Cargo.toml and .git
|
|
13
|
+
* are present — so developers can install deps and then `npm run build`
|
|
14
|
+
* the binding themselves. Published packages do not ship Cargo.toml or
|
|
15
|
+
* .git, so the check is hard there.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('node:fs');
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
|
|
21
|
+
const root = path.join(__dirname, '..');
|
|
22
|
+
const isSourceCheckout = fs.existsSync(path.join(root, 'Cargo.toml')) && fs.existsSync(path.join(root, '.git'));
|
|
23
|
+
const skipEnv = process.env.BUN_RUSTY_OPUS_SKIP_PREBUILT_CHECK === '1';
|
|
24
|
+
|
|
25
|
+
if (skipEnv) {
|
|
26
|
+
console.log('@ndqp/bun-rusty-opus: prebuilt check skipped (BUN_RUSTY_OPUS_SKIP_PREBUILT_CHECK=1)');
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
require('../index.js');
|
|
32
|
+
console.log('@ndqp/bun-rusty-opus: prebuilt native binary OK');
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error(`\n${err.message}\n`);
|
|
35
|
+
if (isSourceCheckout) {
|
|
36
|
+
console.error(
|
|
37
|
+
'@ndqp/bun-rusty-opus: source checkout detected — continuing anyway. ' +
|
|
38
|
+
'Build the binding locally with `npm run build` (or `npm run build:local` for a native-CPU build).\n',
|
|
39
|
+
);
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
console.error(
|
|
43
|
+
'@ndqp/bun-rusty-opus: install failed — no prebuilt binary for this platform and ' +
|
|
44
|
+
'this package does not build from source on install.\n',
|
|
45
|
+
);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|