No decode pass
Skips decodeAudioData() entirely — the most expensive step. Saves roughly 1–5s per track on slow connections.
By default, the player decodes the audio file with the Web Audio API to extract the waveform — it downloads the file, runs decodeAudioData(), and walks the samples to build peaks. That is accurate but expensive: it costs a full download plus a decode pass (roughly 1–5s per track on a slow connection) before anything draws.
Pre-computed peaks sidestep all of it. You generate the peak data once (at build time, on upload, or in a worker), store it as a small JSON file next to the audio, and hand it to the player. The player skips the decode entirely and draws the waveform the instant the JSON arrives.
This is the single biggest performance lever in the library, and the recommended default for any catalogue of more than a handful of tracks.
waveform optionPoint the player at peak data through the waveform option (or the data-waveform attribute). It accepts four shapes:
| Shape | Example | When to use |
|---|---|---|
.json URL |
'/audio/track.json' |
The standard path — fetched, peaks + markers applied, decode skipped. |
Inline number[] |
[0.2, 0.37, 0.41, 0.55] |
Peaks already in memory (e.g. embedded in a JS payload). |
| JSON-array string | '[0.2,0.37,0.41,0.55]' |
Peaks serialized into a single attribute value. |
| Comma-separated string | '0.2,0.37,0.41,0.55' |
Compact inline form, no brackets. |
In every case the Web Audio decode pass is skipped — the player normalizes whatever you give it straight onto the canvas.
npm install @arraypress/waveform-player @arraypress/waveform-gen pnpm add @arraypress/waveform-player @arraypress/waveform-gen yarn add @arraypress/waveform-player @arraypress/waveform-gen bun add @arraypress/waveform-player @arraypress/waveform-genimport WaveformPlayer from '@arraypress/waveform-player';import '@arraypress/waveform-player/dist/waveform-player.css';
const player = new WaveformPlayer('#player', { url: '/audio/track.mp3', waveform: '/audio/track.json', // .json URL → fetched, no decode});data-waveform)<div data-waveform-player data-url="/audio/track.mp3" data-waveform="/audio/track.json"></div>The auto-initialiser scans for [data-waveform-player] on DOMContentLoaded, so the declarative form needs no JS at all. See Data attributes for the full attribute contract.
The fetched file is a single object. Only peaks is required:
{ "peaks": [0.2, 0.37, 0.41, 0.55, 0.48, 0.31], "markers": [ { "time": 0, "label": "Intro" }, { "time": 30, "label": "Chorus" } ]}peaks — number[], each value a normalized amplitude in 0..1. This is the waveform.markers — optional Array<{ time: number, label: string, color?: string }>. Embedded markers are applied automatically only if you have not already set markers on the player — an explicit markers option/attribute wins. See Markers.A bare top-level array is also accepted ([0.2, 0.37, ...]) and treated as the peaks list, but the object form is preferred because it carries markers.
@arraypress/waveform-gen is the companion CLI that produces these JSON files. Run it once over your audio, commit or deploy the output next to the source files, and you are done.
Generate a JSON file per track
npx @arraypress/waveform-gen ./audio/*.mp3 --output ./public/audio/This writes track.json beside each track.mp3.
Scan a directory (optionally recursive)
npx @arraypress/waveform-gen ./audio/ --recursive --output ./public/audio/Point the player at the output — via the waveform option or data-waveform, as shown above.
| Option | Default | Description |
|---|---|---|
--samples <n> |
1800 |
Number of peaks written to the file. |
--precision <n> |
2 |
Decimal places each peak is rounded to. |
--output <dir> |
Same as input | Directory for the generated JSON. |
--format <type> |
json |
json (file) or inline (peaks to stdout). |
--bpm |
off | Detect tempo and write a bpm field. |
--recursive |
off | Scan sub-directories. |
--quiet |
off | Suppress progress output. |
Drop a *.markers.txt file next to the audio and WaveformGen folds the markers into the JSON automatically — no flag needed:
0:00 Intro0:30 Verse 11:15 Chorus1:02:30 BridgeTimestamps accept SS, MM:SS, and H:MM:SS. Lines starting with # are ignored. The resulting markers array lands in the JSON and is applied by the player on fetch (unless you set markers explicitly).
--format inline prints the peaks array to stdout instead of writing a file — handy for embedding peaks directly in a server template or a JS payload, then passing them as an inline array / string waveform value:
npx @arraypress/waveform-gen song.mp3 --format inlinegetPeaksUrl helperIf your peaks JSON lives alongside the audio with a matching name (track.mp3 → track.json), you do not need to track two URLs. WaveformPlayer.getPeaksUrl() derives the JSON URL from the audio URL by swapping the extension:
WaveformPlayer.getPeaksUrl('/audio/track.mp3'); // '/audio/track.json'WaveformPlayer.getPeaksUrl('/audio/track.wav?v=2'); // '/audio/track.json?v=2'WaveformPlayer.getPeaksUrl('/stream/track'); // undefined (no recognised extension)WaveformPlayer.getPeaksUrl(undefined); // undefinedIt recognises mp3, wav, ogg, flac, m4a, and aac, and preserves any ?query and #hash. When the input has no recognised audio extension it returns undefined — which is exactly the right value to pass straight through:
const player = new WaveformPlayer('#player', { url: track.audioUrl, waveform: WaveformPlayer.getPeaksUrl(track.audioUrl),});Because undefined is treated as “no pre-computed peaks”, a track whose URL doesn’t match the pattern (or has no sidecar JSON) transparently falls back to live decoding. You can wire this up unconditionally across a whole catalogue without branching.
Two methods bring peaks in after construction:
setWaveformData(data)Normalize and redraw with new peaks on a live player. Accepts the same four shapes as the waveform option — array, JSON/CSV string, or .json URL (fetched async). Malformed input collapses to an empty waveform rather than throwing.
player.setWaveformData('/audio/other.json'); // fetch + redrawplayer.setWaveformData([0.1, 0.4, 0.6, 0.3]); // inline + redrawWhen the argument is a .json URL, embedded markers are applied (again, only if you haven’t already set markers).
loadTrack(...)When swapping tracks, pass waveform in the per-track options so the new track gets its own peaks:
player.loadTrack('/audio/next.mp3', 'Next Track', null, { waveform: WaveformPlayer.getPeaksUrl('/audio/next.mp3'),});Need peaks but no player instance (e.g. to cache them yourself)? WaveformPlayer.generateWaveformData() decodes a URL and resolves to the peaks array:
const peaks = await WaveformPlayer.generateWaveformData('/audio/track.mp3', 256);// peaks: number[]This still performs a Web Audio decode — it is the runtime counterpart to the build-time WaveformGen CLI. For production catalogues, prefer generating ahead of time.
No decode pass
Skips decodeAudioData() entirely — the most expensive step. Saves roughly 1–5s per track on slow connections.
Tiny payloads
A peaks JSON is a few KB. You fetch numbers, not megabytes of audio, to draw the waveform.
Render before audio
The waveform paints from JSON while the audio is still preload="metadata" — the UI is ready before playback is.
Scales to catalogues
Lists of dozens of players each skipping a decode is the difference between instant and janky.
samples and preload.data-waveform contract.markers option.