Events
Subscribing
Listen to any event with ws.on(eventName, listener). The method returns an unsubscribe function — call it to stop listening without needing to keep a reference to the listener itself.
// Basic subscription
ws.on('ready', (duration) => {
console.log('Audio ready, duration:', duration)
})
// Store the unsubscribe function for cleanup
const unsubscribe = ws.on('timeupdate', (currentTime) => {
document.querySelector('#time').textContent = currentTime.toFixed(2)
})
// Later — stop listening
unsubscribe()
once
ws.once() fires the listener a single time and then automatically removes it. It also returns an unsubscribe function if you need to cancel it before it fires.
ws.once('ready', (duration) => {
// Runs exactly once, then removes itself
enablePlayButton()
})
un
ws.un(eventName, listener) removes a specific listener by reference. Using the unsubscribe function returned by on is usually more convenient, but un is available when you already hold the listener reference.
function onTimeUpdate(currentTime) { /* ... */ }
ws.on('timeupdate', onTimeUpdate)
// Remove later
ws.un('timeupdate', onTimeUpdate)
unAll
ws.unAll() removes all listeners for all events at once. Use with care — it also removes internal listeners added by plugins.
The events
All event names and payloads are taken directly from WaveSurferEvents in wavesurfer.ts.
| Event | Payload | When it fires |
|---|---|---|
init |
— | After the WaveSurfer instance is created |
load |
url: string |
When audio starts loading |
loading |
percent: number |
Repeatedly during network fetch (0–100) |
decode |
duration: number |
When the audio has been decoded |
ready |
duration: number |
When audio is decoded and can play |
play |
— | When playback starts |
pause |
— | When playback pauses |
finish |
— | When audio plays through to the end |
timeupdate |
currentTime: number |
On every position change (playback and seeks) |
audioprocess |
currentTime: number |
Like timeupdate, but only during active playback |
seeking |
currentTime: number |
When the browser seeks to a new position |
interaction |
newTime: number |
When the user clicks or drags on the waveform |
click |
relativeX: number, relativeY: number |
When the user clicks the waveform |
dblclick |
relativeX: number, relativeY: number |
When the user double-clicks the waveform |
drag |
relativeX: number |
While the user drags the cursor |
dragstart |
relativeX: number |
When the user starts dragging the cursor |
dragend |
relativeX: number |
When the user finishes dragging the cursor |
scroll |
visibleStartTime: number, visibleEndTime: number, scrollLeft: number, scrollRight: number |
When the waveform is scrolled (panned) |
zoom |
minPxPerSec: number |
When the zoom level changes |
redraw |
— | When the visible waveform is drawn |
redrawcomplete |
— | When all audio channel chunks have finished drawing |
error |
error: Error |
When the file cannot be fetched, decoded, or the media element throws |
resize |
— | When the audio container is resized |
destroy |
— | Just before the instance is destroyed |
relativeX and relativeY are values between 0 and 1 representing position within the waveform. newTime in interaction is already converted to seconds.
Common patterns
Update a time display
timeupdate fires during both playback and programmatic seeks. audioprocess is an alias that fires only while audio is actually playing — useful if you want to avoid redundant updates when the user just clicks to seek.
const timeEl = document.querySelector('#current-time')
ws.on('timeupdate', (currentTime) => {
timeEl.textContent = formatTime(currentTime)
})
function formatTime(seconds) {
const m = Math.floor(seconds / 60)
const s = Math.floor(seconds % 60).toString().padStart(2, '0')
return `${m}:${s}`
}
Enable UI elements once audio is ready
ready is the right place to unlock controls that depend on a valid duration.
const playBtn = document.querySelector('#play')
playBtn.disabled = true
ws.on('ready', (duration) => {
playBtn.disabled = false
document.querySelector('#duration').textContent = formatTime(duration)
})
Detect user interaction vs. programmatic seeks
interaction fires only when the user physically clicks or drags the waveform. seeking fires for both user gestures and calls to ws.setTime() / ws.seekTo(). Use interaction when you need to distinguish deliberate user input.
ws.on('interaction', (newTime) => {
console.log('User jumped to', newTime.toFixed(2), 's')
// e.g. log analytics, reset a loop region, etc.
})
Clean up on destroy
The destroy event fires before the instance tears itself down. It is a good place to release any resources your code holds.
const unsubscribeTime = ws.on('timeupdate', updateDisplay)
ws.on('destroy', () => {
unsubscribeTime()
})
finish may not fire reliably on some audio files. If the file’s reported duration is slightly longer than the actual audio content (a common encoding artifact), the media element never fires its ended event and finish is never emitted. Similarly, audio that ends on near-silence can confuse some browser decoders. As a safeguard, compare currentTime against getDuration() inside a timeupdate handler if you need a guaranteed end-of-audio signal.
To ensure your on calls are registered before wavesurfer auto-loads the URL, subscribe to events immediately after WaveSurfer.create(). The instance delays its internal init and load cycle by one microtask (Promise.resolve().then(…)), so synchronous subscriptions in the same call stack are always set up in time.