@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/index.d.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ndqp/bun-rusty-opus — Bun/Node bindings to rusty-opus (pure-Rust Opus codec).
|
|
3
|
+
*
|
|
4
|
+
* `OpusEncoder` mirrors @discordjs/opus: one class that encodes AND decodes,
|
|
5
|
+
* s16le PCM Buffers in and out. See README.md for the full API.
|
|
6
|
+
*
|
|
7
|
+
* NOTE: this is the hand-written public type definition. The napi-rs
|
|
8
|
+
* generated types for the raw native binding live in lib/binding.d.ts.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type OpusApplication = 'voip' | 'audio' | 'lowDelay';
|
|
12
|
+
export type OpusBandwidth = 'auto' | 'narrowband' | 'mediumband' | 'wideband' | 'superwideband' | 'fullband';
|
|
13
|
+
export type OpusSignal = 'auto' | 'voice' | 'music';
|
|
14
|
+
|
|
15
|
+
export interface OpusEncoderOptions {
|
|
16
|
+
/** 'voip' (speech), 'audio' (music/general, default), 'lowDelay' (CELT only). */
|
|
17
|
+
application?: OpusApplication;
|
|
18
|
+
/** Target bitrate in bits/sec. Default 64000. */
|
|
19
|
+
bitrate?: number;
|
|
20
|
+
/** Complexity 0 (fastest) to 10 (best quality). Default 9. */
|
|
21
|
+
complexity?: number;
|
|
22
|
+
/** Constant bitrate. Default false (VBR). */
|
|
23
|
+
cbr?: boolean;
|
|
24
|
+
/** @deprecated Use `cbr`. */
|
|
25
|
+
useCbr?: boolean;
|
|
26
|
+
/** In-band forward error correction (SILK LBRR). Default false. */
|
|
27
|
+
fec?: boolean;
|
|
28
|
+
/** @deprecated Use `fec`. */
|
|
29
|
+
useFec?: boolean;
|
|
30
|
+
/** Discontinuous transmission. Default false. */
|
|
31
|
+
dtx?: boolean;
|
|
32
|
+
/** @deprecated Use `dtx`. */
|
|
33
|
+
useDtx?: boolean;
|
|
34
|
+
/** Expected packet loss, 0-100 (%). Default 0. */
|
|
35
|
+
packetLoss?: number;
|
|
36
|
+
/** @deprecated Use `packetLoss`. */
|
|
37
|
+
packetLossPerc?: number;
|
|
38
|
+
/** Input bit depth assumed by the analysis (16 for s16 source, default). */
|
|
39
|
+
lsbDepth?: number;
|
|
40
|
+
/** Force an audio bandwidth, or 'auto' (default). */
|
|
41
|
+
bandwidth?: OpusBandwidth;
|
|
42
|
+
/** Cap the automatically selected bandwidth. Default 'fullband'. */
|
|
43
|
+
maxBandwidth?: OpusBandwidth;
|
|
44
|
+
/** Voice/music bias, or 'auto' (default). */
|
|
45
|
+
signal?: OpusSignal;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @discordjs/opus-compatible encoder/decoder.
|
|
50
|
+
*
|
|
51
|
+
* ```ts
|
|
52
|
+
* import { OpusEncoder } from '@ndqp/bun-rusty-opus';
|
|
53
|
+
* const encoder = new OpusEncoder(48000, 2);
|
|
54
|
+
* const packet = encoder.encode(pcmBuffer); // 20 ms s16le frame -> Opus
|
|
55
|
+
* const pcm = encoder.decode(packet);
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export declare class OpusEncoder {
|
|
59
|
+
constructor(rate?: number, channels?: number, options?: OpusEncoderOptions | OpusApplication);
|
|
60
|
+
readonly rate: number;
|
|
61
|
+
readonly channels: number;
|
|
62
|
+
readonly application: OpusApplication;
|
|
63
|
+
/**
|
|
64
|
+
* End-to-end encoder delay in samples per channel — the Ogg OpusHead
|
|
65
|
+
* pre-skip for streams encoded here. 120 at 48 kHz for CELT/Hybrid,
|
|
66
|
+
* 312 (scaled by rate) for SILK-only modes. See `opusPreSkip`.
|
|
67
|
+
*/
|
|
68
|
+
readonly preSkip: number;
|
|
69
|
+
|
|
70
|
+
/** Encode one s16le PCM frame -> one Opus packet. `frameSize` in samples per channel; inferred from the buffer length when omitted. */
|
|
71
|
+
encode(pcm: Buffer | Uint8Array, frameSize?: number): Buffer;
|
|
72
|
+
/**
|
|
73
|
+
* Encode into a caller-provided output buffer. Returns bytes written.
|
|
74
|
+
* No Buffer is allocated per frame. `out` should hold at least 1276 bytes.
|
|
75
|
+
*/
|
|
76
|
+
encodeInto(pcm: Buffer | Uint8Array, out: Buffer | Uint8Array, frameSize?: number): number;
|
|
77
|
+
/** Decode one Opus packet -> s16le PCM. `frameSize` inferred from the packet TOC when omitted. */
|
|
78
|
+
decode(packet: Buffer | Uint8Array, frameSize?: number): Buffer;
|
|
79
|
+
/** Decode into a caller-provided PCM buffer. Returns PCM bytes written. */
|
|
80
|
+
decodeInto(packet: Buffer | Uint8Array, out: Buffer | Uint8Array, frameSize?: number): number;
|
|
81
|
+
/** Reconstruct a lost frame from the in-band FEC (LBRR) in the *next* packet. `frameSize` is required. */
|
|
82
|
+
decodeFec(packet: Buffer | Uint8Array, frameSize: number): Buffer;
|
|
83
|
+
/** Packet-loss concealment: synthesise `frameSize` samples per channel for a lost frame. */
|
|
84
|
+
decodePacketLoss(frameSize: number): Buffer;
|
|
85
|
+
|
|
86
|
+
setBitrate(bitrate: number): void;
|
|
87
|
+
getBitrate(): number;
|
|
88
|
+
setFEC(enabled: boolean): void;
|
|
89
|
+
/** Expected packet loss, 0-100 (%). */
|
|
90
|
+
setPLP(percentage: number): void;
|
|
91
|
+
setPacketLoss(percentage: number): void;
|
|
92
|
+
setComplexity(complexity: number): void;
|
|
93
|
+
setCBR(enabled: boolean): void;
|
|
94
|
+
setDTX(enabled: boolean): void;
|
|
95
|
+
setBandwidth(bandwidth: OpusBandwidth): void;
|
|
96
|
+
setMaxBandwidth(bandwidth: OpusBandwidth): void;
|
|
97
|
+
setSignal(signal: OpusSignal): void;
|
|
98
|
+
setLsbDepth(depth: number): void;
|
|
99
|
+
/** Raw `opus_encoder_ctl` passthrough for the codes in `OPUS_CTL`. Unsupported codes throw. */
|
|
100
|
+
applyEncoderCTL(ctl: number, value: number): void;
|
|
101
|
+
/** rusty-opus exposes no decoder controls — this always throws. Kept for @discordjs/opus API parity. */
|
|
102
|
+
applyDecoderCTL(ctl: number, value: number): void;
|
|
103
|
+
/** Final range-coder state of the last encoded packet (OPUS_GET_FINAL_RANGE). */
|
|
104
|
+
finalRange(): number;
|
|
105
|
+
/** Final range-decoder state of the last decoded packet. */
|
|
106
|
+
lastRange(): number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Decode-only codec. Same decoder as `OpusEncoder`, without an encoder. */
|
|
110
|
+
export declare class OpusDecoder {
|
|
111
|
+
constructor(rate?: number, channels?: number);
|
|
112
|
+
readonly rate: number;
|
|
113
|
+
readonly channels: number;
|
|
114
|
+
decode(packet: Buffer | Uint8Array, frameSize?: number): Buffer;
|
|
115
|
+
decodeInto(packet: Buffer | Uint8Array, out: Buffer | Uint8Array, frameSize?: number): number;
|
|
116
|
+
decodeFec(packet: Buffer | Uint8Array, frameSize: number): Buffer;
|
|
117
|
+
decodePacketLoss(frameSize: number): Buffer;
|
|
118
|
+
lastRange(): number;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export declare const SUPPORTED_SAMPLE_RATES: readonly [8000, 12000, 16000, 24000, 48000];
|
|
122
|
+
export declare const FRAME_DURATIONS_MS: readonly number[];
|
|
123
|
+
export declare const OPUS_APPLICATIONS: Readonly<{ voip: 2048; audio: 2049; lowDelay: 2051 }>;
|
|
124
|
+
export declare const OPUS_BANDWIDTHS: Readonly<{
|
|
125
|
+
auto: -1000;
|
|
126
|
+
narrowband: 1101;
|
|
127
|
+
mediumband: 1102;
|
|
128
|
+
wideband: 1103;
|
|
129
|
+
superwideband: 1104;
|
|
130
|
+
fullband: 1105;
|
|
131
|
+
}>;
|
|
132
|
+
export declare const OPUS_SIGNALS: Readonly<{ auto: 0; voice: 1; music: 2 }>;
|
|
133
|
+
export declare const OPUS_CTL: Readonly<{
|
|
134
|
+
SET_APPLICATION: 4000;
|
|
135
|
+
SET_BITRATE: 4002;
|
|
136
|
+
SET_VBR: 4006;
|
|
137
|
+
SET_BANDWIDTH: 4008;
|
|
138
|
+
SET_MAX_BANDWIDTH: 4009;
|
|
139
|
+
SET_COMPLEXITY: 4010;
|
|
140
|
+
SET_INBAND_FEC: 4012;
|
|
141
|
+
SET_PACKET_LOSS_PERC: 4014;
|
|
142
|
+
SET_DTX: 4016;
|
|
143
|
+
SET_SIGNAL: 4024;
|
|
144
|
+
SET_LSB_DEPTH: 4036;
|
|
145
|
+
SIGNAL_AUTO: -1000;
|
|
146
|
+
SIGNAL_VOICE: 3001;
|
|
147
|
+
SIGNAL_MUSIC: 3002;
|
|
148
|
+
}>;
|
|
149
|
+
|
|
150
|
+
/** Infer the total output frame size (samples per channel) of an Opus packet from its TOC byte. */
|
|
151
|
+
export declare function packetFrameSize(packet: Buffer | Uint8Array, rate: number): number;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* End-to-end encoder delay (pre-skip), in samples per channel, for the
|
|
155
|
+
* given rate and application. SILK-only (voip/audio at <= 16 kHz) is
|
|
156
|
+
* `round(rate * 312 / 48000)`; CELT/Hybrid is `round(rate * 120 / 48000)`.
|
|
157
|
+
* Unlike libopus, rusty-opus does not pad its output to a uniform 312 —
|
|
158
|
+
* use this value as the Ogg OpusHead pre-skip for streams encoded here.
|
|
159
|
+
*/
|
|
160
|
+
export declare function opusPreSkip(rate: number, application: OpusApplication): number;
|
package/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// bun-rusty-opus — Bun/Node bindings to rusty-opus (pure-Rust Opus codec).
|
|
4
|
+
//
|
|
5
|
+
// The native binding is loaded by lib/loader.js (which delegates to the
|
|
6
|
+
// napi-rs generated loader in lib/binding.js) and fails loudly with
|
|
7
|
+
// ERR_BUN_RUSTY_OPUS_NO_PREBUILT when no prebuilt binary fits this machine.
|
|
8
|
+
// This package never builds from source on install (see README).
|
|
9
|
+
//
|
|
10
|
+
// NOTE: `napi build` generates its loader as lib/binding.js (see the
|
|
11
|
+
// `build` script in package.json) — it must NOT overwrite this file.
|
|
12
|
+
|
|
13
|
+
module.exports = require('./lib/wrapper.js');
|
package/lib/binding.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/* auto-generated by NAPI-RS */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Which binding artifact the generated loader actually loaded: `'native'` for
|
|
6
|
+
* a native addon, otherwise the `platformArchABI` of the WASI flavor. Every
|
|
7
|
+
* flavor napi-rs can build is listed, because `NAPI_RS_NATIVE_LIBRARY_PATH`
|
|
8
|
+
* can point the loader at a WASI artifact this package does not build itself.
|
|
9
|
+
*/
|
|
10
|
+
export declare const __napiBindingTarget: 'native' | 'wasm32-wasi' | 'wasm32-wasip1'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* One native codec: an encoder and a decoder sharing the same rate/channels.
|
|
14
|
+
* The JS `OpusEncoder` (encode + decode, @discordjs/opus shape) and the JS
|
|
15
|
+
* `OpusDecoder` (decode only) are both thin wrappers over this struct.
|
|
16
|
+
*/
|
|
17
|
+
export declare class NativeOpus {
|
|
18
|
+
rate(): number
|
|
19
|
+
channels(): number
|
|
20
|
+
getBitrate(): number
|
|
21
|
+
setBitrate(bitrate: number): void
|
|
22
|
+
setComplexity(complexity: number): void
|
|
23
|
+
setCbr(enabled: boolean): void
|
|
24
|
+
setFec(enabled: boolean): void
|
|
25
|
+
setDtx(enabled: boolean): void
|
|
26
|
+
setPacketLoss(percent: number): void
|
|
27
|
+
setBandwidth(code: number): void
|
|
28
|
+
clearBandwidth(): void
|
|
29
|
+
setMaxBandwidth(code: number): void
|
|
30
|
+
setSignal(code: number): void
|
|
31
|
+
setLsbDepth(depth: number): void
|
|
32
|
+
/**
|
|
33
|
+
* Raw `opus_encoder_ctl` passthrough for the CTL codes rusty-opus can
|
|
34
|
+
* express. Unknown / unsupported codes are an error, never silent.
|
|
35
|
+
* libopus CTL numbers: https://opus-codec.org/docs/opus_api-1.5/group__opus__encoderctls.html
|
|
36
|
+
*/
|
|
37
|
+
applyEncoderCtl(ctl: number, value: number): void
|
|
38
|
+
/**
|
|
39
|
+
* rusty-opus exposes no decoder-side settings (no gain, no CTL), so
|
|
40
|
+
* every decoder CTL is an explicit error rather than a silent no-op.
|
|
41
|
+
*/
|
|
42
|
+
applyDecoderCtl(ctl: number, value: number): void
|
|
43
|
+
finalRange(): number
|
|
44
|
+
lastRange(): number
|
|
45
|
+
/**
|
|
46
|
+
* `frame_size` is samples per channel; `0` infers it from the input
|
|
47
|
+
* length (`input.len() / (channels * 2)`).
|
|
48
|
+
*/
|
|
49
|
+
encode(input: Buffer, frameSize: number): Buffer
|
|
50
|
+
/**
|
|
51
|
+
* Encode into a caller-provided output buffer. Returns bytes written.
|
|
52
|
+
* This is the zero-alloc hot path: no `Buffer` is created per frame.
|
|
53
|
+
*/
|
|
54
|
+
encodeInto(input: Buffer, output: Buffer, frameSize: number): number
|
|
55
|
+
/**
|
|
56
|
+
* `frame_size` is the expected output samples per channel and is
|
|
57
|
+
* required here — the JS wrapper infers it from the packet TOC when
|
|
58
|
+
* the caller omits it. Pass an empty packet for PLC (lost frame).
|
|
59
|
+
*/
|
|
60
|
+
decode(packet: Buffer, frameSize: number): Buffer
|
|
61
|
+
/** Decode into a caller-provided PCM buffer. Returns PCM bytes written. */
|
|
62
|
+
decodeInto(packet: Buffer, output: Buffer, frameSize: number): number
|
|
63
|
+
/** Reconstruct a lost frame from the LBRR in the *next* packet. */
|
|
64
|
+
decodeFec(packet: Buffer, frameSize: number): Buffer
|
|
65
|
+
/**
|
|
66
|
+
* Packet-loss concealment for a lost frame: synthesises `frame_size`
|
|
67
|
+
* samples per channel from decoder state.
|
|
68
|
+
*/
|
|
69
|
+
decodePlc(frameSize: number): Buffer
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* JS entry point for creating a codec: `createOpus({ rate, channels,
|
|
74
|
+
* application, bitrate, complexity, useCbr, useFec, useDtx, packetLoss,
|
|
75
|
+
* lsbDepth })`. A free function (not a class constructor) so construction
|
|
76
|
+
* errors surface as a JS exception instead of a panic.
|
|
77
|
+
*/
|
|
78
|
+
export declare function createOpus(opts: OpusOptions): NativeOpus
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Creation options, passed as one JS object to `createOpus` — one argument
|
|
82
|
+
* instead of ten (clippy `too_many_arguments`), and the field names are the
|
|
83
|
+
* contract with `lib/wrapper.js` (`useCbr`, `packetLoss`, `lsbDepth`, ...).
|
|
84
|
+
*/
|
|
85
|
+
export interface OpusOptions {
|
|
86
|
+
rate: number
|
|
87
|
+
channels: number
|
|
88
|
+
application: number
|
|
89
|
+
bitrate: number
|
|
90
|
+
complexity: number
|
|
91
|
+
useCbr: boolean
|
|
92
|
+
useFec: boolean
|
|
93
|
+
useDtx: boolean
|
|
94
|
+
packetLoss: number
|
|
95
|
+
lsbDepth: number
|
|
96
|
+
}
|
package/lib/loader.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Load the prebuilt native binding — and fail loudly if there is none.
|
|
5
|
+
*
|
|
6
|
+
* The actual loading is done by the napi-rs generated loader in
|
|
7
|
+
* `./binding.js` (regenerated by `npm run build`; do not hand-edit it).
|
|
8
|
+
* It tries a locally built `./bun-rusty-opus.<triple>.node` first, then the
|
|
9
|
+
* platform package `@ndqp/bun-rusty-opus-<triple>` installed as an
|
|
10
|
+
* optionalDependency. This file only translates its failure into the
|
|
11
|
+
* package's own error, so users get a clear message instead of napi-rs's
|
|
12
|
+
* generic "Cannot find native binding".
|
|
13
|
+
*
|
|
14
|
+
* There is intentionally NO build fallback here: no cargo, no node-gyp,
|
|
15
|
+
* no postinstall compile. If no prebuilt fits this machine, `require()`
|
|
16
|
+
* throws (and the `postinstall` check in `scripts/check-prebuilt.js`
|
|
17
|
+
* makes `npm install` / `bun install` fail at install time instead of
|
|
18
|
+
* at first use).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('node:fs');
|
|
22
|
+
const path = require('node:path');
|
|
23
|
+
|
|
24
|
+
const BINARY_NAME = 'bun-rusty-opus';
|
|
25
|
+
|
|
26
|
+
/** All triples CI builds (see .github/workflows/release.yml). */
|
|
27
|
+
const SUPPORTED_TARGETS = [
|
|
28
|
+
'darwin-arm64',
|
|
29
|
+
'linux-x64-gnu',
|
|
30
|
+
'linux-x64-musl',
|
|
31
|
+
'linux-arm64-gnu',
|
|
32
|
+
'linux-arm64-musl',
|
|
33
|
+
'win32-x64-msvc',
|
|
34
|
+
'win32-arm64-msvc',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
function isMusl() {
|
|
38
|
+
try {
|
|
39
|
+
if (fs.existsSync('/etc/alpine-release')) return true;
|
|
40
|
+
const { execFileSync } = require('node:child_process');
|
|
41
|
+
const out = execFileSync('ldd', ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
42
|
+
return /musl/i.test(out);
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function candidateTriples() {
|
|
49
|
+
const platform = process.platform;
|
|
50
|
+
const arch = process.arch;
|
|
51
|
+
if (platform === 'linux') {
|
|
52
|
+
const libc = isMusl() ? 'musl' : 'gnu';
|
|
53
|
+
const other = libc === 'musl' ? 'gnu' : 'musl';
|
|
54
|
+
return [`linux-${arch}-${libc}`, `linux-${arch}-${other}`];
|
|
55
|
+
}
|
|
56
|
+
if (platform === 'darwin') return [`darwin-${arch}`];
|
|
57
|
+
if (platform === 'win32') return [`win32-${arch}-msvc`];
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function loadNative() {
|
|
62
|
+
const bindingPath = path.join(__dirname, '..', 'binding.js');
|
|
63
|
+
if (!fs.existsSync(bindingPath)) {
|
|
64
|
+
// Fresh checkout before any build — a dev problem, not a prebuilt one.
|
|
65
|
+
throw new Error(
|
|
66
|
+
'@ndqp/bun-rusty-opus: native binding has not been built yet (binding.js is missing). ' +
|
|
67
|
+
'Build it with `npm run build` (or `npm run build:local` for a native-CPU build).',
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
return require(bindingPath);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
const detected = `${process.platform}-${process.arch}${process.platform === 'linux' ? (isMusl() ? '-musl' : '-gnu') : ''}`;
|
|
74
|
+
const error = new Error(
|
|
75
|
+
`@ndqp/bun-rusty-opus: no prebuilt native binary for this machine (${detected}).\n` +
|
|
76
|
+
`Supported targets: ${SUPPORTED_TARGETS.join(', ')}.\n` +
|
|
77
|
+
`This package ships prebuilt binaries only and does not build from source on install. ` +
|
|
78
|
+
`If your platform is not listed, please open an issue at ` +
|
|
79
|
+
`https://github.com/nemesisqp/bun-rusty-opus/issues — do not try to build it by hand on this machine.\n` +
|
|
80
|
+
`Underlying loader error: ${err.message}`,
|
|
81
|
+
);
|
|
82
|
+
error.code = 'ERR_BUN_RUSTY_OPUS_NO_PREBUILT';
|
|
83
|
+
error.cause = err;
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { loadNative, candidateTriples, SUPPORTED_TARGETS, BINARY_NAME };
|