Skip to main content

howler.js vs tone.js vs wavesurfer.js: Web Audio in JavaScript (2026)

·PkgPulse Team

TL;DR

howler.js is the best for sound effects and audio playback — handles format fallbacks (WebM/MP3/OGG), 3D spatial audio, audio sprites, and works across all browsers. tone.js is the synthesizer and music programming library — built on the Web Audio API for scheduling, synthesis, effects, and building music applications. wavesurfer.js is the waveform visualization library — displays audio as an interactive waveform for players, podcasts, and audio editors. These three solve different problems: use howler for game sounds and UI audio, tone.js for synthesis and DAW-like features, and wavesurfer.js for waveform display.

Key Takeaways

  • howler.js: ~1.5M weekly downloads — multi-format, 3D audio, audio sprites, Web Audio + HTML5 fallback
  • tone.js: ~600K weekly downloads — Web Audio synthesis, scheduling, effects chain, music apps
  • wavesurfer.js: ~500K weekly downloads — waveform visualization, interactive playback, plugins
  • All three are browser-based — Node.js doesn't have Web Audio API
  • howler.js handles cross-browser format compatibility automatically
  • tone.js uses a higher-level musical abstraction (notes, beats, BPM) over raw Web Audio nodes

howler.js

howler.js — audio made easy for modern web:

Basic usage

import { Howl, Howler } from "howler"

// Create and play a sound:
const sound = new Howl({
  src: ["click.webm", "click.mp3"],  // Format fallback — uses first supported
  volume: 0.8,
  loop: false,
  preload: true,  // Load immediately
})

// Play:
const id = sound.play()

// Control:
sound.pause(id)
sound.stop(id)
sound.seek(2.5, id)      // Seek to 2.5 seconds
sound.volume(0.5, id)    // Set volume 0-1
sound.rate(1.5, id)      // Playback rate (1.5x speed)
sound.mute(true, id)     // Mute without stopping

// Events:
const bgMusic = new Howl({
  src: ["background.mp3"],
  loop: true,
  volume: 0.4,
  onplay: () => console.log("Music started"),
  onend: () => console.log("Music ended"),
  onload: () => console.log("Audio loaded"),
  onloaderror: (id, err) => console.error("Load failed:", err),
})

Audio sprites (multiple sounds in one file)

import { Howl } from "howler"

// Audio sprite — one file, multiple named clips (efficient for game sounds):
const uiSounds = new Howl({
  src: ["ui-sounds.webm", "ui-sounds.mp3"],
  sprite: {
    click: [0, 200],          // Start: 0ms, Duration: 200ms
    hover: [300, 150],        // Start: 300ms, Duration: 150ms
    success: [600, 800],      // Start: 600ms, Duration: 800ms
    error: [1500, 600],       // Start: 1500ms, Duration: 600ms
    notification: [2200, 400],
  },
})

// Play specific sprite:
uiSounds.play("click")
uiSounds.play("success")

// Preload everything in one HTTP request — much better than individual files

3D spatial audio

import { Howl } from "howler"

// 3D positional audio (uses PannerNode):
const footstep = new Howl({
  src: ["footstep.mp3"],
  pannerAttr: {
    panningModel: "HRTF",           // Head-related transfer function (most realistic)
    distanceModel: "inverse",
    refDistance: 1,
    maxDistance: 100,
    rolloffFactor: 2,
  },
})

// Position the sound in 3D space:
footstep.pos(3, 0, -5)  // x, y, z position
footstep.play()

// Move the listener (camera/player position):
Howler.pos(0, 0, 0)        // Listener position
Howler.orientation(0, 0, -1, 0, 1, 0)  // Direction + up vector

// Useful for: games, VR audio, 3D environments

Global controls

import { Howler } from "howler"

// Global mute (mutes ALL sounds):
Howler.mute(true)
Howler.mute(false)

// Global volume:
Howler.volume(0.5)

// Unload all sounds (free memory):
Howler.unload()

// Check codec support:
Howler.codecs("webm")   // true/false
Howler.codecs("mp3")    // true/false
Howler.codecs("ogg")    // true/false

tone.js

tone.js — Web Audio synthesis and music programming:

Basic synthesis

import * as Tone from "tone"

// Start audio context (required after user interaction):
await Tone.start()

// Simple synthesizer:
const synth = new Tone.Synth().toDestination()

// Play a note:
synth.triggerAttackRelease("C4", "8n")  // C4 for an eighth note
synth.triggerAttackRelease("A4", 0.5)   // A4 for 0.5 seconds
synth.triggerAttackRelease("G3", "4n", Tone.now() + 1)  // G3, starts in 1 second

// Trigger attack (hold) and release separately:
synth.triggerAttack("C5")        // Start holding the note
await Tone.sleep(0.3)
synth.triggerRelease()           // Release

Polyphonic synth and chords

import * as Tone from "tone"

await Tone.start()

// Polyphonic synth — play chords:
const polySynth = new Tone.PolySynth(Tone.Synth).toDestination()

// C major chord:
polySynth.triggerAttackRelease(["C4", "E4", "G4"], "2n")

// Schedule chord sequence:
const part = new Tone.Part((time, chord) => {
  polySynth.triggerAttackRelease(chord, "2n", time)
}, [
  [0, ["C4", "E4", "G4"]],      // Beat 0: C major
  [1, ["F4", "A4", "C5"]],      // Beat 1: F major
  [2, ["G4", "B4", "D5"]],      // Beat 2: G major
  [3, ["C4", "E4", "G4"]],      // Beat 3: C major
])

part.start(0)
Tone.Transport.start()

Effects chain

import * as Tone from "tone"

await Tone.start()

