├── .github └── workflows │ └── test.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── lib.d.ts ├── lib.js ├── lib.mjs ├── node ├── arm64-darwin.node ├── arm64-linux.node ├── index.js ├── index.mjs ├── x64-darwin.node ├── x64-linux.node └── x64-win32.node ├── package.json ├── tests ├── index.js ├── index.mjs ├── index.ts ├── test.pcm └── wasm.js └── wasm ├── index.js ├── index.mjs ├── opus.wasm └── simd.wasm /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | name: node ${{ matrix.version }} | ${{ matrix.os }} 13 | 14 | strategy: 15 | matrix: 16 | version: [10.x, 12.x, 14.x, 16.x] 17 | os: [macos-latest, ubuntu-latest, windows-latest] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: actions/setup-node@v2 22 | with: 23 | node-version: ${{ matrix.version }} 24 | 25 | - run: node tests/index.js 26 | - run: node tests/wasm.js || true 27 | - run: node tests/index.mjs || true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | test/ 3 | .github/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-present evanwashere 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fast opus bindings for node and browsers 2 | 3 | `bun add @evan/opus` 4 | 5 | `npm install @evan/opus` 6 | 7 | ```js 8 | import { Encoder, Decoder } from '@evan/opus'; 9 | 10 | const e = new Encoder({ channels: 2, sample_rate: 48_000 }); 11 | const d = new Decoder({ channels: 2, sample_rate: 48_000 }); 12 | 13 | d.decode(e.encode(pcm)); 14 | ``` 15 | 16 | ## supported platforms 17 | | | node@10 | node@12 | node@14 | node@16 | node@18 | 18 | | ---------------- | ------- | ------- | ------- | ------- | ------- | 19 | | wasm32 | ✕ | ✓ | ✓ | ✓ | ✓ | 20 | | macos x64 | ✓ | ✓ | ✓ | ✓ | ✓ | 21 | | macos arm64 | ✓ | ✓ | ✓ | ✓ | ✓ | 22 | | windows x64 | ✓ | ✓ | ✓ | ✓ | ✓ | 23 | | linux x64 gnu | ✓ | ✓ | ✓ | ✓ | ✓ | 24 | | linux arm64 gnu | ✓ | ✓ | ✓ | ✓ | ✓ | 25 | 26 | you can force usage of wasm by setting `OPUS_FORCE_WASM` env variable 27 | 28 | for deno and browsers use `@evan/wasm/target/opus/deno.js` from [npm](https://npmjs.com/@evan/wasm)/[cdn](https://unpkg.com/@evan/wasm/target/opus/deno.js) 29 | 30 | ## benchmarks 31 | 32 | ![encoding](https://plot.evan.lol/bar/eyJ0aXRsZSI6Im9wcy9zIChoaWdoZXIgaXMgYmV0dGVyKSIsInBvaW50cyI6W3sibmFtZSI6ImVuY29kZShwY20vMmNoLzQ4SHopIDEyMGticHMiLCJzY29yZXMiOls0MDA0LjAzLDcyMTUuMjQsMjUzMi4zNCw1NDcwLjg2XX0seyJuYW1lIjoiZW5jb2RlKHBjbS8yY2gvNDhIeikgMzIwa2JwcyIsInNjb3JlcyI6WzM4ODguNSw2MTYzLjM0LDIyNjguNjMsNTAxNS4yNV19LHsibmFtZSI6ImVuY29kZShwY20vMmNoLzQ4SHopIDUxMmticHMiLCJzY29yZXMiOlszNTY2LjgxLDU1MDkuMjcsMjA1OC4wNiw0Njc5LjA2XX1dLCJsZWdlbmQiOlt7Im5hbWUiOiJ3YXNtKEBldmFuL29wdXMpIiwiY29sb3IiOjg1MTEzODA0N30seyJuYW1lIjoibmF0aXZlKEBldmFuL29wdXMpIiwiY29sb3IiOi0xMjUwOTcyNjczfSx7Im5hbWUiOiJ3YXNtKG9wdXNzY3JpcHQpIiwiY29sb3IiOi0xMTU2NTAwNDgxfSx7Im5hbWUiOiJuYXRpdmUoQGRpc2NvcmRqcy9vcHVzKSIsImNvbG9yIjotMTQxMjg3MTY5fV19.png) 33 | ![decoding](https://plot.evan.lol/bar/eyJ0aXRsZSI6Im9wcy9zIChoaWdoZXIgaXMgYmV0dGVyKSIsInBvaW50cyI6W3sibmFtZSI6ImRlY29kZShwY20vMmNoLzQ4SHopIDEyMGticHMiLCJzY29yZXMiOlsxMzUwOS44NiwxMTk4Ni4zLDE5NTY1LjY5LDE5Nzc0LjZdfSx7Im5hbWUiOiJkZWNvZGUocGNtLzJjaC80OEh6KSAzMjBrYnBzIiwic2NvcmVzIjpbMTEzOTEuMSwxMDg1Ny4zMiwxNTM5MS4yNiwxNTU3OS41MV19LHsibmFtZSI6ImRlY29kZShwY20vMmNoLzQ4SHopIDUxMmticHMiLCJzY29yZXMiOls3ODAzLjI0LDc1ODYuNTUsMTAyODIuODMsMTAwMzIuODRdfV0sImxlZ2VuZCI6W3sibmFtZSI6Indhc20oQGV2YW4vb3B1cykiLCJjb2xvciI6ODUxMTM4MDQ3fSx7Im5hbWUiOiJ3YXNtKG9wdXNzY3JpcHQpIiwiY29sb3IiOi0xMjUwOTcyNjczfSx7Im5hbWUiOiJuYXRpdmUoQGV2YW4vb3B1cykiLCJjb2xvciI6LTExNTY1MDA0ODF9LHsibmFtZSI6Im5hdGl2ZShAZGlzY29yZGpzL29wdXMpIiwiY29sb3IiOi0xNDEyODcxNjl9XX0=.png) 34 | 35 | ## License 36 | 37 | MIT © [Evan](https://github.com/evanwashere) -------------------------------------------------------------------------------- /lib.d.ts: -------------------------------------------------------------------------------- 1 | type signal = 'auto' | 'voice' | 'music'; 2 | type sample_rate = 8000 | 12000 | 16000 | 24000 | 48000; 3 | type application = 'voip' | 'audio' | 'restricted_lowdelay'; 4 | type complexity = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10; 5 | type expert_frame_duration = 5 | 10 | 20 | 40 | 60 | 80 | 2.5 | 100 | 120 | 'arg'; 6 | type bandwidth = 'auto' | 'wideband' | 'fullband' | 'narrowband' | 'mediumband' | 'superwideband'; 7 | 8 | declare module '@evan/opus' { 9 | export class Decoder { 10 | constructor(options?: { 11 | channels?: 1 | 2, 12 | sample_rate?: sample_rate, 13 | }); 14 | 15 | channels: 1 | 2; 16 | ctl(cmd: number, value?: number): number; 17 | decode(buf: ArrayBufferView): Uint8Array; 18 | 19 | // https://www.opus-codec.org/docs/opus_api-1.3.1/group__opus__decoderctls.html 20 | get gain(): number; 21 | set gain(int: number); 22 | get pitch(): null | number; 23 | get last_packet_duration(): number; 24 | 25 | // https://www.opus-codec.org/docs/opus_api-1.3.1/group__opus__genericctls.html 26 | reset(): void; 27 | get in_dtx(): boolean; 28 | get bandwidth(): bandwidth; 29 | get sample_rate(): sample_rate; 30 | get phase_inversion_disabled(): boolean; 31 | set phase_inversion_disabled(bool: boolean); 32 | } 33 | 34 | export class Encoder { 35 | constructor(options?: { 36 | channels?: 1 | 2, 37 | sample_rate?: sample_rate, 38 | application?: application, 39 | }); 40 | 41 | channels: 1 | 2; 42 | ctl(cmd: number, value?: number): number; 43 | encode(buf: ArrayBufferView): Uint8Array; 44 | encode_pcm_stream(frame_size: number, iter: Iterable | AsyncIterable): AsyncIterable; 45 | 46 | // https://www.opus-codec.org/docs/opus_api-1.3.1/group__opus__genericctls.html 47 | reset(): void; 48 | get in_dtx(): boolean; 49 | get bandwidth(): bandwidth; 50 | get sample_rate(): sample_rate; 51 | get phase_inversion_disabled(): boolean; 52 | set phase_inversion_disabled(bool: boolean); 53 | 54 | // https://www.opus-codec.org/docs/opus_api-1.3.1/group__opus__encoderctls.html 55 | get vbr(): boolean; 56 | get dtx(): boolean; 57 | get signal(): signal; 58 | get bitrate(): number; 59 | get lookhead(): number; 60 | get lsb_depth(): number; 61 | get inband_fec(): boolean; 62 | get packet_loss(): number; 63 | get complexity(): complexity; 64 | get vbr_constraint(): boolean; 65 | get application(): application; 66 | get max_bandwidth(): bandwidth; 67 | get prediction_disabled(): boolean; 68 | get force_channels(): 1 | 2 | 'auto'; 69 | get expert_frame_duration(): expert_frame_duration; 70 | 71 | set vbr(bool: boolean); 72 | set dtx(bool: boolean); 73 | set signal(arg: signal); 74 | set lsb_depth(int: number); 75 | set packet_loss(int: number); 76 | set inband_fec(bool: boolean); 77 | set bandwidth(arg: bandwidth); 78 | set complexity(int: complexity); 79 | set vbr_constraint(bool: boolean); 80 | set application(arg: application); 81 | set max_bandwidth(arg: bandwidth); 82 | set prediction_disabled(bool: boolean); 83 | set force_channels(arg: 1 | 2 | 'auto'); 84 | set bitrate(arg: 'max' | 'auto' | number); 85 | set expert_frame_duration(arg: expert_frame_duration); 86 | } 87 | } -------------------------------------------------------------------------------- /lib.js: -------------------------------------------------------------------------------- 1 | let opus; 2 | 3 | if (process.env.OPUS_FORCE_WASM) opus = require('./wasm/index.js'); 4 | else try { opus = require('./node/index.js'); } catch { opus = require('./wasm/index.js'); } 5 | function clamp(min, int, max) { const t = int < min ? min : int; return t > max ? max : t; } 6 | 7 | const ctl = { 8 | auto: -1000, 9 | bitrate_max: -1, 10 | reset_state: 4028, 11 | 12 | signal: { 13 | auto: -1000, // default 14 | voice: 3001, // best for voice 15 | music: 3002, // best for audio/music 16 | }, 17 | 18 | application: { 19 | voip: 2048, // best for voice 20 | audio: 2049, // best for audio/music 21 | restricted_lowdelay: 2051, // best for low coding delay 22 | }, 23 | 24 | bandwidth: { 25 | auto: -1000, 26 | wideband: 1103, // 8 KHz 27 | fullband: 1105, // 20 KHz 28 | narrowband: 1101, // 4 KHz 29 | mediumband: 1102, // 6 KHz 30 | superwideband: 1104, // 12 KHz 31 | }, 32 | 33 | framesize: { 34 | 5: 5002, // 5ms frames 35 | 10: 5003, // 10ms frames 36 | 20: 5004, // 20ms frames 37 | 40: 5005, // 40ms frames 38 | 60: 5006, // 60ms frames 39 | 80: 5007, // 80ms frames 40 | 100: 5008, // 100ms frames 41 | 120: 5009, // 120ms frames 42 | [2.5]: 5001, // 2.5ms frames 43 | arg: 5000, // encode argument 44 | }, 45 | 46 | set: { 47 | vbr: 4006, 48 | dtx: 4016, 49 | gain: 4034, 50 | signal: 4024, 51 | bitrate: 4002, 52 | bandwidth: 4008, 53 | lsb_depth: 4036, 54 | complexity: 4010, 55 | inband_fec: 4012, 56 | application: 4000, 57 | max_bandwidth: 4004, 58 | vbr_constraint: 4020, 59 | force_channels: 4022, 60 | packet_loss_perc: 4014, 61 | prediction_disabled: 4042, 62 | expert_frame_duration: 4040, 63 | phase_inversion_disabled: 4046, 64 | }, 65 | 66 | get: { 67 | vbr: 4007, 68 | dtx: 4017, 69 | gain: 4045, 70 | pitch: 4033, 71 | signal: 4025, 72 | in_dtx: 4049, 73 | bitrate: 4003, 74 | lsb_depth: 4037, 75 | bandwidth: 4009, 76 | lookahead: 4027, 77 | complexity: 4011, 78 | inband_fec: 4013, 79 | application: 4001, 80 | sample_rate: 4029, 81 | final_range: 4031, 82 | max_bandwidth: 4005, 83 | vbr_constraint: 4021, 84 | force_channels: 4023, 85 | packet_loss_perc: 4015, 86 | prediction_disabled: 4043, 87 | last_packet_duration: 4039, 88 | expert_frame_duration: 4041, 89 | phase_inversion_disabled: 4047, 90 | }, 91 | }; 92 | 93 | const convert = { 94 | fc: { 95 | 1: 1, 96 | 2: 2, 97 | 'auto': [ctl.auto], 98 | [ctl.auto]: 'auto', 99 | }, 100 | 101 | s: { 102 | [ctl.auto]: 'auto', 103 | [ctl.signal.voice]: 'voice', 104 | [ctl.signal.music]: 'music', 105 | }, 106 | 107 | a: { 108 | [ctl.application.voip]: 'voip', 109 | [ctl.application.audio]: 'audio', 110 | [ctl.application.restricted_lowdelay]: 'restricted_lowdelay', 111 | }, 112 | 113 | b: { 114 | [ctl.auto]: 'auto', 115 | [ctl.bandwidth.wideband]: 'wideband', 116 | [ctl.bandwidth.fullband]: 'fullband', 117 | [ctl.bandwidth.narrowband]: 'narrowband', 118 | [ctl.bandwidth.mediumband]: 'mediumband', 119 | [ctl.bandwidth.superwideband]: 'superwideband', 120 | }, 121 | 122 | efd: { 123 | [ctl.framesize[5]]: 5, 124 | [ctl.framesize[10]]: 10, 125 | [ctl.framesize[20]]: 20, 126 | [ctl.framesize[40]]: 40, 127 | [ctl.framesize[60]]: 60, 128 | [ctl.framesize[80]]: 80, 129 | [ctl.framesize[2.5]]: 2.5, 130 | [ctl.framesize[100]]: 100, 131 | [ctl.framesize[120]]: 120, 132 | [ctl.framesize.arg]: 'arg', 133 | }, 134 | } 135 | 136 | class Decoder extends opus.Decoder { 137 | constructor({ channels = 2, sample_rate = 48000 } = {}) { 138 | super({ channels, sample_rate }); 139 | 140 | this.channels = channels; 141 | } 142 | 143 | // https://www.opus-codec.org/docs/opus_api-1.3.1/group__opus__decoderctls.html 144 | get gain() { return this.ctl(ctl.get.gain); } 145 | set gain(int) { this.ctl(ctl.set.gain, clamp(-32768, int, 32767)); } 146 | get pitch() { const x = this.ctl(ctl.get.pitch); return 0 === x ? null : x } 147 | get last_packet_duration() { return this.ctl(ctl.get.last_packet_duration); } 148 | 149 | // https://www.opus-codec.org/docs/opus_api-1.3.1/group__opus__genericctls.html 150 | reset() { this.ctl(ctl.reset_state); } 151 | get in_dtx() { return !!this.ctl(ctl.get.in_dtx); } 152 | get sample_rate() { return this.ctl(ctl.get.sample_rate); } 153 | get bandwidth() { return convert.b[this.ctl(ctl.get.bandwidth)]; } 154 | get phase_inversion_disabled() { return !!this.ctl(ctl.get.phase_inversion_disabled); } 155 | set phase_inversion_disabled(bool) { this.ctl(ctl.set.phase_inversion_disabled, bool ? 1 : 0); } 156 | } 157 | 158 | class Encoder extends opus.Encoder { 159 | constructor({ channels = 2, application, sample_rate = 48000 } = {}) { 160 | super({ channels, sample_rate, application: application || 'audio' }); 161 | 162 | this.channels = channels; 163 | } 164 | 165 | async *encode_pcm_stream(size, iter) { 166 | let needle = 0; 167 | const self = this; 168 | const r = 2 * size * this.channels; 169 | 170 | const t8 = new Uint8Array(r); 171 | 172 | function* consume(buf) { 173 | let offset = 0; 174 | 175 | while (offset < buf.length) { 176 | const s = buf.subarray(offset, offset += r); 177 | 178 | if (r === s.length) yield self.encode(s); 179 | else { 180 | t8.set(s, needle); 181 | needle += s.length; 182 | 183 | break; 184 | }; 185 | } 186 | }; 187 | 188 | for await (const buffer of iter) { 189 | const b8 = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength); 190 | 191 | if (0 === needle) for (const x of consume(b8)) yield x; 192 | else { 193 | const rr = r - needle; 194 | if (rr > b8.length) (t8.set(b8, needle), needle += b8.length); 195 | else { 196 | t8.set(b8.subarray(0, rr), needle); 197 | yield (needle = 0, self.encode(t8)); 198 | for (const x of consume(b8.subarray(rr))) yield x; 199 | } 200 | } 201 | } 202 | } 203 | 204 | // https://www.opus-codec.org/docs/opus_api-1.3.1/group__opus__genericctls.html 205 | reset() { this.ctl(ctl.reset_state); } 206 | get in_dtx() { return !!this.ctl(ctl.get.in_dtx); } 207 | get sample_rate() { return this.ctl(ctl.get.sample_rate); } 208 | get bandwidth() { return convert.b[this.ctl(ctl.get.bandwidth)]; } 209 | get phase_inversion_disabled() { return !!this.ctl(ctl.get.phase_inversion_disabled); } 210 | set phase_inversion_disabled(bool) { this.ctl(ctl.set.phase_inversion_disabled, bool ? 1 : 0); } 211 | 212 | // https://www.opus-codec.org/docs/opus_api-1.3.1/group__opus__encoderctls.html 213 | get vbr() { return !!this.ctl(ctl.get.vbr); } 214 | get dtx() { return !!this.ctl(ctl.get.dtx); } 215 | get bitrate() { return this.ctl(ctl.get.bitrate); } 216 | get lookahead() { return this.ctl(ctl.get.lookahead); } 217 | get lsb_depth() { return this.ctl(ctl.get.lsb_depth); } 218 | get complexity() { return this.ctl(ctl.get.complexity); } 219 | get inband_fec() { return !!this.ctl(ctl.get.inband_fec); } 220 | get signal() { return convert.s[this.ctl(ctl.get.signal)]; } 221 | get packet_loss() { return this.ctl(ctl.get.packet_loss_perc); } 222 | get vbr_constraint() { return !!this.ctl(ctl.get.vbr_constraint); } 223 | get application() { return convert.a[this.ctl(ctl.get.application)]; } 224 | get max_bandwidth() { return convert.b[this.ctl(ctl.get.max_bandwidth)]; } 225 | get force_channels() { return convert.fc[this.ctl(ctl.get.force_channels)]; } 226 | get prediction_disabled() { return !!this.ctl(ctl.get.prediction_disabled); } 227 | get expert_frame_duration() { return covert.efd[this.ctl(ctl.get.expert_frame_duration)]; } 228 | 229 | set vbr(bool) { this.ctl(ctl.set.vbr, bool ? 1 : 0); } 230 | set dtx(bool) { this.ctl(ctl.set.dtx, bool ? 1 : 0); } 231 | set signal(arg) { this.ctl(ctl.set.signal, ctl.signal[arg]); } 232 | set inband_fec(bool) { this.ctl(ctl.set.inband_fec, bool ? 1 : 0); } 233 | set lsb_depth(int) { this.ctl(ctl.set.lsb_depth, clamp(8, int, 24)); } 234 | set bandwidth(arg) { this.ctl(ctl.set.bandwidth, ctl.bandwidth[arg]); } 235 | set complexity(int) { this.ctl(ctl.set.complexity, clamp(0, int, 10)); } 236 | set vbr_constraint(bool) { this.ctl(ctl.set.vbr_constraint, bool ? 1 : 0); } 237 | set application(arg) { this.ctl(ctl.set.application, ctl.application[arg]); } 238 | set force_channels(arg) { this.ctl(ctl.set.force_channels, convert.fc[arg]); } 239 | set packet_loss(int) { this.ctl(ctl.set.packet_loss_perc, clamp(0, int, 100)); } 240 | set prediction_disabled(bool) { this.ctl(ctl.set.prediction_disabled, bool ? 1 : 0); } 241 | set expert_frame_duration(arg) { this.ctl(ctl.set.expert_frame_duration, ctl.framesize[arg]) }; 242 | set max_bandwidth(arg) { this.ctl(ctl.set.max_bandwidth, ctl.bandwidth[arg === 'auto' ? 'fullband' : arg]); } 243 | set bitrate(arg) { this.ctl(ctl.set.bitrate, arg === 'max' ? ctl.bitrate_max : (arg === 'auto' ? ctl.auto : clamp(500, arg | 0, 512000))); } 244 | } 245 | 246 | module.exports = { ctl, Encoder, Decoder }; -------------------------------------------------------------------------------- /lib.mjs: -------------------------------------------------------------------------------- 1 | import lib from './lib.js'; 2 | export const ctl = lib.ctl; 3 | export const Encoder = lib.Encoder; 4 | export const Decoder = lib.Decoder; -------------------------------------------------------------------------------- /node/arm64-darwin.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanwashere/opus/93e526621942221ea0bccf35d5b04484e3e6d4ee/node/arm64-darwin.node -------------------------------------------------------------------------------- /node/arm64-linux.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanwashere/opus/93e526621942221ea0bccf35d5b04484e3e6d4ee/node/arm64-linux.node -------------------------------------------------------------------------------- /node/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require(`./${process.arch}-${process.platform}.node`); 2 | 3 | const eb = module.exports.buffers.encoder(); 4 | module.exports.Encoder = class Encoder extends module.exports.Encoder { 5 | encode(buf) { return eb.slice(0, super.encode(buf)); } 6 | } 7 | 8 | const db = module.exports.buffers.decoder(); 9 | module.exports.Decoder = class Decoder extends module.exports.Decoder { 10 | decode(buf) { return db.slice(0, super.decode(buf)); } 11 | } -------------------------------------------------------------------------------- /node/index.mjs: -------------------------------------------------------------------------------- 1 | import lib from './index.js'; 2 | export const Encoder = lib.Encoder; 3 | export const Decoder = lib.Decoder; -------------------------------------------------------------------------------- /node/x64-darwin.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanwashere/opus/93e526621942221ea0bccf35d5b04484e3e6d4ee/node/x64-darwin.node -------------------------------------------------------------------------------- /node/x64-linux.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanwashere/opus/93e526621942221ea0bccf35d5b04484e3e6d4ee/node/x64-linux.node -------------------------------------------------------------------------------- /node/x64-win32.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanwashere/opus/93e526621942221ea0bccf35d5b04484e3e6d4ee/node/x64-win32.node -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "main": "lib.js", 4 | "version": "1.0.3", 5 | "types": "lib.d.ts", 6 | "name": "@evan/opus", 7 | "author": "evanwashere ", 8 | "homepage": "https://github.com/evanwashere/opus#readme", 9 | "description": "fast opus bindings for node and browsers", 10 | "keywords": ["fast", "opus", "wasm", "native", "bindings"], 11 | "bugs": { "url": "https://github.com/evanwashere/opus/issues" }, 12 | "repository": { "type": "git", "url": "git+https://github.com/evanwashere/opus.git" }, 13 | "exports": { ".": [{ "import": "./lib.mjs", "require": "./lib.js" }, "./lib.js"], "./*": "./*" } 14 | } 15 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | const { ctl, Encoder, Decoder } = require('../'); 2 | const e = new Encoder({ channels: 2, sample_rate: 48000 }); 3 | const d = new Decoder({ channels: 2, sample_rate: 48000 }); 4 | const sample = new Uint8Array(require('fs').readFileSync('./tests/test.pcm').subarray(0, 4 * 960)); 5 | 6 | if (9 !== e.complexity) throw 'complexity'; 7 | if (sample.length !== d.decode(e.encode(sample)).length) throw 'decoding'; -------------------------------------------------------------------------------- /tests/index.mjs: -------------------------------------------------------------------------------- 1 | import { ctl, Encoder } from '../lib.mjs'; 2 | const e = new Encoder({ sample_rate: 48000 }); 3 | 4 | if (9 !== e.complexity) throw 'complexity'; -------------------------------------------------------------------------------- /tests/index.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { Encoder } from '@evan/opus'; 3 | 4 | const e = new Encoder(); 5 | 6 | e.signal = 'music'; 7 | e.bitrate = 512_000; 8 | 9 | const packets = e.encode_pcm_stream(960, fs.createReadStream('./tests/test.pcm')); -------------------------------------------------------------------------------- /tests/test.pcm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanwashere/opus/93e526621942221ea0bccf35d5b04484e3e6d4ee/tests/test.pcm -------------------------------------------------------------------------------- /tests/wasm.js: -------------------------------------------------------------------------------- 1 | process.env.OPUS_FORCE_WASM = '1'; 2 | const { ctl, Encoder } = require('../'); 3 | const e = new Encoder({ sample_rate: 48000 }); 4 | 5 | if (9 !== e.complexity) throw 'complexity'; -------------------------------------------------------------------------------- /wasm/index.js: -------------------------------------------------------------------------------- 1 | let u8; 2 | let wasm; 3 | let pptr; 4 | let bptr; 5 | let pptrs; 6 | let bptrs; 7 | 8 | { 9 | const mod = new WebAssembly.Module(require('fs').readFileSync(require('path').join(__dirname, `${WebAssembly.validate(Uint8Array.of(0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2, 1, 0, 10, 9, 1, 7, 0, 65, 0, 253, 15, 26, 11)) ? 'simd' : 'opus'}.wasm`))); 10 | 11 | const instance = new WebAssembly.Instance(mod, { 12 | wasi_snapshot_preview1: { fd_seek() { }, fd_write() { }, fd_close() { }, proc_exit() { } }, 13 | 14 | env: { 15 | emscripten_notify_memory_growth() { 16 | u8 = new Uint8Array(wasm.memory.buffer); 17 | pptrs = u8.subarray(pptr, pptr + 2 ** 13); 18 | bptrs = u8.subarray(bptr, bptr + 2 ** 15); 19 | }, 20 | }, 21 | }); 22 | 23 | wasm = instance.exports; 24 | pptr = wasm.malloc(2 ** 13); 25 | bptr = wasm.malloc(2 ** 15); 26 | u8 = new Uint8Array(wasm.memory.buffer); 27 | pptrs = u8.subarray(pptr, pptr + 2 ** 13); 28 | bptrs = u8.subarray(bptr, bptr + 2 ** 15); 29 | } 30 | 31 | function err(code) { if (0 > code) throw new Error(`opus: ${load_static_string(u8, wasm.opus_strerror(code))}`); else return code; } 32 | function cgc(f) { return !('FinalizationRegistry' in globalThis) ? { delete(_) { }, add(_, __) { } } : { r: new FinalizationRegistry(f), delete(k) { this.r.unregister(k); }, add(k, v) { this.r.register(k, v, k); } }; } 33 | 34 | function load_static_string(u8, ptr) { 35 | let s = ''; 36 | const l = u8.length | 0; 37 | 38 | for (let o = ptr | 0; o < l; o++) { 39 | const x = u8[o]; 40 | if (0 === x) break; 41 | s += String.fromCharCode(x); 42 | } 43 | 44 | return s; 45 | } 46 | 47 | const pptrl = 2 ** 13; 48 | const gc = cgc(ptr => wasm.free(ptr)); 49 | 50 | class Decoder { 51 | #ptr = 0; 52 | 53 | constructor({ channels, sample_rate }) { 54 | gc.add(this, this.#ptr = wasm.malloc(wasm.opus_decoder_get_size(this.channels = channels || 2))); 55 | try { err(wasm.opus_decoder_init(this.#ptr, sample_rate || 48000, this.channels)); } catch (e) { throw (this.drop(), e); } 56 | } 57 | 58 | drop() { if (this.#ptr) (gc.delete(this), wasm.free(this.#ptr), this.#ptr = 0); } 59 | ctl(cmd, arg) { if (arg == null) return wasm.opus_decoder_ctl_get(this.#ptr, cmd); else return err(wasm.opus_decoder_ctl_set(this.#ptr, cmd, arg)); } 60 | 61 | decode(buffer) { 62 | pptrs.set(buffer = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)); 63 | return bptrs.slice(0, 2 * this.channels * err(wasm.opus_decode(this.#ptr, pptr, buffer.length, bptr, 5760, 0))); 64 | } 65 | } 66 | 67 | class Encoder { 68 | #ptr = 0; 69 | 70 | constructor({ channels, application, sample_rate }) { 71 | this.channels = channels || 2; 72 | gc.add(this, this.#ptr = wasm.malloc(wasm.opus_encoder_get_size(this.channels))); 73 | application = ({ voip: 2048, audio: 2049, restricted_lowdelay: 2051 })[application || 'audio']; 74 | try { err(wasm.opus_encoder_init(this.#ptr, sample_rate || 48000, this.channels, application)); } catch (e) { throw (this.drop(), e); } 75 | } 76 | 77 | drop() { if (this.#ptr) (gc.delete(this), wasm.free(this.#ptr), this.#ptr = 0); } 78 | ctl(cmd, arg) { if (arg == null) return wasm.opus_encoder_ctl_get(this.#ptr, cmd); else return err(wasm.opus_encoder_ctl_set(this.#ptr, cmd, arg)); } 79 | 80 | encode(buffer) { 81 | bptrs.set(buffer = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)); 82 | return pptrs.slice(0, err(wasm.opus_encode(this.#ptr, bptr, buffer.length / 2 / this.channels, pptr, pptrl))); 83 | } 84 | } 85 | 86 | module.exports = { Encoder, Decoder }; -------------------------------------------------------------------------------- /wasm/index.mjs: -------------------------------------------------------------------------------- 1 | import lib from './index.js'; 2 | export const Encoder = lib.Encoder; 3 | export const Decoder = lib.Decoder; -------------------------------------------------------------------------------- /wasm/opus.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanwashere/opus/93e526621942221ea0bccf35d5b04484e3e6d4ee/wasm/opus.wasm -------------------------------------------------------------------------------- /wasm/simd.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanwashere/opus/93e526621942221ea0bccf35d5b04484e3e6d4ee/wasm/simd.wasm --------------------------------------------------------------------------------