howler.js vs tone.js vs wavesurfer.js: Web Audio in JavaScript (2026)
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
| Feature | howler.js | tone.js | wavesurfer.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.