Audio Pipeline
The #1 user complaint across all browser emulators is audio crackling. We fix it permanently.
The Problem
Every browser N64 emulator suffers from audio issues:
| Emulator | Audio Issues |
|---|---|
| EmulatorJS | Chrome/Safari static, Firefox degradation after 5 seconds |
| N64Wasm | Some users get no audio at all |
| mupen64plus-web | "Audio is experimental" |
Root cause: They all use ScriptProcessorNode (deprecated since 2014) or the Emscripten OpenAL bridge, both of which process audio on the main thread with unpredictable timing.
When the main thread is busy rendering a frame or handling input, audio processing gets delayed → pops, crackles, static.
Our Architecture: AudioWorklet
Why AudioWorklet Is Superior
| ScriptProcessorNode | AudioWorklet | |
|---|---|---|
| Thread | Main thread | Dedicated audio thread |
| Timing | Irregular (event-based) | Deterministic (128 samples) |
| Latency | High (2048+ samples) | Low (128 samples) |
| Priority | Competes with rendering | OS real-time priority |
| Status | Deprecated | Standard |
| Buffer underrun | Audible pop | Graceful interpolation |
Implementation
Ring Buffer (SharedArrayBuffer)
// Shared between emulation worker and audio worklet
class AudioRingBuffer {
private buffer: Float32Array; // SharedArrayBuffer-backed
private writeHead: Int32Array; // Atomic write position
private readHead: Int32Array; // Atomic read position
private capacity: number;
write(samples: Float32Array): void {
const head = Atomics.load(this.writeHead, 0);
// Copy samples into ring buffer
for (let i = 0; i < samples.length; i++) {
this.buffer[(head + i) % this.capacity] = samples[i];
}
Atomics.store(this.writeHead, 0, (head + samples.length) % this.capacity);
Atomics.notify(this.writeHead, 0);
}
read(output: Float32Array): number {
const write = Atomics.load(this.writeHead, 0);
const read = Atomics.load(this.readHead, 0);
const available = (write - read + this.capacity) % this.capacity;
const toRead = Math.min(available, output.length);
for (let i = 0; i < toRead; i++) {
output[i] = this.buffer[(read + i) % this.capacity];
}
Atomics.store(this.readHead, 0, (read + toRead) % this.capacity);
return toRead;
}
}
AudioWorklet Processor
// audio-processor.js (runs on audio thread)
class N64AudioProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.ringBuffer = null;
}
process(inputs, outputs, parameters) {
const output = outputs[0];
const left = output[0]; // 128 samples
const right = output[1]; // 128 samples
if (this.ringBuffer) {
// Read interleaved stereo from ring buffer
const stereo = new Float32Array(256);
const read = this.ringBuffer.read(stereo);
for (let i = 0; i < 128; i++) {
left[i] = i < read / 2 ? stereo[i * 2] : left[i - 1] || 0;
right[i] = i < read / 2 ? stereo[i * 2 + 1] : right[i - 1] || 0;
}
}
return true; // Keep processor alive
}
}
registerProcessor('n64-audio', N64AudioProcessor);
Audio-Driven Frame Pacing
The audio system becomes the master clock for emulation. This is the same technique used by the most polished native emulators (RetroArch, Ares):
Why this works:
- Audio hardware has a fixed sample rate (48kHz) — it's the most consistent clock in the system
- When the audio buffer runs low, emulation runs faster to fill it
- When the buffer is full, emulation pauses naturally
- Result: perfectly smooth audio AND correct emulation speed
Sample Rate Conversion
The N64 outputs audio at variable rates (typically 22050Hz or 32000Hz depending on the game). We resample to the system rate (typically 48000Hz):
N64 Audio Output (22050Hz) → Blip Buffer Resampler → Ring Buffer (48000Hz) → AudioWorklet → Speakers
The resampler uses band-limited interpolation (blip_buf algorithm) for high-quality upsampling without aliasing artifacts.
Graceful Degradation
If the emulator can't keep up (low-end device):
- Buffer underrun → Interpolate last known samples (smooth fade, not silence pop)
- Sustained underrun → Drop emulation to 55fps (90% speed) — better than choppy audio
- Audio context suspended (user hasn't interacted) → Show "Tap to start" overlay
This ensures audio never pops or crackles, even on slow devices. The worst case is slightly slow gameplay — not broken audio.