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.