@kirigami/audiowaveform-wasm
v1.2.0
Published
Waveform peak extraction and ID3 tag/cover-art reading from BBC's audiowaveform, compiled to WebAssembly for Node.js.
Maintainers
Readme
@kirigami/audiowaveform-wasm
Waveform peak extraction and ID3 tag/cover-art reading for Node.js, compiled from BBC's audiowaveform to WebAssembly.
Built for the Kirigami static site generator's MP3 player plugin.
Overview
@kirigami/audiowaveform-wasm ships a pre-compiled WebAssembly build of the peak-generation core of BBC's audiowaveform — not its CLI, not its PNG renderer — plus ID3 tag and cover-art reading via libid3tag:
- ✅ Node.js only, no browser target
- ✅ One monolithic wasm module — no JSPI, no Asyncify (peak extraction is synchronous, CPU-bound work)
- ✅ Buffer in, plain JS object out — peaks are shaped like
audiowaveform's own documented JSON format, sowaveform-data.jscan consume them directly - ✅ MP3, fully verified: both CBR and VBR, every common bitrate/quality level, mono and stereo, MPEG1 and MPEG2 sample rates, with and without a Xing/LAME VBR header
- ✅ WAV (16/24-bit PCM, 32-bit float), AIFF, FLAC, Ogg Vorbis, Opus, M4A/AAC, and WebM (Vorbis/Opus audio) — see Format support
Built by audiowaveform-wasm-compiler, which also documents the full build pipeline and architecture decisions.
Part of the Kirigami project ecosystem.
Table of contents
- @kirigami/audiowaveform-wasm
- Overview
- Requirements
- Installation
- Usage
- API and runtime files
- What's new in 1.2.0
- Format support
- ID3 tag support
- License
- Author
Requirements
- Node.js
>= 24.0.0
Installation
npm install @kirigami/audiowaveform-wasmUsage
import { extractAudioPeaks, getId3Tags, getId3CoverArt } from '@kirigami/audiowaveform-wasm';
import fs from 'node:fs';
const mp3Bytes = fs.readFileSync('song.mp3');
const peaks = await extractAudioPeaks(mp3Bytes, 512);
// { version: 2, channels: 1, sample_rate: 44100, samples_per_pixel: 512,
// bits: 16, length: 1234, data: [...] }
// Note: `channels` is always 1 — stereo (or any multichannel) input is
// downmixed into one merged waveform, matching audiowaveform's own CLI
// default. It does not reflect the source file's real channel count.
const tags = await getId3Tags(mp3Bytes);
// { title, artist, album, albumArtist, year, track, genre } — each a
// string or null; absent metadata can produce an object of all-null fields
const cover = await getId3CoverArt(mp3Bytes);
// { mimeType, pictureType, data } — data is a plain array of byte values
// (0-255); null if there's no embedded cover art
if (cover) {
const extension = cover.mimeType === 'image/png' ? 'png'
: cover.mimeType === 'image/jpeg' ? 'jpg' : 'bin';
fs.writeFileSync(`cover.${extension}`, Buffer.from(cover.data));
}extractAudioPeaks() auto-detects the format from the buffer's own container magic bytes — no need to know it up front.
API and runtime files
Only these three named functions and ./package.json are exported; TypeScript
interfaces are supplied by index.d.ts.
| Function | Input | Result |
|---|---|---|
| extractAudioPeaks(bytes, samplesPerPixel = 512) | Uint8Array or Node Buffer; use a positive integer resolution | Promise<WaveformPeaks \| null> |
| getId3Tags(mp3Bytes) | Uint8Array or Buffer | Promise<Id3Tags \| null>; untagged input can return an object with seven null fields |
| getId3CoverArt(mp3Bytes) | Uint8Array or Buffer | Promise<{ mimeType, pictureType, data } \| null>; data is a plain byte array |
These APIs accept file contents, not paths or streams. The JavaScript wrapper
forwards values without validating types or numeric ranges. Check both
rejections and nullable results. Do not infer metadata presence merely from
a truthy getId3Tags() result. The declarations allow both a null result and
an object whose seven fields are null, matching the current binary.
Waveform output contains version, channels, sample_rate,
samples_per_pixel, bits, length, and data. Current output uses version
2, one merged channel, and 16-bit peaks. length counts min/max pairs, so
data.length is twice length, not the number of source audio samples.
Cover bytes are not converted to JPEG; choose the saved extension from the
reported MIME type.
The package includes dist/audiowaveform.js and
dist/audiowaveform.wasm; the loader locates the WASM file beside itself.
Keep that layout when deploying or bundling. No compiler, native executable,
or extra JavaScript dependency is required at runtime. The module is loaded
on demand and reused; there is no public reset/dispose API. The Promise API
does not move decoding off the calling thread: extraction is CPU-bound.
The local 2026-09-21 audit exercised stereo PCM WAV extraction, invalid audio, and absent ID3 metadata/cover art. The format matrix below records earlier verification and was not rerun in full during this audit.
What's new in 1.2.0
resamplePeaks(peaks, samples = 1000): resize peaks to exactlysamplesmin/max pairs, whatever the extractionsamplesPerPixel.peaksToSvg(peaks, options): SVG in the style of midi-audio-player, one unfilled<path>in a stretchable viewBox, styled from CSS. Options:width(1000),height(width / 5),samples,className(audiowaveform),mirror,normalize.getAudioPackage(bytes, options):{ meta, svg }in one call.metaholdsformat,size,duration,sampleRate, averagebitrate(kbps),id3andcover(MP3 only;coveris{ mimeType, pictureType, data }withdataaUint8Array, ornullwhen there is no embedded art). PassincludePeaks: trueto also get the resampled peaks. Resolves tonullif the audio can't be decoded.
import { getAudioPackage } from '@kirigami/audiowaveform-wasm';
const { meta, svg } = await getAudioPackage(fs.readFileSync('song.mp3'), { width: 1000 });These are plain JavaScript on top of the peaks; duration is accurate to about one samplesPerPixel of audio.
Format support
| Format | Status | | --- | --- | | MP3 | ✅ Works — verified against 25 real files plus a deliberate 16-case CBR/VBR encoding matrix (see below) | | WAV (16-bit PCM) | ✅ Works | | WAV (24-bit PCM) | ✅ Works | | WAV (32-bit float) | ✅ Works | | AIFF | ✅ Works | | FLAC | ✅ Works | | Ogg Vorbis | ✅ Works | | Opus | ✅ Works | | M4A/AAC | ✅ Works | | WebM (Vorbis or Opus audio) | ✅ Works |
Unsupported input resolves to null when native decoding reports no result. Invalid argument types and WASM/runtime failures can still reject the promise.
MP3 encoding modes tested
This is the recorded format-verification matrix, not a regression suite executed by this repository’s npm test scripts.
| Case | Status | | --- | --- | | CBR 64 / 128 / 192 / 256 / 320 kbps | ✅ Works | | VBR quality 0 (best, ~245kbps) / 4 (~165kbps) / 9 (worst, ~65kbps) | ✅ Works | | Mono, CBR and VBR | ✅ Works | | 48000 Hz (MPEG1 framing) | ✅ Works | | 22050 Hz / 16000 Hz (MPEG2 framing) | ✅ Works | | Full-length file (~6.5 min), VBR | ✅ Works | | No Xing/LAME VBR header frame | ✅ Works | | Simple (non-joint) stereo vs. joint-stereo | ✅ Works |
See audiowaveform-wasm-compiler's README for how this matrix was generated.
ID3 tag support
Tested against 25 real, randomly-sampled tagged MP3s plus a deliberately adversarial batch: ID3v2.3, ID3v2.4, ID3v1-only, Unicode (Japanese, emoji, Cyrillic, French accents), very long strings, and numeric-style genre codes. See audiowaveform-wasm-compiler's CLAUDE.md for the full test results.
License
GPL-3.0-or-later — inherited from audiowaveform itself, which links libmad (GPL-2.0). See LICENSE for the full text.
Author
Maxime Larrivée-Roy, 2026
