Godstrument/REVELATION_BRIEF.md
2026-07-14 13:21:57 +10:00

131 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# REVELATION — build brief · TESTAMENT · WHEELS · PSALM · CANON
> **You are an execution agent (Opus).** One lane of a four-feature wave. Read **Part 0**, execute ONLY your lane, end with the §0.8 report (defined in `GODRUM_BRIEF.md`, our process root — GODRUM and THE PANTHEON both shipped through this loop). The planning model (Fable) reviews every stage before merge.
---
## Part 0 — shared context
### 0.1 The four revelations
| name | what | maps to (the DAW-study gap) |
|---|---|---|
| **📼 TESTAMENT** | record the performance — the master mix to a file, one button | a live instrument that can't record itself |
| **☸ WHEELS** | per-lane pattern lengths (116) — polymeter, the wheel within a wheel | flexible MIDI/pattern editing |
| **🕊 PSALM** | sing to the instrument — mic pitch, quantized to the scale, played as notes (or latched into GODSPEED) | vocal integration, Godstrument-style |
| **✦ CANON** | factory dimensions — curated patches that ship with the instrument | Logic's stock-content advantage |
### 0.2 Ground truth & hard gate
- **GATE: do not branch until PANTHEON Stage 3ε has merged to `main`.** Confirm your base has ε's work (`git log --oneline -5` shows the ε commit; grep `godspeak` reaches a `handleMessage` branch). If ε hasn't landed, STOP and report. All line anchors below were verified at `main@b8b1188` (pre-ε) — **they WILL have drifted; grep for names, never trust raw numbers.**
- One-file app `viz/index.html` (~9.4k lines): app IIFE with the `Synth` IIFE near the bottom (`const Synth = (function () {`). Python side: `hub.py` (OSC→ws), `run.py`, `workers/`. Conventions, worktrees, commit style, §0.7 regression (zero console errors; `♪ T O B G` + ⌘Z all work), temp-hook discipline, never-deploy/never-main: all per `GODRUM_BRIEF.md` Part 0 — read it.
- Relevant machinery you inherit: the groove clock (`frame()` advances `seqPhase`; on each new 16th `seqCurStep = st16; seqTick()`), `seqs` (`{on, steps[16], vel, chance, rat, lock}` via `seqFor`/`drumSeqFor`, normalized by `migrateSeq`, serialized wholesale by `serializePatch`), the beat panel (`openBeat`, lanes, `beatRefresh`), the arranger grooves tab, `exportMid()` (ch-9 drum block added by GODRUM-E), `Synth.pluck(midi, vel)`, GODSPEED's `arp`/`arpLatch(note, vel)`, UNGODLY's `ungodlyMark`/`ungodlyGesture` (mark BEFORE mutating), the master chain `out → squash → squashMakeup → lim → destination` (~8585 pre-ε), the `Recorder` AudioWorklet (fixed-length captures — NOT what TESTAMENT uses), `gScale`/`SCALES`/`scaleRows`, and the hub's `_scan_patches` (serves named template patches from a dir to the client's template select).
- Branches: `revelation/t-testament` · `revelation/w-wheels` · `revelation/p-psalm` · `revelation/c-canon` · `revelation/o-closer` (ω), each from post-ε `main`.
### Stage map
| stage | lanes | parallel-safe because |
|---|---|---|
| 1 | **T 📼 ∥ W ☸** | T = Synth IIFE + one header button; W = app-scope sequencer/panels/exportMid — disjoint, and neither touches ε's `handleMessage` |
| 2 (after 1 merges) | **P 🕊 ∥ C ✦** | P = index.html only (new self-contained block); C = Python + a new `factory/` dir + JSON — file-disjoint |
| 3 (after 2 merges) | **ω the closer** | grimoire chapter (all four), manual, cross-feature QA, deploy checklist |
Reviews between stages. Do not start later-stage work.
---
# Stage 1T — 📼 TESTAMENT ("it is written")
**Territory:** inside the `Synth` IIFE, PLUS: one header button (`#testament`, created app-scope beside the ♪ button — note the ♪ button itself is DOM built *inside* the Synth IIFE at its tail; mirror that pattern and build yours there too, keeping you 100% IIFE-resident), one `ctxItem`, one small CSS block. Nothing else.
**T.1 The tap.** Recording captures the TRUE final mix — post-squash, post-limiter. In `build()`: expose `lim` on the returned `N` (one word added to the return object). The recorder itself is lazy:
```js
let testDest = null, testRec = null, testChunks = [], testT0 = 0, testTimer = null;
function testamentStart() {
if (!ctx) start(); // wake the graph — a recording can begin the session
if (testRec) return false; // already rolling
if (!testDest) { testDest = ctx.createMediaStreamDestination(); N.lim.connect(testDest); }
const mime = ["audio/webm;codecs=opus", "audio/webm", "audio/mp4"]
.find(m => window.MediaRecorder && MediaRecorder.isTypeSupported(m));
if (!mime) return false; // (Safari fallback lands on audio/mp4)
testChunks = []; testRec = new MediaRecorder(testDest.stream, { mimeType: mime, audioBitsPerSecond: 256000 });
testRec.ondataavailable = e => { if (e.data && e.data.size) testChunks.push(e.data); };
testRec.start(1000); // timesliced — a crash loses ≤1s
testT0 = ctx.currentTime; return true;
}
function testamentStop() { ... } // → Promise<{blob, secs, ext}> resolved in onstop
function testamentState() { return { on: !!testRec, secs: testRec ? ctx.currentTime - testT0 : 0 }; }
```
Export `testamentStart/Stop/State` on the Synth return object. The worklet `Recorder` (GODSONIQ/AMPLER) is untouched — different machinery, different purpose.
**T.2 The button.** `#testament` next to `#sound` (♪): shows `⏺`; while rolling it turns red (`.on`), pulses gently, and shows elapsed `m:ss` (update via a 1s interval only while rolling). Click toggles. On stop: build the file name `godstrument-testament-<yyyymmdd-hhmm>.<ext>` (derive the timestamp from `new Date()` at stop time), trigger the download (the `a[download]` + `URL.createObjectURL` idiom), and ticker: `📼 testament: <m:ss> — it is written`. Failure (no MediaRecorder) → ticker `📼 this browser cannot bear witness`, button stays off. Add `ctxItem("📼 TESTAMENT — record what god does", …)` near GODSONIQ.
**T.3 Verify.** Live: record ~6s of the sounding world (drums playing), stop → blob size > 0, elapsed matches ±0.5s, and **prove the audio is real**: load the blob into an `<audio>` element and play it, or decode it back through `decodeAudioData`/an offline probe and measure nonzero RMS. Record-while-♪-off (graph wakes, captures the beat you then start). Two consecutive recordings (state resets). §0.7 regression, zero console errors. Note honestly what the headless env can't do (audition by ear).
---
# Stage 1W — ☸ WHEELS ("a wheel within a wheel")
**Territory:** viz/index.html app scope, OUTSIDE the Synth IIFE: the seq factories (`seqFor`, `drumSeqFor`), `migrateSeq` (+ its self-check), the clock lines in `frame()`, `seqTick`, the S&H block in `computeDests`, `applyGroove` call sites, the beat panel + arranger grooves tab (lane-length UI + playhead paint), `exportMid`'s drum block. NOT the Synth IIFE, NOT `handleMessage`, NOT GODSPEED's arp block.
**W.1 The data.** Every seq gains `len` (int 116, default 16): add to both factories; `migrateSeq` clamps/defaults it (`Number.isFinite` → round → clamp 1..16, else 16) and the self-check gains one assertion (a len-12 seq round-trips; a len-less legacy seq gets 16). It rides `serializePatch` automatically — verify, don't assume.
**W.2 The clock.** Add ONE global absolute step counter: `let seqAbsStep = 0;`, incremented exactly where `seqCurStep` is assigned in `frame()` (`seqCurStep = st16; seqAbsStep++; seqTick();`). Then one tiny helper next to `seqFor`:
```js
const wheelStep = (sq) => (sq && sq.len && sq.len < 16) ? seqAbsStep % sq.len : seqCurStep;
```
Replace the per-seq step index in: `seqTick`'s drum branch (`const cur = wheelStep(sq)`), `seqTick`'s note branch, the S&H sampling in `computeDests`, and every `applyGroove(sq, step, v)` call site (grep them — pass `wheelStep(sq)`). A full-length lane (`len` 16) must behave **bit-identically to today** (the helper returns `seqCurStep` — that's the regression guarantee).
**W.3 The UI.** Per-lane length control in BOTH grid editors, minimal and house-style:
- Beat panel: a small `16`-showing number chip in each lane's lead (beside ⬢); click-drag or click-to-edit 116; `ungodlyGesture("wheel")` before mutating. Cells at index ≥ `len` render dimmed (a `.off` class, ~35% opacity) and clicking them does nothing; the playhead for that lane paints at `wheelStep(sq)`.
- Arranger grooves tab: the same control in the euclid row for the selected lane; the big grid dims ≥ `len` and its playhead uses `wheelStep`.
- Euclid fills should fill within `len` (pass `len` as the default `len` argument where the lane's control is present — keep the explicit euclid-steps input working as an override).
**W.4 Export.** In `exportMid`'s ch-9 drum block: replace the `bar × 16-step` nesting with one absolute loop — `for (let sa = 0; sa < arr.bars * 16; sa++) { const s2 = sq.len < 16 ? sa % sq.len : sa % 16; … tick = sa * TICKS16 … }` — so a 12-wheel hat exports exactly as it plays. Note lanes stay as-is (they're arranged by clips; polymeter for note clips is a flagged future, not v1).
**W.5 Verify.** Deterministic: kick len 16 + hat len 12 → drive 48 ticks (LCM) and assert the hat's fire pattern wraps at 12 while kick wraps at 16 (spy `Synth.drum` or the branch internals via a temp hook); len 1 fires every step; legacy patch (no len) loads → all 16, identical behavior to pre-WHEELS (byte-level: same fires over 32 ticks). S&H on a source with len 8 samples on the 8-wheel. UI: dimmed cells, per-lane playheads diverge visibly, undo reverts a len change. Export: byte-parse a 12-wheel pattern → note-ons at ticks matching `sa % 12`. §0.7 + GODRUM/PANTHEON regression, zero console errors.
---
# Stage 2P — 🕊 PSALM ("sing, and the instrument answers")
**Territory:** viz/index.html only — one new self-contained block (place after the GODSPEED block), one `ctxItem`, one small CSS chip. Nothing in the Synth IIFE; you consume `Synth.pluck` and `arpLatch` (both exist).
**P.1 The listener.** Mic analysis lives in its OWN throwaway `AudioContext` — the mic never enters the music graph (zero feedback risk; say so in a comment). `getUserMedia({ audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: false } })``AnalyserNode` (fftSize 2048) → a `setInterval(…, 25)` loop (NOT rAF — hidden tabs freeze it) pulling time-domain frames.
**P.2 The pitch.** Autocorrelation in plain JS (house-idiomatic, no libraries): normalize the frame, compute ACF over lags spanning 801000 Hz, pick the first peak above 0.9 × max with parabolic interpolation; reject frames with RMS below the sensitivity threshold or ACF clarity < ~0.6. Convert `freq → midi` (float). Write the detector as a **pure function of a Float32Array** (`psalmDetect(buf, sampleRate) → {freq, clarity, rms}`) so it's unit-testable without a mic.
**P.3 The note.** Quantize the float midi to the global scale — reuse the instrument's own machinery (`gScale` + `scaleRows`/the quantize helpers — grep how the piano roll folds to scale and mirror it; nearest scale degree across octaves). Note-on logic: gate opens when RMS > threshold AND the same quantized note holds for 2 consecutive frames → fire ONCE; re-fire only when the quantized note *changes* (legato) or after the gate closes (RMS below threshold for ~150ms) and reopens. Velocity from RMS (clamped 0.31). Where it goes: **if `arp.on` → `arpLatch(note, vel)`** (sing your chord into GODSPEED, one note at a time — the killer demo); **else → `Synth.pluck(note, vel)`**.
**P.4 The UI.** `ctxItem("🕊 PSALM — sing, and the instrument answers", …)` toggles it (requests the mic on first enable; denial → ticker `🕊 the instrument cannot hear you — allow the microphone`, state off). While on: a small fixed chip (near the launcher stack) showing 🕊 + the live note name (`midiToName`), dimmed when the gate is closed; clicking the chip toggles off. A sensitivity slider on the chip's hover/click is optional — a sane fixed threshold with a `PSALM_SENS` const is acceptable v1. Privacy note in a comment: local analysis only, nothing leaves the page.
**P.5 Verify.** The detector is pure — unit-test it headlessly: synthesize Float32Array sines (220, 440, 554.37 Hz + a noisy one + silence) → assert freq within ±1%, clarity high, silence rejected; run the note-logic as a pure state machine on scripted `{freq, rms}` sequences → assert single-fire, legato re-fire, gate close/reopen, correct scale-fold (set `gScale` to minor pent, feed 445 Hz → the folded degree). Live mic = human lane; say so. UI chip renders/toggles; mic-denied path degrades cleanly. §0.7, zero console errors.
---
# Stage 2C — ✦ CANON (factory dimensions)
**Territory:** Python + data only: `hub.py` (`_scan_patches` region), a NEW git-tracked `factory/` directory of patch JSONs, `run.py` only if genuinely needed (it shouldn't be). ZERO index.html.
**C.1 The shelf.** Today `_scan_patches` serves user patches from the gitignored `patches/` dir. Add a second scan of `factory/` (git-tracked, ships with the deploy — confirm the CLAUDE.md rsync filter doesn't exclude it; it doesn't) whose entries appear in the same `patchList` with names prefixed `✦ ` (e.g. `✦ first light in stereo`). Factory patches are read-only: the hub's save path must never write into `factory/` (guard by name prefix). Client-side needs nothing — the template select already renders whatever `patchList` carries.
**C.2 The canon itself.** Author **four** patches as JSON (the `serializePatch` shape: `routes, tweaks, seqs, arr, bpm, groove, scale, kit`). Derive the exact shape by reading `serializePatch`/`loadPatch` and one existing saved patch if available. The four, each a one-line lesson in an instrument feature:
1. **`✦ the heartbeat`** — a GODRUM showcase: mpc 8-4 groove, a tasteful boom-bap kit (use the seeded pattern as a base, vary it), kick duck 0.5, bpm 88.
2. **`✦ the wheel within the wheel`** — WHEELS showcase: kick len 16, hat len 12, clap len 10, sparse, hypnotic, bpm 96. (Gate: only author this if WHEELS has merged — it has, you're Stage 2.)
3. **`✦ the squeeze`** — GODSQUASH showcase: a route from a pumping world source onto `squash` at high amount, drums under it, bpm 120.
4. **`✦ quiet arrival`** — a gentle first-light-style ambient patch: earth echo audible, sparse routes, no drums, slow bpm.
Validate each by round-tripping through the client's `migrateSeq` expectations (16-step arrays, sane ranges) — write a tiny `--check`-style python validator in the lane (a `test_factory.py`-named file so the deploy filter excludes it) that asserts shape.
**Taste disclaimer in your report:** these are structural drafts for the human to re-voice by ear; correctness of shape is your deliverable, beauty is his.
**C.3 Verify.** Run the hub locally (`--profile minimal`), confirm `patchList` carries the four `✦` names, load one via the client template select in a browser (full path), confirm the read-only guard (attempt a save named `✦ x` → rejected/redirected), and that a missing/empty `factory/` dir degrades silently. Zero index.html changes (`git diff --stat`).
---
# Stage 3ω — the closer (brief finalized after Stage 2 review)
Scope preview: grimoire chapter for all four (Ezekiel 1:16 belongs in the WHEELS entry — *"as it were a wheel in the middle of a wheel"*; PSALM's privacy line — the instrument listens locally, nothing leaves the machine; TESTAMENT's "it is written"; the CANON as the shelf of worked examples) · `build_manual.py` + commit manual · cross-feature QA (record a TESTAMENT of a WHEELS beat while PSALM latches the arp — the whole wave in one take) · deploy checklist. Do not start early.