Skip to content

External / visualization-only mode

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:

  • One shared player, many inline waveforms. A playlist page where every row shows a waveform, but only a single <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.
  • You already have an audio pipeline. Web Audio graphs, HLS/DASH players, a <video> element, <media-controller>, Howler, etc. The waveform becomes a pure view over a clock you already own.
  • Multiple synchronized views of the same track (e.g. a compact bar plus a large hero waveform) that must stay in lockstep.

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.

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)

Section titled “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). For request-play / request-pause a veto only affects the singlePlay bookkeeping — a vetoed play doesn’t claim WaveformPlayer.currentlyPlaying, a vetoed pause keeps it (1.28.0+). Neither event changes the play/pause visual either way; only your setPlayingState() call does. destroy() never emits request-pause (1.28.0+).

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:

  1. Clicking a row’s button fires request-play. We point the shared <audio> at that row’s URL and start it.

  2. The element’s play event flows back through setPlayingState(true), which flips the button and emits waveformplayer:play.

  3. 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.

  4. 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