1# tools/measure 2 3Measures each song's mix as written and records it in the song's `SPEC.md` 4frontmatter as `measured:`, so the art critic judges craft on how the parts 5actually sit, not only on the code. 6 7```sh 8nix run .#measure # every song, print only 9nix run .#measure -- --write # and record in each SPEC.md 10nix run .#measure -- --song jev/lightning-in-a-bottle --write 11nix run .#measure -- --url http://localhost:4322 --tabs 4 12``` 13 14It needs the dev server running (`buck2 run //:dev`, default 15`http://localhost:4322`). Each song plays in its own silent headless tab 16(`?session=measure-<pid>-N`, unique per run so runs side by side never share a tab), at normal speed for its length as written plus a 17few seconds for the ending to ring, with the Jev relay blocked, so every 18`jev()` plays its fallbacks and the song plays as written. The page's meter 19(`website/src/jev/meter.mjs`) then gives loudness and peak in dBFS, in total 20and per part. A part is the orbit a song names for its question 21(`part('riff', …)` plays on `.orbit('riff')`); numbered orbits are reported 22together as `other`. 23 24Songs run `--tabs` at a time (default 4), so the whole set takes about as 25long as its longest few songs: Lightning alone is 102 s of music. 26 27Each song is measured whole and per section. Sections follow the song's 28`.every(N)` cadence (its longest, when it has several: `sectionCycles` in 29`website/src/jev/measured.mjs`, which `tools/check-songs` uses too), and are 30keyed by the cycles they span, counted from 1: `"1-4"`, `"5-8"`, …, the last 31cut short where the written length ends. So a spec's promise such as "each 32drop at least 6 dB over the think before it" can be checked against the 33numbers. The ending's ring-out after the last bar counts in the whole-song 34figures only. 35 36The record is one JSON line, which is also YAML: 37 38```yaml 39measured: {"date":"2026-09-24","song":"<hash of song.js>","total":{"loudnessDbfs":-16.2,"peakDbfs":-1.4},"parts":{"riff":{…},…},"sections":{"1-4":{"total":{…},"parts":{"riff":{…},…}},…}} 40``` 41 42A section the meter heard too little of is `null`. The line runs to a few 43kilobytes for a long song; the site's pages and the art critic take only each 44section's total (`withoutSectionParts`, and `measuredMix` in 45`website/src/jev/critic.mjs`), since every part of every section would 46multiply both for detail the whole-song parts already give. 47 48The total is what reaches the speakers, after superdough's master limiter at 49-1 dBTP (`packages/superdough/superdoughoutput.mjs`); parts are each orbit's 50output, before it. A total peak of -1.0 means the limiter is holding the mix 51down: turn the song's postgain down until it measures under. 52 53`song` is the hash of the `song.js` measured 54(`website/src/jev/measured.mjs`). Once the song changes, the measurement no 55longer applies and the critic stops reading it: measure again after editing 56a song, then rescore it with `tools/critic/`. 57 58Run from a worktree while the dev server serves another checkout, songs hear 59the worktree's `samples/` (re-rendered and new sounds included), and the 60output says so first (`tools/worktree-samples/`). The site's code is still 61the server's: to measure a worktree's superdough or site changes, run a dev 62server from it on another port and pass `--url`. 63 64`flake.nix`'s `measure` app runs this script from the working tree (it 65writes the working tree's specs), with Playwright and its browsers from the 66same nixpkgs.