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

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