// Chain effects:
const synth = new Tone.Synth()
const reverb = new Tone.Reverb({ decay: 2.5, wet: 0.4 })
const delay = new Tone.FeedbackDelay("8n", 0.3)
const compressor = new Tone.Compressor(-12, 3)

// Connect: synth → delay → reverb → compressor → output
synth.chain(delay, reverb, compressor, Tone.Destination)

synth.triggerAttackRelease("C4", "4n")

// Individual effect controls:
reverb.decay = 3           // Longer reverb tail
delay.feedback.value = 0.4 // More echo repeats
delay.delayTime.value = "16n"

Sequencer / beat machine

import * as Tone from "tone"

await Tone.start()

const kick = new Tone.MembraneSynth().toDestination()
const snare = new Tone.NoiseSynth({ envelope: { sustain: 0.05 } }).toDestination()
const hihat = new Tone.MetalSynth({ frequency: 400, envelope: { release: 0.1 } }).toDestination()

// 16-step sequencer:
const kickPattern =  [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]
const snarePattern = [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0]
const hihatPattern = [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0]

let step = 0

const loop = new Tone.Sequence((time) => {
  if (kickPattern[step]) kick.triggerAttackRelease("C1", "8n", time)
  if (snarePattern[step]) snare.triggerAttackRelease("8n", time)
  if (hihatPattern[step]) hihat.triggerAttackRelease("32n", time)

  step = (step + 1) % 16
}, null, "16n")

Tone.Transport.bpm.value = 120
loop.start(0)
Tone.Transport.start()

wavesurfer.js

wavesurfer.js — interactive audio waveform visualization:

Basic usage

import WaveSurfer from "wavesurfer.js"

// Create waveform in a container:
const wavesurfer = WaveSurfer.create({
  container: "#waveform",    // CSS selector or DOM element
  waveColor: "#FF8800",
  progressColor: "#CC5500",
  cursorColor: "#ffffff",
  barWidth: 3,
  barRadius: 3,
  height: 128,
  normalize: true,           // Normalize waveform to fill height
})

// Load audio:
await wavesurfer.load("/podcast-episode.mp3")

// Playback controls:
wavesurfer.play()
wavesurfer.pause()
wavesurfer.stop()
wavesurfer.playPause()

// Seek:
wavesurfer.seekTo(0.5)         // 50% through the track
wavesurfer.setTime(30)         // Jump to 30 seconds

// Volume:
wavesurfer.setVolume(0.8)
wavesurfer.setMuted(true)

// Events:
wavesurfer.on("ready", () => {
  console.log("Duration:", wavesurfer.getDuration())
})

wavesurfer.on("timeupdate", (currentTime) => {
  updateProgressBar(currentTime)
})

wavesurfer.on("finish", () => {
  playNextTrack()
})

React integration

import { useEffect, useRef } from "react"
import WaveSurfer from "wavesurfer.js"

interface AudioPlayerProps {
  src: string
  color?: string
}

export function AudioPlayer({ src, color = "#FF8800" }: AudioPlayerProps) {
  const containerRef = useRef<HTMLDivElement>(null)
  const wavesurferRef = useRef<WaveSurfer | null>(null)

  useEffect(() => {
    if (!containerRef.current) return

    wavesurferRef.current = WaveSurfer.create({
      container: containerRef.current,
      waveColor: color,
      progressColor: color + "88",
      height: 80,
      barWidth: 2,
      barRadius: 2,
    })

    wavesurferRef.current.load(src)

    return () => {
      wavesurferRef.current?.destroy()
    }
  }, [src])

  return (
    <div>
      <div ref={containerRef} />
      <button onClick={() => wavesurferRef.current?.playPause()}>
        Play/Pause
      </button>
    </div>
  )
}

Plugins

import WaveSurfer from "wavesurfer.js"
import RegionsPlugin from "wavesurfer.js/dist/plugins/regions"
import TimelinePlugin from "wavesurfer.js/dist/plugins/timeline"

// Timeline under waveform:
const timeline = TimelinePlugin.create({
  height: 20,
  timeInterval: 5,
  primaryLabelInterval: 10,
})

// Regions (highlight segments):
const regions = RegionsPlugin.create()

const wavesurfer = WaveSurfer.create({
  container: "#waveform",
  plugins: [timeline, regions],
})

await wavesurfer.load("/audio.mp3")

// Add a region:
regions.addRegion({
  start: 10,
  end: 30,
  content: "Interesting part",
  color: "rgba(255, 136, 0, 0.3)",
  drag: true,
  resize: true,
})

regions.on("region-clicked", (region, e) => {
  e.stopPropagation()
  region.play()
})

Feature Comparison

Featurehowler.jstone.jswavesurfer.js
Audio playback✅ Excellent
Sound effects⚠️ (overkill)
Synthesis✅ Excellent
Music scheduling
Waveform display✅ Excellent
3D spatial audio
Format fallback
Audio sprites
Effects chain
Recording✅ (plugin)
TypeScript
React support
Bundle size~30KB~400KB~100KB

When to Use Each

Choose howler.js if:

  • Game sound effects, UI audio feedback (clicks, notifications)
  • Background music that needs format fallbacks across browsers
  • 3D spatial audio for immersive experiences
  • Audio sprites for efficient multiple-sound loading

Choose tone.js if:

  • Building a music application, synthesizer, or digital instrument
  • Precise audio scheduling and beat-synced playback
  • Audio effects chains (reverb, delay, compression)
  • Generative music, MIDI-like sequencing

Choose wavesurfer.js if:

  • Podcast player with visual waveform
  • Audio editor with region selection
  • Music player showing song waveform
  • Interview clips, audio transcription tools

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on howler.js v2.x, tone.js v14.x, and wavesurfer.js v7.x.

Compare audio and multimedia packages on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.