Skip to content

openclaw/libopus-wasm

Repository files navigation

libopus-wasm

CI Docs

Small, modern WebAssembly bindings for libopus raw packet encode/decode. One single-file ES module that runs unchanged in browsers and Node — no locateFile hook, no second .wasm request, no native build step.

The default path is realtime voice: 48 kHz, stereo, 20 ms frames, raw Opus packets, no Ogg/WebM container layer.

  • Browser and Node from one import. Bundles cleanly with Vite, webpack, esbuild.
  • Int16 and Float32 PCM — use whatever your pipeline already speaks.
  • Loss-resilient — in-band FEC and packet-loss concealment.
  • Tunable — bitrate, VBR/CBR, complexity, signal, bandwidth, DTX, plus a curated CTL passthrough.
  • Drop-in @discordjs/opus adapter — same method shape, no node-gyp.

📖 Full documentation: libopus-wasm.dev

Install

npm install libopus-wasm

ESM-only; Node 20+ or any current browser. No @types install needed.

Quick start

import { createDecoder, createEncoder, getPacketInfo } from "libopus-wasm";

const encoder = await createEncoder(); // 48 kHz, stereo, 20 ms, audio
const decoder = await createDecoder();

const pcm = new Int16Array(encoder.frameSize * encoder.channels); // 960 * 2
const packet = encoder.encode(pcm);    // Uint8Array — one raw Opus packet
const info = await getPacketInfo(packet); // duration, frames, bandwidth
const frame = decoder.decode(packet);  // Int16Array — interleaved PCM

encoder.free();
decoder.free();

Both factories share one lazily-loaded WASM module; the first call pays the load cost and the rest are cheap.

Examples

Float32 PCM

Encode and decode floats directly — ideal for Web Audio:

const frame = new Float32Array(encoder.frameSize * encoder.channels); // [-1, 1]
const packet = encoder.encodeFloat(frame);
const decoded = decoder.decodeFloat(packet); // Float32Array

Batches

const packets = encoder.encodeFrames([frameA, frameB, frameC]); // Uint8Array[]
const frames = decoder.decodeFrames(packets);                   // Int16Array[]

Packet loss: FEC + concealment

// Encoder: enable in-band FEC and declare the expected loss rate.
const encoder = await createEncoder({ fec: true, packetLossPercent: 15 });

// Decoder: a packet is lost. If the next packet is in hand, recover from its
// FEC data; otherwise synthesize a concealment frame.
const recovered = decoder.decode(nextPacket, { decodeFec: true, frameSize: 960 });
const concealed = decoder.decodePacketLoss(960); // == decode(null, { frameSize: 960 })

See Packet loss for the full receive loop.

Tuning the encoder

const encoder = await createEncoder({
  application: Application.Audio,
  bitrate: 96000,        // or "auto" / "max"
  complexity: 10,        // 0..10
  signal: Signal.Music,
  vbr: true,
});

encoder.setBitrate(128000);
encoder.setMaxBandwidth(Bandwidth.Wideband);
encoder.getBitrate();    // 128000

Deterministic cleanup with using

{
  using encoder = await createEncoder();
  using decoder = await createDecoder();
  decoder.decode(encoder.encode(new Int16Array(960 * 2)));
} // both freed automatically at scope exit

discord.js compatibility

libopus-wasm/discordjs matches the @discordjs/opus method shape, minus the native toolchain. It is Node-only (uses Buffer) and loads asynchronously:

import { OpusEncoder } from "libopus-wasm/discordjs";

const opus = await OpusEncoder.create(48000, 2);
const packet = opus.encode(pcmBuffer);
const decoded = opus.decode(packet);
opus.setBitrate(64000);
opus.setFEC(true);
opus.free();

Or construct directly and await ready to keep existing call sites:

const opus = new OpusEncoder(48000, 2);
await opus.ready;

More in discord.js compatibility.

Browser

