Web Audio & advanced

wavesurfer.js draws its waveform and handles playback independently, so you can layer a full Web Audio graph on top of a standard MediaElement instance without switching backends.

When you need Web Audio

The default MediaElement backend gives you streaming playback with low memory use. Connect a Web Audio graph on top when you need:

  • Audio effects — equalizers, compressors, reverb, distortion, or any AudioNode chain.
  • Spatial audio — stereo panning, 3D positioning with PannerNode.
  • Real-time analysisAnalyserNode for frequency or waveform data (visualisations, metering).
  • Precise routing — sending audio to multiple destinations or sidechain processing.

You do not need backend: 'WebAudio' for any of these. All the examples on this page use the default MediaElement backend and connect a Web Audio graph to the underlying <audio> element via createMediaElementSource(). See Core concepts for a full comparison of the two backends.

Connect your Web Audio graph inside the 'play' event (or later), not at creation time. Many browsers suspend the AudioContext until there has been a user gesture — creating nodes before first play can leave the context in a suspended state and produce no audio.


Connecting an audio graph

The entry point to Web Audio is ws.getMediaElement(), which returns the underlying HTMLMediaElement. Wrap it in a MediaElementAudioSourceNode once, then chain whatever nodes you need.

Equalizer

The following mirrors the approach used in the live Web Audio demo: one BiquadFilterNode per frequency band, connected in series.

import WaveSurfer from 'wavesurfer.js'

const eqBands = [32, 64, 125, 250, 500, 1000, 2000, 4000, 8000, 16000]

const ws = WaveSurfer.create({
  container: '#waveform',
  url: '/audio/track.mp3',
  mediaControls: true,
})

ws.once('play', () => {
  // Create the AudioContext on first play (satisfies browser autoplay policy)
  const audioContext = new AudioContext()

  // Create one BiquadFilterNode per band
  const filters = eqBands.map((band) => {
    const filter = audioContext.createBiquadFilter()
    filter.type = band <= 32 ? 'lowshelf' : band >= 16000 ? 'highshelf' : 'peaking'
    filter.frequency.value = band
    filter.gain.value = 0  // flat — adjust per band as needed
    filter.Q.value = 1
    return filter
  })

  // Wrap the <audio> element in a source node
  const mediaNode = audioContext.createMediaElementSource(ws.getMediaElement())

  // Chain: source → filter[0] → filter[1] → … → destination
  const output = filters.reduce((prev, curr) => {
    prev.connect(curr)
    return curr
  }, mediaNode)

  output.connect(audioContext.destination)

  // Wire up slider controls
  document.querySelectorAll('.eq-slider').forEach((slider, i) => {
    slider.oninput = () => {
      filters[i].gain.value = Number(slider.value)
    }
  })
})

Once you call createMediaElementSource(element), the browser reroutes audio from the element into your graph and away from the default output. You must connect the last node in your chain to audioContext.destination (or another output node) — otherwise no audio will be heard.

Each <audio> element can only be wrapped in a MediaElementAudioSourceNode once per AudioContext. Guard against calling createMediaElementSource more than once (e.g. cache the node or create it outside the event handler if the user can pause and play repeatedly — the 'play' event fires every time playback starts).


Panning

Add a StereoPannerNode between the source and destination to pan audio left or right. A value of -1 is full left, 0 is centre, and 1 is full right.

import WaveSurfer from 'wavesurfer.js'

const ws = WaveSurfer.create({
  container: '#waveform',
  url: '/audio/track.mp3',
  mediaControls: true,
})

let panner = null

ws.once('play', () => {
  const audioContext = new AudioContext()
  const mediaNode = audioContext.createMediaElementSource(ws.getMediaElement())

  panner = audioContext.createStereoPanner()
  panner.pan.value = 0  // centre

  mediaNode.connect(panner)
  panner.connect(audioContext.destination)
})

// Change pan value from a slider (range: -1 to 1)
document.querySelector('#pan-slider').oninput = (e) => {
  if (panner) panner.pan.value = Number(e.target.value)
}

The legacy /example/panner/ demo uses a PannerNode (3D) with setPosition(). For simple left/right stereo panning, StereoPannerNode is simpler and more predictable.


Pitch and time-stretch

wavesurfer.js does not include pitch shifting or time-stretching. Adjusting ws.setPlaybackRate() changes speed and pitch together (unless preservePitch: true is supported by the browser’s media engine, which is not guaranteed).

For independent pitch or tempo control, integrate a dedicated library:

  • Tone.js — feature-rich Web Audio framework with Tone.PitchShift and Tone.Player. Wire it into wavesurfer by passing the AudioContext from Tone: new AudioContext() → use Tone.context.rawContext.
  • SoundTouch.js — a port of the SoundTouch DSP library; runs as a ScriptProcessorNode or AudioWorklet and can independently adjust pitch and tempo.

Both approaches follow the same graph pattern: wrap the wavesurfer media element with createMediaElementSource(), insert the pitch/stretch processor node, then connect to the destination.


Multitrack

wavesurfer-multitrack is a separate library that lines up multiple wavesurfer instances on a shared timeline with draggable start positions, per-track volume envelopes, and fade in/out handles.

import Multitrack from 'wavesurfer-multitrack'

const multitrack = Multitrack.create(
  [
    {
      id: 1,
      url: '/audio/track1.mp3',
      startPosition: 0,
      draggable: true,
      volume: 0.8,
      options: { waveColor: 'hsl(46, 87%, 49%)', progressColor: 'hsl(46, 87%, 20%)' },
    },
    {
      id: 2,
      url: '/audio/track2.mp3',
      startPosition: 14,   // seconds into the timeline
      draggable: true,
      volume: 0.9,
      options: { waveColor: 'hsl(161, 87%, 49%)', progressColor: 'hsl(161, 87%, 20%)' },
    },
  ],
  {
    container: document.querySelector('#container'),
    minPxPerSec: 10,
    cursorColor: '#D72F21',
  },
)

// Destroy all instances when done
window.onbeforeunload = () => multitrack.destroy()

Known caveats

  • Full-file downloads even with peaks. Each track still fetches its full audio file for Web Audio decoding, even when you supply pre-computed peaks. On long sessions with many tracks this can be a significant bandwidth cost.
  • Sync accuracy. Tracks are aligned by scheduling AudioBufferSourceNode start times. Minor drift can occur after long playback or when the browser throttles timers in background tabs.
  • No server-side mix. The mix is client-side only; exporting a rendered file requires an additional step (e.g. recording the AudioContext output with a MediaRecorder).