Godstrument/REVELATION_BRIEF.md
type-two e6c464daae 📋 revelation: post-convergence amendments — wlen (len is taken), gates, deploy notes (fable)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 13:26:54 +10:00

19 KiB
Raw Permalink Blame History

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.


⚠ AMENDMENTS — read BEFORE your lane (Fable, 2026-07-14, post-convergence)

This brief was drafted at main@b8b1188, before the pantheon merged with the crossover wave (keyroll note-length · world_sats · OpenSky cache · the god's-eye Spirit · 18 post-merge review fixes). The convergence landed as 19f1770. These amendments override anything they contradict below.

  1. GATE (replaces §0.2's): branch from main19f1770. Confirm your base has BOTH waves: grep -c godspeakCommand viz/index.html ≥ 1 (ε) AND grep -c attachNoteLengths viz/index.html ≥ 1 (keyroll) AND git log --oneline -3 includes the ⚭ convergence commit. If not, STOP.
  2. WHEELS — the len name is TAKEN. Keyroll landed seq.len as a per-step ARRAY of note lengths (see seqFor: len: new Array(16).fill(1), plus migrateSeq's len-default/len-badlen self-checks). WHEELS's per-lane pattern length must be a different field: wlen (int 116, default 16). Everywhere W.1W.5 below says len, read wlen. Your migrateSeq additions must coexist with the existing len-array migration (add wlen clamping beside it; new self-check assertions for wlen, don't touch the len ones).
  3. WHEELS × keyroll indexing: the note branch of seqTick now reads sq.len[cur] (per-step note length) — with wheels, cur becomes wheelStep(sq) for BOTH the steps[] gate and the len[] lookup (one cur, used twice — do not let them diverge).
  4. WHEELS × exportMid: the drum block now has an owned(v, sq) gate (lanes export only if playing or edited off DRUM_SEED) — your absolute-loop rewrite in W.4 must keep that gate exactly. The note track is length-aware with a same-pitch overlap clamp; don't disturb it.
  5. TESTAMENT: the master chain gained nodes since your anchor was written (squash from β; the Schumann layer predates). Grep the actual tail of build() for the final limiter node and its name — do not trust N.lim as a name, verify it. Also Synth.pluck now takes an optional durMs — irrelevant to your tap, but don't be surprised by the signature.
  6. PSALM: Synth.pluck(midi, vel, durMs) exists — sustained sung notes may pass a duration instead of relying on the fixed decay; treat that as an allowed (flag-it-in-report) enhancement, not scope creep. The mic-analysis AudioContext isolation rule stands.
  7. CANON: factory patches must survive migrateSeq with the NEW fields (len arrays, wlen once WHEELS lands, locked, kit) — author them by exporting real patches from a post-WHEELS build, never by hand-writing JSON.
  8. Regression additions to §0.7: the groove self-check must stay green (it now asserts len-array migration), and the god's-eye skin must still cycle (right-click → skin) with zero console errors — it shares the overlay/render path some of you will be near.
  9. Deploy note for ω (the closer): the production box is currently TWO waves behind and its venv still lacks sgp4 — the deploy checklist you prepare must start with /home/humanjing/godstrument/.venv/bin/pip install sgp4, then the CLAUDE.md filtered rsync, then restart. GODSPEAK stays local-rig-only (inert on the VPS); mlx-whisper must never enter requirements.txt.

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:

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:

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.onarpLatch(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.