Skip to content

Pre-computed peaks

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.

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

Terminal window
npm install @arraypress/waveform-player @arraypress/waveform-gen
import 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
});
<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" }
]
}
  • peaksnumber[], 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.

  1. Generate a JSON file per track

    Terminal window
    npx @arraypress/waveform-gen ./audio/*.mp3 --output ./public/audio/

    This writes track.json beside each track.mp3.

  2. Scan a directory (optionally recursive)

    Terminal window
    npx @arraypress/waveform-gen ./audio/ --recursive --output ./public/audio/
  3. 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:

song.markers.txt
0:00 Intro
0:30 Verse 1
1:15 Chorus
1:02:30 Bridge

Timestamps 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:

Terminal window
npx @arraypress/waveform-gen song.mp3 --format inline

If your peaks JSON lives alongside the audio with a matching name (track.mp3track.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); // undefined

It 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:

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 + redraw
player.setWaveformData([0.1, 0.4, 0.6, 0.3]); // inline + redraw

When the argument is a .json URL, embedded markers are applied (again, only if you haven’t already set markers).

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.

  • Options — the full option surface, including samples and preload.
  • Data attributes — the declarative data-waveform contract.
  • Markers — how embedded JSON markers interact with the markers option.
  • BPM — surfacing tempo alongside pre-computed peaks.