jevstrudel.git / tools / measure / README.md
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.