Library API
Library API
Section titled “Library API”The package’s one entry (@arraypress/waveform-gen, ESM) exports generatePeaks and roundPeaks, plus — since 2.1.0 — the sounds-manifest functions: buildManifest, manifestEntry, isLoopPath (2.2.0+), createManifest, parseFilename, encodePeaks, resamplePeaks, fileUrl, naturalCompare, commonRoot, relativeTo, and the constants MANIFEST_VERSION (1) and DEFAULT_MANIFEST_BARS (64). No typings ship with it.
import { generatePeaks } from '@arraypress/waveform-gen';
const { peaks, bpm, duration } = await generatePeaks('./song.mp3', { samples: 1800, // optional — library now defaults to 1800 (matches the CLI) precision: 2, detectBPM: true,});
// peaks → number[] normalized 0..1 (loudest = 1.0)// bpm → number | null// duration → number (seconds, 2.1.0+)generatePeaks(filePath, options?)
Section titled “generatePeaks(filePath, options?)”(filePath: string, options?: { samples?: number; precision?: number; detectBPM?: boolean }) => Promise<{ peaks: number[]; bpm: number | null; duration: number }>
Reads and decodes the file, extracts max-amplitude peaks per bin across all channels, normalizes so the loudest peak is 1.0, and rounds to precision.
| Option | Default | Type | Description |
|---|---|---|---|
samples |
1800 |
number | Number of peaks. Must be a positive integer; numeric strings ('400') are accepted. Omitted / null → 1800. 0, negatives and non-integers throw (2.0.0+). |
precision |
2 |
number | Decimal places. >= 0 rounds; < 0 returns unrounded normalized peaks. |
detectBPM |
false |
boolean | When true, also run BPM detection and return bpm (number | null). |
Returns a Promise resolving to { peaks, bpm, duration }. bpm is null unless detectBPM is true and a stable tempo was found. duration is the decoded length in seconds (2.1.0+).
roundPeaks(peaks, precision) is the rounding step on its own — a negative precision returns the peaks unchanged.
Rejects on an invalid samples, a file that can’t be read, or one that can’t be decoded — a corrupt mp3/wav/flac/ogg is reported as appears to be corrupt or unreadable, m4a/aac get a convert-first hint, and a file that decodes to no audio throws instead of returning empty peaks.
import { generatePeaks } from '@arraypress/waveform-gen';import { writeFile } from 'node:fs/promises';
const result = await generatePeaks('./track.wav', { samples: 1800 });
// Build the same JSON shape the CLI writesconst output = { peaks: result.peaks };if (result.bpm != null) output.bpm = result.bpm;
await writeFile('./waveforms/track.json', JSON.stringify(output, null, 2) + '\n');BPM detection
Section titled “BPM detection”BPM is energy/onset based (window 2048, hop 1024): it detects transients, buckets the intervals into tempos 60 < bpm < 200, picks the most common, applies octave-doubling/halving correction, and subtracts a -1 calibration offset. It returns:
- an integer when a stable tempo is found,
nullwhen there are fewer than 2 onsets or no bucket matched.
It’s a heuristic, not exact metadata — verify critical values. When null, the CLI omits bpm from the JSON entirely.
Sounds manifests
Section titled “Sounds manifests”The functions behind --manifest (2.1.0+). Everything but buildManifest is pure — no decoding, no file system.
buildManifest(files, options?)
Section titled “buildManifest(files, options?)”(files: string[], options?) => Promise<{ version: 1, sounds: object[] }>
Decodes each file and returns a WaveformSounds manifest, sorted by path in natural order. files are audio file paths — expand folders yourself. It rejects on the first file it can’t decode (the CLI instead leaves that file out and exits 1), on a file outside root, and on a bars that isn’t a positive integer.
| Option | Default | Description |
|---|---|---|
root |
the deepest folder containing every file | The folder URLs and types are relative to. |
baseUrl |
'/' |
The public URL of root. |
bars |
64 |
Bars per sound in the hex peaks. |
type |
the file’s folder name | One type for every sound. |
detectBPM |
false |
Detect a tempo for files whose name carries none. |
samples |
1800 |
Resolution decoded at before downsampling to bars. |
waveform |
— | (file, rel) => string | undefined — the URL of a file’s full peaks JSON, written as that sound’s waveform. |
import { buildManifest } from '@arraypress/waveform-gen';
const manifest = await buildManifest(files, { root: './public/previews', baseUrl: '/previews/' });The pieces
Section titled “The pieces”| Function | Description |
|---|---|
parseFilename(name) |
{ title, bpm, key } read from a file name or path — the rules. bpm and key are null when not found. parseFilename('NW_Bass_Loop_04_128_Fmin.wav') → { title: 'NW Bass Loop 04', bpm: 128, key: 'Fm' }. |
manifestEntry(analysis, options?) |
One sound from { rel, peaks, duration?, bpm? } (bpm is a detected tempo, used only when the name has none) and { baseUrl, bars, type, waveform }. Adds loop: true when the path names a loop (2.2.0+). |
isLoopPath(rel): boolean |
Whether a folder or the file name has the word “loop” / “loops” in it (camelCase split first; a whole word only). 2.2.0+. |
createManifest(items) |
{ version: 1, sounds } from { rel, entry }[], sorted by rel in natural order. |
encodePeaks(peaks) |
Peaks 0..1 → 8-bit hex, byte-for-byte WaveformSounds’ encodePeaks. |
resamplePeaks(peaks, count) |
Downsample to count bars, keeping the loudest value in each bucket. |
fileUrl(relPath, baseUrl = '/') |
baseUrl + the path, each segment URL-encoded (a trailing / is added to baseUrl). |
naturalCompare(a, b) |
The path order: numeric, case-insensitive, folder by folder. |
commonRoot(entries) |
The deepest folder containing every { path, isDir } entry (a file counts as its folder); null when there is none. |
relativeTo(root, file) |
A file’s path under root with / separators; throws when it’s outside. |
Feeding the player
Section titled “Feeding the player”The generated JSON is consumed by the player and bar via attributes (no WaveformGen-specific attributes exist — these belong to the consuming libraries):
<div data-waveform-player data-url="song.mp3" data-waveform="waveforms/song.json"></div>Or via JS — see Options:
new WaveformPlayer(el, { url: 'song.mp3', waveform: 'waveforms/song.json', }); <div data-wb-play data-url="song.mp3" data-wb-waveform="waveforms/song.json"></div>See WaveformBar.
When a player is given pre-generated peaks, it skips client-side decoding entirely and renders the waveform immediately. If the JSON also carries bpm and markers, those flow through to the player’s tempo display and marker track.
Build-pipeline usage
Section titled “Build-pipeline usage”Batch a folder into a waveforms/ directory
Section titled “Batch a folder into a waveforms/ directory”waveform-gen ./public/audio/ --recursive --output ./public/waveforms/ --samples 1800 --bpmEvery supported file under ./public/audio/ becomes a JSON at the same relative path under ./public/waveforms/ — audio/albums/intro.mp3 → waveforms/albums/intro.json — with peaks, detected bpm, and any sidecar markers (2.0.0+; earlier versions flattened nested files into one folder).
npm script
Section titled “npm script”{ "scripts": { "waveforms": "waveform-gen ./src/audio/ --recursive --output ./public/waveforms/ --samples 1800 --bpm --quiet", "prebuild": "npm run waveforms" }}--quiet keeps CI logs clean — it hides progress only, so errors still reach stderr. Wiring it to prebuild regenerates peaks before every production build, and since a failed file exits 1 (2.0.0+), a corrupt file stops the build instead of shipping a missing JSON.
Programmatic batch with a manifest
Section titled “Programmatic batch with a manifest”When you need more control — your own index file, custom file names, or merged metadata — drive the library directly. (For a WaveformSounds list, use --manifest or buildManifest() instead: they write the format it reads.)
import { generatePeaks } from '@arraypress/waveform-gen';import { readdir, writeFile, mkdir } from 'node:fs/promises';import { join, basename, extname } from 'node:path';
const SRC = './src/audio';const OUT = './public/waveforms';const SAMPLES = 1800; // match the CLI default explicitly
await mkdir(OUT, { recursive: true });
const manifest = {};for (const file of await readdir(SRC)) { if (!/\.(mp3|wav|flac|ogg)$/i.test(file)) continue;
const name = basename(file, extname(file)); try { const { peaks, bpm } = await generatePeaks(join(SRC, file), { samples: SAMPLES, precision: 2, detectBPM: true, });
const output = { peaks }; if (bpm != null) output.bpm = bpm;
await writeFile(join(OUT, `${name}.json`), JSON.stringify(output, null, 2) + '\n'); manifest[file] = { waveform: `waveforms/${name}.json`, bpm }; } catch (err) { console.error(`[build] ${file}: ${err.message}`); // don't abort the batch }}
await writeFile(join(OUT, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');