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.

JavaScriptintermediate
17 min read

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:

javascriptjavascript
// 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
});
PropertyDescription
ctx.state"suspended", "running", or "closed"
ctx.sampleRateSamples per second (44100 or 48000 typically)
ctx.currentTimeElapsed time in seconds since context creation
ctx.destinationFinal output node (speakers)
ctx.listenerSpatial audio listener position

Generating Sound with Oscillators

javascriptjavascript
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, muted

Loading and Playing Audio Files

javascriptjavascript
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)

javascriptjavascript
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 TypeEffect
lowpassPasses frequencies below cutoff, removes highs
highpassPasses frequencies above cutoff, removes lows
bandpassPasses frequencies near cutoff, removes others
notchRemoves frequencies near cutoff, passes others
lowshelfBoosts or cuts frequencies below cutoff
highshelfBoosts or cuts frequencies above cutoff
peakingBoosts or cuts frequencies around cutoff
allpassPasses all frequencies, shifts phase

Real-Time Audio Analysis

javascriptjavascript
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

javascriptjavascript
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

javascriptjavascript
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

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 setTargetAtTime and linearRampToValueAtTime for 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
RunePowered by Rune AI

Frequently Asked Questions

Why does AudioContext start in a suspended state?

Browser autoplay policies require user interaction before audio can play. The `AudioContext` starts suspended until `ctx.resume()` is called inside a user-triggered event handler (click, keydown). Always attach audio initialization to a button click or similar gesture.

How many AudioContext instances should I create?

One per application. Creating multiple contexts wastes resources and can cause synchronization issues. Reuse a single `AudioContext` and create/connect new nodes within it as needed. The `close()` method permanently destroys a context.

Can the Web Audio API process audio from a microphone?

Yes. Use `navigator.mediaDevices.getUserMedia({ audio: true })` to get a `MediaStream`, then create a source with `ctx.createMediaStreamSource(stream)`. Connect this source to `AnalyserNode` for visualization or processing. See [Requesting Desktop Notification Permissions in JS](/tutorials/programming-languages/javascript/requesting-desktop-notification-permissions-in-js) for a similar permission request pattern.

What is the difference between `setValueAtTime` and `setTargetAtTime`?

`setValueAtTime` sets an instant value at a specific time. `setTargetAtTime` exponentially approaches a target value with a time constant (smoothing). Use `setValueAtTime` for precise scheduling and `setTargetAtTime` for smooth parameter transitions like volume fading.

Does the Web Audio API work on mobile browsers?

Yes, but with restrictions. Mobile Safari and Chrome require user interaction to start the `AudioContext`. Some mobile browsers limit the number of simultaneous audio sources. Always test on target devices and handle the suspended state gracefully with a "tap to start" interaction.

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.