@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/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');
@@ -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 };