jevstrudel.git / tools / measure / README.md

tools/measure

Measures each song's mix as written and records it in the song's SPEC.md frontmatter as measured:, so the art critic judges craft on how the parts actually sit, not only on the code.

nix run .#measure                                   # every song, print only
nix run .#measure -- --write                        # and record in each SPEC.md
nix run .#measure -- --song jev/lightning-in-a-bottle --write
nix run .#measure -- --url http://localhost:4322 --tabs 4

It needs the dev server running (buck2 run //:dev, default http://localhost:4322). Each song plays in its own silent headless tab (?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 few seconds for the ending to ring, with the Jev relay blocked, so every jev() plays its fallbacks and the song plays as written. The page's meter (website/src/jev/meter.mjs) then gives loudness and peak in dBFS, in total and per part. A part is the orbit a song names for its question (part('riff', …) plays on .orbit('riff')); numbered orbits are reported together as other.

Songs run --tabs at a time (default 4), so the whole set takes about as long as its longest few songs: Lightning alone is 102 s of music.

Each song is measured whole and per section. Sections follow the song's .every(N) cadence (its longest, when it has several: sectionCycles in website/src/jev/measured.mjs, which tools/check-songs uses too), and are keyed by the cycles they span, counted from 1: "1-4", "5-8", …, the last cut short where the written length ends. So a spec's promise such as "each drop at least 6 dB over the think before it" can be checked against the numbers. The ending's ring-out after the last bar counts in the whole-song figures only.

The record is one JSON line, which is also YAML:

measured: {"date":"2026-09-24","song":"<hash of song.js>","total":{"loudnessDbfs":-16.2,"peakDbfs":-1.4},"parts":{"riff":{…},…},"sections":{"1-4":{"total":{…},"parts":{"riff":{…},…}},…}}

A section the meter heard too little of is null. The line runs to a few kilobytes for a long song; the site's pages and the art critic take only each section's total (withoutSectionParts, and measuredMix in website/src/jev/critic.mjs), since every part of every section would multiply both for detail the whole-song parts already give.

The total is what reaches the speakers, after superdough's master limiter at -1 dBTP (packages/superdough/superdoughoutput.mjs); parts are each orbit's output, before it. A total peak of -1.0 means the limiter is holding the mix down: turn the song's postgain down until it measures under.

song is the hash of the song.js measured (website/src/jev/measured.mjs). Once the song changes, the measurement no longer applies and the critic stops reading it: measure again after editing a song, then rescore it with tools/critic/.

Run from a worktree while the dev server serves another checkout, songs hear the worktree's samples/ (re-rendered and new sounds included), and the output says so first (tools/worktree-samples/). The site's code is still the server's: to measure a worktree's superdough or site changes, run a dev server from it on another port and pass --url.

flake.nix's measure app runs this script from the working tree (it writes the working tree's specs), with Playwright and its browsers from the same nixpkgs.