Sounds manifest
For WaveformSounds, write one manifest for a whole pack instead of a JSON per sound: every file’s URL, a title, its type, BPM and key (read from the file name), its length and low-resolution peaks — so a list of hundreds of previews loads from one request.
npx @arraypress/waveform-gen ./public/previews/ --recursive \ --manifest ./public/previews/sounds.json --base-url /previews/<div data-waveform-sounds data-manifest="/previews/sounds.json"></div>| Flag | Default | Description |
|---|---|---|
--manifest <file> |
— | Where to write the manifest. Its folders are created as needed. Without --output, no per-file JSON is written — only the manifest. |
--root <dir> |
the deepest folder containing every input | The folder URLs and types are relative to. A directory argument counts as itself, a file argument (e.g. one a shell glob expanded) as its folder. A file outside --root fails with … is outside the manifest root …. |
--base-url <url> |
/ |
The public URL --root is served at. A path (/previews/) or an origin (https://cdn.example.com/packs); a trailing / is added if missing. |
--type <name> |
the file’s folder name | One type for every sound. By default a file’s type is the name of the folder it’s in — and files directly in --root get none. |
--manifest-bars <n> |
64 |
Bars per sound in peaks. An integer of 1 or more, or the run exits 1. |
--waveform-base-url <url> |
--base-url |
The public URL of --output. With --output, each sound links its full per-file JSON as waveform (used by the strip layout’s docked player). |
They combine with the usual flags: --recursive to walk sub-folders (which is what gives sounds their types), --bpm to detect a tempo for files whose name carries none, --samples for the resolution decoded at before downsampling to --manifest-bars, --quiet, and --output / --precision for the per-file JSON. Value flags take --flag value or --flag=value. --manifest with --format inline is a usage error (exit 2).
--base-url is the one to get right
Section titled “--base-url is the one to get right”Every url is --base-url joined with the file’s path relative to --root, each segment URL-encoded (Drum Loops/kick 1.wav → Drum%20Loops/kick%201.wav). The default --base-url is / — the site root — so set it whenever the audio isn’t served from the root:
| Files on disk | Served at | Command | A url |
|---|---|---|---|
public/previews/Bass/loop.wav |
/previews/ |
waveform-gen ./public/previews/ --recursive --manifest … --base-url /previews/ |
/previews/Bass/loop.wav |
public/previews/Bass/loop.wav |
— | the same, with no --base-url |
/Bass/loop.wav — wrong, a 404 |
packs/neon/Bass/loop.wav |
https://cdn.example.com/neon/ |
waveform-gen ./packs/neon/ --recursive --manifest … --base-url https://cdn.example.com/neon/ |
https://cdn.example.com/neon/Bass/loop.wav |
The manifest file itself can live anywhere — its location doesn’t change the URLs. With --output, the waveform links are built the same way from --waveform-base-url and each JSON’s path under --output:
npx @arraypress/waveform-gen ./public/previews/ --recursive \ --manifest ./public/previews/sounds.json --base-url /previews/ \ --output ./public/waveforms/ --waveform-base-url /waveforms/Output
Section titled “Output”{ "version": 1, "sounds": [ { "url": "/previews/Bass/NW_Bass_Loop_04_128_Fmin.wav", "title": "NW Bass Loop 04", "type": "Bass", "bpm": 128, "key": "Fm", "loop": true, "duration": 8.02, "peaks": "1f3a…" } ]}Pretty-printed (2-space indent) with a trailing newline. Each sound has, in this order:
| Field | Always? | Notes |
|---|---|---|
url |
Yes | --base-url + the path relative to --root, segment-encoded. |
title |
Yes | The file name with its extension dropped, separators (_, -, spaces) turned into single spaces, and the BPM and key words removed. If nothing would be left, the whole name. |
type |
When there is one | --type, else the name of the file’s folder. None for a file directly in --root. |
bpm |
When known | From the file name; with --bpm, a detected tempo fills in when the name has none. Left out otherwise — never guessed. |
key |
When known | From the file name, in the short form (Fm, C#, Bb). Left out otherwise. |
loop |
Loops only | true when a folder or the file name has the word “loop” in it (Loops from the path, 2.2.0+). Left out for everything else — a one-shot. |
duration |
When decoded | Seconds, to two decimals. |
peaks |
Yes | An 8-bit hex string, two lowercase characters per bar (00–ff). |
waveform |
With --output |
The URL of the sound’s full per-file JSON. |
Peaks. 64 bars by default (--manifest-bars), downsampled from the same normalised peaks the per-file JSON holds by keeping the loudest value in each bucket — so a short transient (a kick, a snare hit) still shows, and the loudest bar is ff. The encoding is byte-for-byte WaveformSounds’ own encodePeaks: each value clamped to 0..1, scaled by 255 and rounded. At 64 bars that’s 128 characters a sound. There is no peakScale in the output — it only applies to integer arrays, and hex needs none.
Order. Sounds are sorted by path, in natural order — Loop 2 before Loop 10, folder by folder, case-insensitively.
Failures. A file that fails to decode is left out of the manifest and still fails the run (exit 1); the manifest is written with every file that succeeded. With --output, a sound is only listed once its JSON has been written, so it never links a file that doesn’t exist.
Without --quiet (and without --output), each file’s line shows what was read from its name — ✅ NW_Bass_Loop_04_128_Fmin.wav (128 BPM, Fm) — and the run ends with a summary: 📋 ./public/previews/sounds.json: 300 sounds, 212 with BPM, 187 with key. Worth a glance — it’s how you spot a pack whose names the parser couldn’t read.
BPM and key from file names
Section titled “BPM and key from file names”The parser errs towards leaving a value out: an unknown BPM just isn’t filterable, while a wrong one files the sound under the wrong filter. The words of a name are the parts between spaces, _ and -; brackets and trailing punctuation around a word are ignored, so (C# minor) and [140 BPM] read fine.
BPM — a whole word that is a number from 50 to 220:
- Marked tempos win:
128bpm,bpm128,128 BPM,BPM_174. - A bare number counts too (
_128_) — but not one with a leading zero (090is an index), and not when there are two different bare candidates with nothing to choose between them. Then the one sitting right beside a key wins; if neither is, there’s no BPM. - Numbers outside 50–220 (
250,04) are never a BPM.
Key — a whole word (or a root followed by a separate mode word, F_minor, C# Major):
- Read anywhere: a mode word (
Fmin,Cmaj,F#minor,Bbmin), a sharp (F#,A#m), a flat withm(Bbm), or an upper-case root with a lower-casem(Fm— but notFM, the synth, and notfm). - Read only right beside the BPM: the ambiguous spellings — a lone root (
A,C), a lone flat (Eb), andAm(also an English word). - Written in the short form:
Fmin→Fm,Cmaj→C,A♯→A#.
| File name | title |
bpm |
key |
|---|---|---|---|
NW_Bass_Loop_04_128_Fmin.wav |
NW Bass Loop 04 |
128 |
Fm |
Lead 128bpm A#m.wav |
Lead |
128 |
A#m |
Vox (C# minor) [140 BPM].wav |
Vox |
140 |
C#m |
Bass_F_minor_124.wav |
Bass |
124 |
Fm |
Keys_Gbmaj_96.wav |
Keys |
96 |
Gb |
Drums_BPM_174.wav |
Drums |
174 |
— |
Chord_Loop_A_120.wav |
Chord Loop |
120 |
A (beside the BPM) |
Chord_Loop_A.wav |
Chord Loop A |
— | — (a lone A, no BPM beside it) |
Pluck_120_Am.wav |
Pluck |
120 |
Am (beside the BPM) |
Am_Pluck_120.wav |
Am Pluck |
120 |
— (Am isn’t beside the BPM) |
Loop_120_Eb.wav |
Loop |
120 |
Eb |
Synth FM 128.wav |
Synth FM |
128 |
— (FM is the synth) |
Pad_090_Cmaj.wav |
Pad 090 |
— (leading zero: an index) | C |
Loop_90_128.wav |
Loop 90 128 |
— (two bare numbers, no key beside either) | — |
Loop_100_140_Fm.wav |
Loop 100 |
140 (the one beside the key) |
Fm |
Perc_250.wav |
Perc 250 |
— (out of range) | — |
Kick_01.wav |
Kick 01 |
— | — |
Names the parser can’t read are left with no BPM or key — fill them in afterwards if you know them, or rename the files. WaveformSounds normalises keys again when it reads the manifest, so a hand-written "F minor" works too.
Loops from the path
Section titled “Loops from the path”2.2.0+. A sound gets "loop": true when a folder or the file name has the word loop or loops in it; everything else is left unmarked, which WaveformSounds reads as a one-shot. A whole word only, and camelCase is split first:
| Path | loop |
|---|---|
Drum Loops/Groove_01.wav |
true (the folder) |
Bass_Loop_01.wav |
true |
DrumLoop_120.wav |
true (camelCase) |
Loopmasters_Kick.wav |
— (inside another word) |
Kick_128.wav |
— (a tempo doesn’t make a loop) |
In code, isLoopPath(rel) gives the same answer.
From code
Section titled “From code”The same thing as a library call, for a build script:
import { buildManifest } from '@arraypress/waveform-gen';import { writeFile } from 'node:fs/promises';
const manifest = await buildManifest(['./public/previews/Bass/loop-01.wav', './public/previews/Drums/kick.wav'], { root: './public/previews', baseUrl: '/previews/',});await writeFile('./public/previews/sounds.json', JSON.stringify(manifest, null, 2) + '\n');Unlike the CLI, buildManifest() takes a list of files (expand your folders yourself) and throws on the first file it can’t decode. See Library API for its options and the pieces it’s built from — parseFilename() among them.