The main entry inlines the WASM, so it bundles with no plugins and needs no cross-origin isolation. Web Audio delivers Float32 samples that go straight into encodeFloat — see Browser usage for a microphone-capture walkthrough.

API overview

Full reference with every option and constant lives at libopus-wasm.dev/api-reference.

Top-level

Function Returns Description
loadLibopus() Promise<{ version }> Loads the module; returns the bundled libopus version.
createEncoder(options?) Promise<OpusEncoderHandle> Create a raw-packet encoder.
createDecoder(options?) Promise<OpusDecoderHandle> Create a raw-packet decoder.
getPacketInfo(packet, options?) Promise<OpusPacketInfo> Validate a raw packet and return duration, frame count, channels, and bandwidth.

Encoder

Member Description
encode(pcm, options?) Encode one Int16 frame (Int16Array | Uint8Array) → Uint8Array.
encodeFloat(pcm, options?) Encode one Float32Array frame → Uint8Array.
encodeFrames / encodeFloatFrames Batch variants → Uint8Array[].
setBitrate / getBitrate Bitrate (number | "auto" | "max").
setComplexity setSignal setMaxBandwidth Quality and bandwidth controls.
setVbr setVbrConstraint setDtx Rate-mode controls.
setFec setPacketLossPercent Loss-resilience controls.
getLookahead getInDtx Encoder state.
encoderCtl(request, value) Curated integer-setter CTL passthrough.
free() / [Symbol.dispose]() Release WASM memory.

Read-only: application, channels, frameSize, sampleRate.

Decoder

Member Description
decode(packet, options?) Decode a packet (or null for PLC) → Int16Array.
decodeFloat(packet, options?) Decode → Float32Array.
decodeFrames / decodeFloatFrames Batch variants; null entries are concealed.
decodePacketLoss(frameSize?) Synthesize one concealment frame.
decodePacketLossFloat(frameSize?) Float32 variant.
decoderCtl(request, value) Integer-setter CTL passthrough.
free() / [Symbol.dispose]() Release WASM memory.

Read-only: channels, maxFrameSize, sampleRate.

Constants

Application (Voip, Audio, RestrictedLowDelay) · Signal (Auto, Voice, Music) · Bitrate (Auto, Max) · Bandwidth (NarrowbandFullband) · EncoderCtl / DecoderCtl request codes · OpusError (code, operation).

Supported formats

Constraint Allowed values
Sample rate 8000, 12000, 16000, 24000, 48000 Hz
Channels 1 (mono), 2 (stereo)
Encode frame duration 2.5, 5, 10, 20, 40, 60 ms
Decode output capacity up to 120 ms
PLC / FEC frame size multiples of 2.5 ms, up to 120 ms

Validation errors (wrong frame size, out-of-range option, empty packet, non-allow-listed CTL) throw a RangeError before reaching WASM; libopus errors surface as OpusError.

Build from source

The npm package ships compiled output, so using it needs no toolchain. Building from source requires Emscripten (emcc) on PATH:

pnpm install
pnpm build
pnpm test

pnpm build downloads libopus 1.6.1 from Xiph.Org, verifies the pinned SHA-256, compiles it with Emscripten, and emits a single-file ES module under dist/generated/. See Building from source.

Benchmark

Native comparison requires @discordjs/opus to build on the host:

pnpm benchmark

Apple Silicon, Node 26, 20k iterations, 48 kHz stereo, 20 ms frames:

wasm encode:   15,304 ops/sec
native encode: 15,741 ops/sec
wasm decode:   38,416 ops/sec
native decode: 41,280 ops/sec

A regression check, not a portable score. CI also exposes a manual Benchmark workflow. More in Benchmark.

Documentation & license

Full docs: libopus-wasm.dev. Released under the MIT license; libopus carries its own BSD license, reproduced in THIRD_PARTY_NOTICES. Not affiliated with Xiph.Org.

About

Small, modern WASM bindings for libopus raw packet encode/decode.

Resources

License

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors