github katspaugh/wavesurfer.js 8.0.0

5 hours ago

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 emit error, registerPlugin() throws, and the other mutating methods (play, setTime, setVolume, setOptions, …) do nothing. Calling destroy() twice is safe, and it now also frees the decoded AudioBuffer. To migrate: create a new instance instead of calling load() on a destroyed one.
  • A superseded load now rejects. When a newer load()/loadBlob() replaces an earlier one, the earlier promise rejects with an AbortError instead of quietly resolving. No error event fires for it, and un-awaited calls don't produce unhandled-rejection warnings. To migrate: if you await a load that might be replaced, catch AbortError.
  • getMediaElement() returns HTMLMediaElement | null. It returns null with backend: 'WebAudio'. v7 returned the internal WebAudioPlayer, typed incorrectly as an audio element. If you need the WebAudio player (for example, for getGainNode()), create a WebAudioPlayer yourself and pass it in the media option (see examples/phase-vocoder.js). wavesurfer instanceof Player is also no longer true. The playback methods themselves are unchanged.
  • The error event payload is always an Error. A media element's MediaError is wrapped, and its code is kept.
  • getRenderer() is no longer an event emitter. It has no .on()/.emit(). Use the public WaveSurfer events, or the renderer's signals (clickSignal, dblclickSignal, dragEventsSignal, getVisibleRange(), getScrollSignals()). getWrapper() is still the supported way to reach the wrapper element.
  • Spectrogram noverlap now does what the docs say. The only clamp is fftSamples - 1. The hidden 50% cap and the 64-sample minimum hop are gone. A large noverlap now gives a finer hop, which means more columns and more computation.

TypeScript and packaging

  • WaveSurfer no longer has protected subscriptions, mediaSubscriptions or abortController. Subclasses should use this.scope.add(disposer) instead.
  • The public region.subscriptions field on regions is removed.
  • The package.json#exports wildcard "./dist/*" is now "./dist/*.js". Deep imports need the .js extension (wavesurfer.js/dist/webaudio.js, not …/dist/webaudio).
  • Internal modules are no longer built into dist/: draggable.js, timer.js and reactive/*.
  • dist/fft.js now exports only FFT. The spectrogram math and colormap helpers moved to dist/spectrogram-render-utils.js.
  • dist/types.d.ts is no longer built. Nothing referenced it.

🗑️ Deprecated

  • WindowedSpectrogramPlugin / spectrogram-windowed.js → use SpectrogramPlugin.create({ rendering: 'windowed' }). The old entry point still works.
  • Class-based plugins that extend BasePlugin → use WaveSurfer.definePlugin(). Existing class plugins keep working throughout v8, and removal is planned for v9. BasePlugin.destroy() now disposes this.scope.

✨ New

  • WaveSurfer.definePlugin(name, (ctx, options) => api) is a functional plugin API. You register resources on ctx.scope, and it tears them all down in one step, so plugins don't need a hand-written destroy(). Hover, Zoom, Timeline, Minimap, Envelope and Regions now use it, and their public API is unchanged. yarn make-plugin generates a definePlugin skeleton.
    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({}))
  • Scope is 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() adds loadPhase ('idle' | 'fetching' | 'decoding' | 'ready' | 'error'), scrollPosition and muted. 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 real progress values that reach 1.
  • Regions:
    • The waveform auto-scrolls while you drag, resize or draw a region near the container edge (#4358). The update event gets a new autoScrollDirection argument.
    • region-double-clicked now works on touch devices.
  • Record:
    • A new record-ended-externally event fires when the input device disconnects during recording, and the final blob is still delivered through record-end.
    • mediaRecorderTimeslice defaults to 200 ms, so record-data-available fires continuously (#4349).
    • Calling startRecording() during a recording restarts cleanly.

🐛 Notable fixes

Core and playback

  • play, pause, seeking, finish, timeupdate and other events fire once instead of twice.
  • WebAudio backend:
    • Audio stops on destroy(), and the AudioContext is closed.
    • seeked fires, so isSeeking no longer stays true.
    • play() resumes a suspended context.
    • Load failures emit error.
    • timeupdate reports the correct time after a pause (#4348).
  • play(start, end) and region.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 canplay instead of being dropped (#4353).
  • The object form of dragToSeek ({ debounceTime }) now works and can be toggled with setOptions().

Rendering

  • normalize: true scales 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 NaN seeks.

Regions

  • region-out no 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/maxLength are 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-end events no longer leak from one recording session into the next.

Other plugins

  • Timeline: the duration option 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: 1 no longer divides by zero.
    • The zoom baseline resets when new audio loads.
  • Hover: a string lineWidth no longer breaks positioning.

Spectrogram

  • Loading a new file at the same zoom level no longer keeps showing the old file.
  • frequenciesDataUrl no longer renders blank.
  • Redraws are queued instead of dropped.
  • The lanczoz window and the Bark scale are fixed.
  • Rendering is sharp on HiDPI screens.
  • Setting maxCanvasWidth on 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/finish fire 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 v7 branch 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


npm

Don't miss a new wavesurfer.js release

NewReleases is sending notifications on new releases.