Waveform data & peaks
Supply pre-computed peaks so external players never decode audio. Read more
By default a WaveformPlayer owns its own <audio> element — it loads the file, plays it, and paints progress from the element’s clock. That is audioMode: 'self', and it’s all you need for a standalone player.
External mode (audioMode: 'external') splits those responsibilities. The player still renders the waveform, handles clicks and keyboard seeking, and shows the play/pause/time UI — but it owns no audio. You feed it state through setPlayingState() and setProgress(), and it tells you when the user wants something by emitting cancelable waveformplayer:request-* events. One audio source can drive any number of waveforms.
Reach for external mode whenever the audio and the visualization are not 1:1:
<audio> is ever playing. This is exactly how waveform-bar works — a persistent bottom bar embeds one audioMode: 'self' player and drives the inline external-mode players on the page.<video> element, <media-controller>, Howler, etc. The waveform becomes a pure view over a clock you already own.If you just want a single self-contained player, stay in the default 'self' mode — see Options.
In 'external' mode this.audio stays null, so every audio-bound method becomes a no-op and control flows through events instead:
| Concern | 'self' (default) |
'external' |
|---|---|---|
Owns an <audio> |
Yes | No (this.audio === null) |
play() / pause() |
Calls audio.play() / audio.pause() |
Dispatches cancelable waveformplayer:request-play / request-pause |
| Canvas click / keyboard seek | Seeks the owned audio | Dispatches cancelable waveformplayer:request-seek (detail.percent) |
| Progress / time display | Driven by the audio’s timeupdate |
You call setProgress(currentTime, duration) |
| Play/pause visual state | Driven by audio events | You call setPlayingState(playing) |
seekTo / seekToPercent / setVolume / setPlaybackRate |
Act on the audio | No-op |
| Media Session, speed menu | Active | No-op |
waveformplayer:ended |
From the audio ended event |
Synthesized once when progress >= 1 |
The waveform itself still needs peaks. External-mode players never decode audio for you, so supply pre-computed peaks via the waveform option (inline array, CSV/JSON string, or a .json URL) — or let the controller redraw them per track with setWaveformData() / loadTrack().
External mode is driven by exactly two methods. Call them from your audio source’s events.
setPlayingState(playing: boolean)Flips the play/pause visual state without touching audio. Idempotent — safe to call on every tick. It only emits waveformplayer:play / waveformplayer:pause (and runs onPlay / onPause) on an actual transition, and starts/stops the smooth-update loop accordingly.
audio.addEventListener('play', () => player.setPlayingState(true));audio.addEventListener('pause', () => player.setPlayingState(false));setProgress(currentTime: number, duration: number)Updates the progress overlay and the current/total time displays from your external clock, stores duration (so the accessible slider and keyboard seeking know the track length), redraws the canvas, emits waveformplayer:timeupdate, and synthesizes a one-shot waveformplayer:ended when progress reaches 1. It is a no-op when duration <= 0, so it’s safe to call before metadata is known.
audio.addEventListener('timeupdate', () => { player.setProgress(audio.currentTime, audio.duration);});When the user interacts with an external-mode player, it doesn’t act — it asks. All three request events are cancelable and fire only in external mode. Their detail is the track-detail object (shape below); request-seek adds a percent.
| Event | Trigger | detail |
If not vetoed |
|---|---|---|---|
waveformplayer:request-play |
play() / togglePlay() while paused |
track detail | (nothing local — you start your audio) |
waveformplayer:request-pause |
pause() / togglePlay() while playing |
track detail | (nothing local — you pause your audio) |
waveformplayer:request-seek |
canvas click + keyboard slider | track detail + percent (0..1) |
local overlay advances optimistically |
Call event.preventDefault() to veto the action (e.g. the track isn’t loaded yet, or you want to route it differently).
detail mirrors the input shape WaveformBar.play() accepts, so a controller can forward it verbatim:
player.container.addEventListener('waveformplayer:request-play', (e) => { // e.detail = { url, title, artist, artwork, markers, waveform, id, player } WaveformBar.play(e.detail); // forward straight through});| Field | Source |
|---|---|
url |
options.url |
title |
options.title |
artist |
options.artist |
artwork |
options.artwork |
markers |
options.markers |
waveform |
options.waveform (the peaks you supplied) |
id |
the player’s instance id |
player |
the WaveformPlayer instance itself |
One shared <audio> element driving several inline external-mode waveforms — the pattern waveform-bar automates. Each player is given pre-computed peaks via getPeaksUrl() so nothing decodes audio.
<audio id="shared-audio"></audio>
<div class="row" data-url="/audio/one.mp3" data-title="One"></div><div class="row" data-url="/audio/two.mp3" data-title="Two"></div><div class="row" data-url="/audio/three.mp3" data-title="Three"></div>import WaveformPlayer from '@arraypress/waveform-player';import '@arraypress/waveform-player/dist/waveform-player.css';
const audio = document.getElementById('shared-audio');let active = null; // the player whose track is currently loaded into `audio`
// 1. Build one external-mode player per row.const players = [...document.querySelectorAll('.row')].map((el) => { const url = el.dataset.url; return new WaveformPlayer(el, { audioMode: 'external', url, title: el.dataset.title, waveform: WaveformPlayer.getPeaksUrl(url), // /audio/one.mp3 -> /audio/one.json waveformStyle: 'mirror', });});
// 2. Helper: make `audio` point at a given player's track.function activate(player) { if (active === player) return; if (active) active.setPlayingState(false); // clear the previous waveform's button active = player; audio.src = player.options.url;}
// 3. The user clicked play on a row -> point shared audio at it and play.players.forEach((player) => { player.container.addEventListener('waveformplayer:request-play', (e) => { activate(player); audio.play(); });
player.container.addEventListener('waveformplayer:request-pause', () => { audio.pause(); });
// Seek: detail.percent is 0..1. The local overlay already moved // optimistically; we reconcile by moving the real clock. player.container.addEventListener('waveformplayer:request-seek', (e) => { if (active !== player) activate(player); if (audio.duration) audio.currentTime = e.detail.percent * audio.duration; });});
// 4. Pump shared-audio state back into whichever player is active.audio.addEventListener('play', () => active?.setPlayingState(true));audio.addEventListener('pause', () => active?.setPlayingState(false));audio.addEventListener('timeupdate', () => { active?.setProgress(audio.currentTime, audio.duration);});audio.addEventListener('ended', () => active?.setPlayingState(false));What’s happening:
Clicking a row’s button fires request-play. We point the shared <audio> at that row’s URL and start it.
The element’s play event flows back through setPlayingState(true), which flips the button and emits waveformplayer:play.
Each timeupdate calls setProgress(), redrawing that one waveform and updating its time display. When the clock reaches the end, setProgress() synthesizes waveformplayer:ended for you.
Clicking elsewhere on the canvas (or arrow-key seeking) fires request-seek with a percent; we move the real clock and the overlay reconciles on the next timeupdate.
If instead of many static waveforms you have one external-mode player whose track changes (the bottom-bar case), use loadTrack() to swap URL, title, artist, artwork, markers, and peaks at runtime:
await bar.loadTrack('/audio/next.mp3', 'Next Track', 'Some Artist', { waveform: WaveformPlayer.getPeaksUrl('/audio/next.mp3'), autoplay: false, // loadTrack auto-plays unless this is explicitly false});The accessible seek slider works the same as self mode (accessibleSeek defaults to true). Because keyboard seeking reads the duration you publish through setProgress(), arrow / Page / Home / End keys emit waveformplayer:request-seek with the right percent once a positive duration has been pushed at least once. Until then there’s no known duration and seeking stays inert. See Accessibility.
Waveform data & peaks
Supply pre-computed peaks so external players never decode audio. Read more
WaveformBar
The singleton that automates this whole pattern. Read more
Events
Every event and callback, including the request-* family. Read more
Loading tracks at runtime
loadTrack() and the per-track reset rules. Read more