Streamed Opus File
Overview
Section titled “Overview”Streamed Opus File solves a common problem in web audio: how do you start playing a compressed audio file before it finishes downloading? Instead of fetching the entire Opus file and decoding it in one shot, this project streams chunks through a WebAssembly-based Opus decoder running in a Web Worker, then pipes the decoded PCM samples into an AudioWorklet for gapless real-time playback. The result is near-instant playback start with minimal memory overhead — critical for music apps, game audio, and interactive experiences on bandwidth-constrained connections.
How It Works
Section titled “How It Works”The architecture is a three-stage pipeline that keeps the main thread completely free:
- Fetch + stream — The browser’s
ReadableStreamAPI pulls chunks from the network as they arrive - Decode in a Worker — A dedicated Web Worker runs
OpusStreamDecoder, a C-to-WebAssembly compiled Opus decoder (via Emscripten), decoding each chunk into stereoFloat32ArrayPCM - Playback via AudioWorklet — Decoded samples are transferred (zero-copy via
Transferableobjects) to a customAudioWorkletProcessorthat writes them straight into the audio output buffer
// index.ts — Main thread orchestrationconst decoder = new Worker(decoderUrl, { type: 'module' });await context.audioWorklet.addModule(processorUrl);
const node = new AudioWorkletNode(context, 'stream-audio-processor', { numberOfInputs: 0, numberOfOutputs: 1, outputChannelCount: [2]});
decoder.addEventListener('message', (ev) => { // Transfer decoded buffers to the worklet — no copying node.port.postMessage( { left: ev.data.left, right: ev.data.right, index: ev.data.index }, [ev.data.left, ev.data.right] );});The worklet processor maintains pre-allocated Int16Array ring buffers (sized for ~390 seconds of audio at 48 kHz) and converts to float on the fly during the process() callback — keeping allocations out of the real-time audio thread:
// stream-audio-processor.ts — Runs on the audio rendering threadfor (let i = 0; i < leftChannel.length; i++) { const sourceIndex = this.index + i; leftChannel[i] = (this.leftBuffer[sourceIndex] || 0) / 0x7FFF; rightChannel[i] = (this.rightBuffer[sourceIndex] || 0) / 0x7FFF;}this.index += leftChannel.length;Features
Section titled “Features”- Streaming decode — Playback begins as soon as the first Opus page is decoded, not after the full file downloads
- Off-main-thread architecture — Decoding runs in a Web Worker, playback runs in an AudioWorklet; the main thread does zero audio processing
- WebAssembly Opus decoder — Uses a compiled-from-C
libopusvia Emscripten for native-speed decoding in the browser - Zero-copy buffer transfer — Decoded PCM buffers are sent between Worker → Worklet using
Transferableobjects, eliminating expensive data copies - Stereo 48 kHz output — Matches the Opus codec’s native sample rate for artifact-free playback
- Bun-powered dev server —
serve.tstranspiles TypeScript on the fly withBun.build, enabling a zero-config development workflow - GitHub Pages deploy — CI workflow builds with Bun and deploys to GitHub Pages automatically
Tech Stack
Section titled “Tech Stack”| Language | TypeScript, JavaScript |
| Audio Codec | Opus (via Emscripten/WebAssembly) |
| Web APIs | AudioWorklet, Web Workers, ReadableStream, Transferable Objects |
| Build / Dev | Bun (Bun.build, Bun.serve) |
| Deploy | GitHub Pages via GitHub Actions |
Live Demo
Section titled “Live Demo”A live demo is deployed at jadujoel.github.io/streamed-opus-file.
Source Code
Section titled “Source Code”The source code is available on the project’s GitHub repository.