wavesurfer.js v8 rebuilds the core and plugins on a single ownership model for teardown, so each listener, timer, observer and child object is released by the owner that created it. This removes a large set of memory leaks and events that fired twice, and it fixes dozens of long-standing bugs in playback, rendering, regions, recording and the spectrogram.
Most apps upgrade without code changes. The public WaveSurfer API, events and plugin constructors stay the same. The breaking changes below affect edge cases, TypeScript subclasses and deep imports from dist/.
npm install wavesurfer.js@8⚠️ Breaking changes (migrating from v7)
Runtime behavior
destroy()is final. You can't reuse a destroyed instance.load()/loadBlob()reject and emiterror,registerPlugin()throws, and the other mutating methods (play,setTime,setVolume,setOptions, …) do nothing. Callingdestroy()twice is safe, and it now also frees the decodedAudioBuffer. To migrate: create a new instance instead of callingload()on a destroyed one.- A superseded load now rejects. When a newer
load()/loadBlob()replaces an earlier one, the earlier promise rejects with anAbortErrorinstead of quietly resolving. Noerrorevent fires for it, and un-awaited calls don't produce unhandled-rejection warnings. To migrate: if youawaita load that might be replaced, catchAbortError. getMediaElement()returnsHTMLMediaElement | null. It returnsnullwithbackend: 'WebAudio'. v7 returned the internalWebAudioPlayer, typed incorrectly as an audio element. If you need the WebAudio player (for example, forgetGainNode()), create aWebAudioPlayeryourself and pass it in themediaoption (seeexamples/phase-vocoder.js).wavesurfer instanceof Playeris also no longertrue. The playback methods themselves are unchanged.- The
errorevent payload is always anError. A media element'sMediaErroris wrapped, and itscodeis kept. getRenderer()is no longer an event emitter. It has no.on()/.emit(). Use the publicWaveSurferevents, or the renderer's signals (clickSignal,dblclickSignal,dragEventsSignal,getVisibleRange(),getScrollSignals()).getWrapper()is still the supported way to reach the wrapper element.- Spectrogram
noverlapnow does what the docs say. The only clamp isfftSamples - 1. The hidden 50% cap and the 64-sample minimum hop are gone. A largenoverlapnow gives a finer hop, which means more columns and more computation.
TypeScript and packaging
WaveSurferno longer hasprotected subscriptions,mediaSubscriptionsorabortController. Subclasses should usethis.scope.add(disposer)instead.- The public
region.subscriptionsfield on regions is removed. - The
package.json#exportswildcard"./dist/*"is now"./dist/*.js". Deep imports need the.jsextension (wavesurfer.js/dist/webaudio.js, not…/dist/webaudio). - Internal modules are no longer built into
dist/:draggable.js,timer.jsandreactive/*. dist/fft.jsnow exports onlyFFT. The spectrogram math and colormap helpers moved todist/spectrogram-render-utils.js.dist/types.d.tsis no longer built. Nothing referenced it.
🗑️ Deprecated
WindowedSpectrogramPlugin/spectrogram-windowed.js→ useSpectrogramPlugin.create({ rendering: 'windowed' }). The old entry point still works.- Class-based plugins that extend
BasePlugin→ useWaveSurfer.definePlugin(). Existing class plugins keep working throughout v8, and removal is planned for v9.BasePlugin.destroy()now disposesthis.scope.
✨ New
WaveSurfer.definePlugin(name, (ctx, options) => api)is a functional plugin API. You register resources onctx.scope, and it tears them all down in one step, so plugins don't need a hand-writtendestroy(). Hover, Zoom, Timeline, Minimap, Envelope and Regions now use it, and their public API is unchanged.yarn make-plugingenerates adefinePluginskeleton.const MyPlugin = WaveSurfer.definePlugin('MyPlugin', (ctx, options) => { ctx.scope.add(ctx.wavesurfer.on('timeupdate', (t) => ctx.emit('tick', t))) return { hello: () => 'world' } }) wavesurfer.registerPlugin(MyPlugin.create({}))
Scopeis a disposal tree:add,listen,timeout,interval,raf,createResizeObserver,abortSignal,child,dispose. The whole codebase uses it, and plugin authors can use it too.- More reactive state.
getState()addsloadPhase('idle' | 'fetching' | 'decoding' | 'ready' | 'error'),scrollPositionandmuted.getRenderer().getVisibleRange()returns the visible{ startTime, endTime }and stays current through scrolling and zooming. - Spectrogram: a
rendering: 'full' | 'windowed'option combines both modes into one plugin. Windowed mode uses a byte-based segment cache (256 MB by default) and reports realprogressvalues that reach 1. - Regions:
- The waveform auto-scrolls while you drag, resize or draw a region near the container edge (#4358). The
updateevent gets a newautoScrollDirectionargument. region-double-clickednow works on touch devices.
- The waveform auto-scrolls while you drag, resize or draw a region near the container edge (#4358). The
- Record:
- A new
record-ended-externallyevent fires when the input device disconnects during recording, and the final blob is still delivered throughrecord-end. mediaRecorderTimeslicedefaults to 200 ms, sorecord-data-availablefires continuously (#4349).- Calling
startRecording()during a recording restarts cleanly.
- A new
🐛 Notable fixes
Core and playback
play,pause,seeking,finish,timeupdateand other events fire once instead of twice.- WebAudio backend:
- Audio stops on
destroy(), and theAudioContextis closed. seekedfires, soisSeekingno longer staystrue.play()resumes a suspended context.- Load failures emit
error. timeupdatereports the correct time after a pause (#4348).
- Audio stops on
play(start, end)andregion.play():stopAt()no longer jumps the playhead to the end of the range when you pause or seek (#4366).- The stop time is enforced in background tabs.
- Seeks made before metadata loads are applied at
canplayinstead of being dropped (#4353). - The object form of
dragToSeek({ debounceTime }) now works and can be toggled withsetOptions().
Rendering
normalize: truescales by the peak of the whole file, not the peak of each canvas. This removes jumps in amplitude where canvases meet. The decoder no longer changes peak arrays you pass in.- Blank canvases no longer appear after zooming a waveform that started out non-scrollable. Undrawn strips at the viewport edges are fixed.
- Bars are no longer clipped or unevenly spaced where canvases meet.
- Repeated wheel zoom no longer makes the cursor drift.
setOptions({ width: undefined })goes back to filling the container.- A hidden container no longer causes
NaNseeks.
Regions
region-outno longer fires by mistake when playback starts at a region's start. This fixes in/out ping-pong between neighboring regions (#3631, #3658, #3781, #3866).minLength/maxLengthare enforced while drawing a region.- Regions compress against the container edge instead of sliding past it.
- The region event race is fixed (#4359).
Record
- The mic is released if the plugin is destroyed while the permission prompt is open.
stopMic()restores the wavesurfer options that the live preview changes.- Paused time is no longer counted in the duration (#4352).
- Streams and
record-endevents no longer leak from one recording session into the next.
Other plugins
- Timeline: the
durationoption works before audio loads, and label culling works. - Envelope: a point at the very end of the track no longer causes
NaN(#4350), and the envelope renders right away when registered late. - Zoom:
- The page can scroll before audio is decoded.
iterations: 1no longer divides by zero.- The zoom baseline resets when new audio loads.
- Hover: a string
lineWidthno longer breaks positioning.
Spectrogram
- Loading a new file at the same zoom level no longer keeps showing the old file.
frequenciesDataUrlno longer renders blank.- Redraws are queued instead of dropped.
- The
lanczozwindow and the Bark scale are fixed. - Rendering is sharp on HiDPI screens.
- Setting
maxCanvasWidthon one instance no longer affects the others.
Memory
- Leaks at destroy time and after async work are fixed across the core and all plugins. A GC-based leak test suite (
yarn test:leaks) now runs in CI to catch regressions.
🔧 Tooling
- ESLint and typecheck now block CI. Coverage thresholds are raised.
- A Cypress test asserts that
play/pause/finishfire once per transition in a real browser (#4361). - All examples were updated for the v8 API (#4357).
- 7.x maintenance releases (such as 7.12.12) are published from the
v7branch under their own npm dist-tag.
Pre-releases in this cycle
8.0.0-beta.1 / beta.2 (#4340, #4341, #4342, #4344, #4346) · beta.3 · beta.4 (#4354, #4355) · beta.5 · 8.0.0 (#4361, #4366)
Thanks to @aribradshaw and @Jaybhade for contributions during this cycle.
The complete change list is in CHANGELOG.md.
Full diff: 7.12.11...8.0.0