Using the Web Audio API in JavaScript Full Guide
A complete guide to the Web Audio API in JavaScript. Covers AudioContext, oscillators, gain nodes, audio buffers, spatial audio, real-time analysis with AnalyserNode, audio filters, connecting nodes in a graph, building a synthesizer, and creating audio visualizations.
The Web Audio API provides a powerful system for generating, processing, and analyzing audio in the browser. It uses a modular routing architecture where audio nodes connect in a graph, flowing from sources through effects to the destination (speakers).
AudioContext Basics
Every Web Audio application starts with an AudioContext. This is the environment where all audio operations happen:
// Create a single AudioContext for the application
let audioCtx = null;
function getAudioContext() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
return audioCtx;
}
// Resume context on user interaction (required by autoplay policies)
async function ensureAudioReady() {
const ctx = getAudioContext();
if (ctx.state === "suspended") {
await ctx.resume();
}
return ctx;
}
document.getElementById("start-audio").addEventListener("click", async () => {
const ctx = await ensureAudioReady();
console.log("AudioContext state:", ctx.state); // "running"
console.log("Sample rate:", ctx.sampleRate); // 44100 or 48000
});| Property | Description |
|---|---|
ctx.state | "suspended", "running", or "closed" |
ctx.sampleRate | Samples per second (44100 or 48000 typically) |
ctx.currentTime | Elapsed time in seconds since context creation |
ctx.destination | Final output node (speakers) |
ctx.listener | Spatial audio listener position |
Generating Sound with Oscillators
function playTone(frequency = 440, type = "sine", duration = 1) {
const ctx = getAudioContext();
const oscillator = ctx.createOscillator();
oscillator.type = type; // "sine", "square", "sawtooth", "triangle"
oscillator.frequency.value = frequency;
const gainNode = ctx.createGain();
gainNode.gain.value = 0.3;
// Fade out to avoid clicks
gainNode.gain.setValueAtTime(0.3, ctx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration);
oscillator.connect(gainNode);
gainNode.connect(ctx.destination);
oscillator.start(ctx.currentTime);
oscillator.stop(ctx.currentTime + duration);
return oscillator;
}
// Play different waveforms
playTone(440, "sine", 1); // Pure tone
playTone(440, "square", 1); // Hollow, retro
playTone(440, "sawtooth", 1); // Bright, buzzy
playTone(440, "triangle", 1); // Soft, mutedLoading and Playing Audio Files
class AudioPlayer {
constructor() {
this.ctx = getAudioContext();
this.bufferCache = new Map();
}
async loadAudio(url) {
if (this.bufferCache.has(url)) {
return this.bufferCache.get(url);
}
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await this.ctx.decodeAudioData(arrayBuffer);
this.bufferCache.set(url, audioBuffer);
return audioBuffer;
}
play(buffer, options = {}) {
const source = this.ctx.createBufferSource();
source.buffer = buffer;
source.loop = options.loop || false;
source.playbackRate.value = options.playbackRate || 1;
const gainNode = this.ctx.createGain();
gainNode.gain.value = options.volume || 1;
source.connect(gainNode);
gainNode.connect(this.ctx.destination);
source.start(0, options.offset || 0);
return {
source,
gainNode,
stop: () => source.stop(),
setVolume: (v) => {
gainNode.gain.setTargetAtTime(v, this.ctx.currentTime, 0.05);
},
};
}
async loadAndPlay(url, options = {}) {
const buffer = await this.loadAudio(url);
return this.play(buffer, options);
}
}
// Usage
const player = new AudioPlayer();
const handle = await player.loadAndPlay("/audio/background.mp3", {
loop: true,
volume: 0.5,
});
// Later: adjust volume
handle.setVolume(0.2);
// Stop playback
handle.stop();Audio Filters (BiquadFilter)
function createFilteredSound(frequency, filterType, filterFreq) {
const ctx = getAudioContext();
const oscillator = ctx.createOscillator();
oscillator.type = "sawtooth";
oscillator.frequency.value = frequency;
const filter = ctx.createBiquadFilter();
filter.type = filterType; // "lowpass", "highpass", "bandpass", "notch"
filter.frequency.value = filterFreq;
filter.Q.value = 10; // Resonance
const gain = ctx.createGain();
gain.gain.value = 0.3;
oscillator.connect(filter);
filter.connect(gain);
gain.connect(ctx.destination);
oscillator.start();
// Sweep the filter frequency for a "wah" effect
filter.frequency.setValueAtTime(100, ctx.currentTime);
filter.frequency.exponentialRampToValueAtTime(3000, ctx.currentTime + 2);
setTimeout(() => oscillator.stop(), 2500);
}
// Low-pass filter: removes high frequencies
createFilteredSound(220, "lowpass", 800);| Filter Type | Effect |
|---|---|
lowpass | Passes frequencies below cutoff, removes highs |
highpass | Passes frequencies above cutoff, removes lows |
bandpass | Passes frequencies near cutoff, removes others |
notch | Removes frequencies near cutoff, passes others |
lowshelf | Boosts or cuts frequencies below cutoff |
highshelf | Boosts or cuts frequencies above cutoff |
peaking | Boosts or cuts frequencies around cutoff |
allpass | Passes all frequencies, shifts phase |
Real-Time Audio Analysis
class AudioAnalyzer {
constructor(source) {
this.ctx = getAudioContext();
this.analyser = this.ctx.createAnalyser();
this.analyser.fftSize = 2048;
this.analyser.smoothingTimeConstant = 0.8;
source.connect(this.analyser);
this.analyser.connect(this.ctx.destination);
this.dataArray = new Uint8Array(this.analyser.frequencyBinCount);
this.floatData = new Float32Array(this.analyser.frequencyBinCount);
}
getFrequencyData() {
this.analyser.getByteFrequencyData(this.dataArray);
return this.dataArray;
}
getTimeDomainData() {
this.analyser.getByteTimeDomainData(this.dataArray);
return this.dataArray;
}
getVolume() {
this.analyser.getFloatTimeDomainData(this.floatData);
let sum = 0;
for (let i = 0; i < this.floatData.length; i++) {
sum += this.floatData[i] * this.floatData[i];
}
return Math.sqrt(sum / this.floatData.length);
}
getPeakFrequency() {
this.analyser.getByteFrequencyData(this.dataArray);
let maxIndex = 0;
let maxValue = 0;
for (let i = 0; i < this.dataArray.length; i++) {
if (this.dataArray[i] > maxValue) {
maxValue = this.dataArray[i];
maxIndex = i;
}
}
return (maxIndex * this.ctx.sampleRate) / this.analyser.fftSize;
}
}Canvas Audio Visualization
function drawWaveform(canvas, analyzer) {
const canvasCtx = canvas.getContext("2d");
const width = canvas.width;
const height = canvas.height;
function draw() {
requestAnimationFrame(draw);
const data = analyzer.getTimeDomainData();
canvasCtx.fillStyle = "#1a1a2e";
canvasCtx.fillRect(0, 0, width, height);
canvasCtx.lineWidth = 2;
canvasCtx.strokeStyle = "#00d4ff";
canvasCtx.beginPath();
const sliceWidth = width / data.length;
let x = 0;
for (let i = 0; i < data.length; i++) {
const v = data[i] / 128.0;
const y = (v * height) / 2;
if (i === 0) {
canvasCtx.moveTo(x, y);
} else {
canvasCtx.lineTo(x, y);
}
x += sliceWidth;
}
canvasCtx.lineTo(width, height / 2);
canvasCtx.stroke();
}
draw();
}
function drawFrequencyBars(canvas, analyzer) {
const canvasCtx = canvas.getContext("2d");
const width = canvas.width;
const height = canvas.height;
function draw() {
requestAnimationFrame(draw);
const data = analyzer.getFrequencyData();
canvasCtx.fillStyle = "#1a1a2e";
canvasCtx.fillRect(0, 0, width, height);
const barCount = 64;
const step = Math.floor(data.length / barCount);
const barWidth = width / barCount - 1;
for (let i = 0; i < barCount; i++) {
const value = data[i * step];
const barHeight = (value / 255) * height;
const hue = (i / barCount) * 240;
canvasCtx.fillStyle = `hsl(${hue}, 80%, 55%)`;
canvasCtx.fillRect(
i * (barWidth + 1),
height - barHeight,
barWidth,
barHeight
);
}
}
draw();
}Building a Simple Synthesizer
class Synthesizer {
constructor() {
this.ctx = getAudioContext();
this.masterGain = this.ctx.createGain();
this.masterGain.gain.value = 0.5;
this.masterGain.connect(this.ctx.destination);
this.activeNotes = new Map();
}
noteOn(note, velocity = 0.7) {
if (this.activeNotes.has(note)) this.noteOff(note);
const frequency = 440 * Math.pow(2, (note - 69) / 12);
const osc = this.ctx.createOscillator();
osc.type = "sawtooth";
osc.frequency.value = frequency;
const gain = this.ctx.createGain();
gain.gain.value = 0;
// Attack
gain.gain.setValueAtTime(0, this.ctx.currentTime);
gain.gain.linearRampToValueAtTime(velocity, this.ctx.currentTime + 0.05);
// Sustain
gain.gain.setTargetAtTime(velocity * 0.7, this.ctx.currentTime + 0.05, 0.2);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start();
this.activeNotes.set(note, { osc, gain });
}
noteOff(note) {
const active = this.activeNotes.get(note);
if (!active) return;
const { osc, gain } = active;
// Release
gain.gain.cancelScheduledValues(this.ctx.currentTime);
gain.gain.setValueAtTime(gain.gain.value, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.3);
osc.stop(this.ctx.currentTime + 0.3);
this.activeNotes.delete(note);
}
setVolume(value) {
this.masterGain.gain.setTargetAtTime(value, this.ctx.currentTime, 0.05);
}
panic() {
for (const note of this.activeNotes.keys()) {
this.noteOff(note);
}
}
}
// Usage: keyboard-controlled synth
const synth = new Synthesizer();
const keyMap = { a: 60, s: 62, d: 64, f: 65, g: 67, h: 69, j: 71, k: 72 };
document.addEventListener("keydown", (e) => {
if (keyMap[e.key] && !e.repeat) synth.noteOn(keyMap[e.key]);
});
document.addEventListener("keyup", (e) => {
if (keyMap[e.key]) synth.noteOff(keyMap[e.key]);
});Rune AI
Key Insights
- One AudioContext per app: Create a single context and reuse it for all audio operations to avoid resource waste and synchronization issues
- User interaction required: Browser autoplay policies suspend the context until a user gesture triggers
ctx.resume() - Node graph architecture: Connect sources through processing nodes (gain, filter, analyser) to the destination in a flexible routing graph
- Smooth parameter changes: Use
setTargetAtTimeandlinearRampToValueAtTimefor click-free transitions instead of setting values directly - AnalyserNode for visualization: Extract frequency and time-domain data in real-time to drive canvas-based waveforms and spectrum displays
Frequently Asked Questions
Why does AudioContext start in a suspended state?
How many AudioContext instances should I create?
Can the Web Audio API process audio from a microphone?
What is the difference between `setValueAtTime` and `setTargetAtTime`?
Does the Web Audio API work on mobile browsers?
Conclusion
The Web Audio API provides a complete audio processing pipeline through its node-based architecture. Start with AudioContext, generate sound with oscillators, load files into buffers, apply filters, and visualize with AnalyserNode. Always resume the context on user interaction and reuse a single context across your application. For storing user audio preferences, use localStorage. For observing DOM changes to audio controls, see JavaScript Mutation Observer.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.