Godstrument/GODRUM_BRIEF.md
2026-07-13 12:15:38 +10:00

463 lines
46 KiB
Markdown
Raw Permalink 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.

# GODRUM — build brief · Stage 1
> **You are an execution agent (Opus).** You have been assigned a lane: **Agent A** (drum engine) or **Agent B** (beat panel + sequencer). Read **Part 0** fully, then execute ONLY your own part. Do not do the other agent's work, even if it looks easy. Your output will be reviewed by the planning model before merge; end your run with the report described in §0.8.
---
## Part 0 — shared context (both agents read this)
### 0.1 What we're building
GODRUM: a "make a beat" feature for the Godstrument. Godstrument already has a complete rhythmic backbone — a master clock (`godtime`), a 16-step sequencer with per-step velocity/chance/ratchet/p-lock, swing + humanize with groove presets, Euclidean fills, and an 8-bar arranger — but **no drums**. The only percussion today is one synthesized noise hi-hat. Stage 1 adds:
- **Agent A:** five synthesized drum voices (kick, snare, clap, closed hat, open hat) inside the `Synth` engine, with a small frozen public API.
- **Agent B:** drum lanes in the step sequencer + a `🥁 beat` panel to program them, plus persistence of the kit.
Later stages (not yours): integration + kick-ducking (C), GODSONIQ sample-slicing (D), finger-drumming/live record (E), grimoire docs (F).
### 0.2 The codebase in 60 seconds
- Everything is **one file**: `viz/index.html` (~8,470 lines, ~600 KB). Lines ~4251200 are the in-app manual ("grimoire", HTML prose). Line 1213 opens one giant IIFE containing the whole app. Vanilla JS + raw Web Audio, **no libraries**.
- The synth is the `Synth` IIFE at **lines 77948180**. All voices sum into `N.preSat` → saturation → post → glitch → bitcrush worklet → `N.out` → limiter → destination, with reverb/delay/chorus/Earth-Echo as parallel sends built in `build()` (7817+).
- The groove clock: `frame()` advances `seqPhase` (line 4066); swing/humanize shift each 16th's onset (40684074); on each new step `seqTick()` (line 2413) fires every enabled seq. **Swing is applied at the clock level, so anything fired from `seqTick()` inherits the groove for free.**
- Seqs live in the `seqs` object (line 1488), keyed by destination key, created by `seqFor(key, isNote)` (1556): `{on, steps[16], vel[16], chance[16], rat[16], lock[16]}`.
- Patches serialize via `serializePatch()` (2621) / `loadPatch()` (2634); `seqs` is deep-copied wholesale, and `migrateSeq()` (2560) normalizes each on load — any 16-step seq round-trips, so **drum patterns saved under `seqs` persist with zero extra work**. Kit knob state does need explicit plumbing (Agent B).
- UI: full-screen canvas + DOM overlay panels. Launcher buttons `🎛 tracks` / `🎹 OMNI` are created at lines 23992411 (`#arrbtn`, `#omnibtn` — CSS in the `<style>` block, search for those ids). The right-click sky menu is built with `ctxItem(label, fn, kbd)` around lines 65536631. The arranger panel `openArranger()` (1729) with its "grooves" tab (1783+) is the house style for grid editors — study it before building anything.
- Keyboard keys already taken (handler ~63406362): `P C O W Z T Escape` + arrows. **`B` is free** — GODRUM claims it.
### 0.3 Territory rules — this is what makes parallel work safe
- **Agent A edits ONLY inside the `Synth` IIFE**: between `const Synth = (function () {` (line 7794) and its closing `})();` (line 8180). Not one character outside it.
- **Agent B edits anywhere EXCEPT inside that IIFE** (and B must not redefine or monkey-patch `Synth`).
- Both branches will be merged by git; disjoint regions merge clean. If you believe you *must* cross the boundary, STOP, note it in your report as an open question, and work around it instead.
### 0.4 Git workflow
Work in your own worktree so the two lanes never collide:
```sh
cd /Users/m3ultra/Documents/godstrument
# Agent A:
git worktree add ../godstrument-A -b godrum/a-engine
# Agent B:
git worktree add ../godstrument-B -b godrum/b-beat-panel
```
Do all work in your worktree. Commit style matches the repo: lowercase, emoji-prefixed, e.g. `🥁 godrum: five drum voices in the Synth engine`. Small logical commits are fine; one commit is also fine.
### 0.5 Hard rules
- **Never deploy.** No rsync, no ssh, no systemctl. Stage 1 is local-only.
- **Never commit** `godstrument.db`, `godstrument_users.db`, `auth_secret`, anything in `patches/`, or `viz/manual.html` changes (it's generated).
- **Don't touch the grimoire prose** (lines ~4251200) or `GODSTRUMENT_MANUAL_SOURCE.md` — docs are Stage 4, and `viz/index.html`'s grimoire is canon when they disagree.
- **Surgical edits only.** The file is huge: read it in targeted chunks, never rewrite whole sections, never reformat code you didn't write, match the surrounding style exactly (2-space indent, `const`, terse inline comments in the house voice — poetic but precise).
- **No new dependencies, no external assets.** Drums are synthesized from oscillators and noise buffers, like everything else in this engine.
- All new numeric params are **normalized 0..1** at the API boundary and mapped to real units inside the engine (this is the house convention — see `d("filter.cutoff", 0.3) * 5000` at 7976).
### 0.6 The frozen interface contract
Agent A implements exactly this; Agent B calls exactly this and nothing else. Neither agent changes a signature without flagging it as an open question.
```js
// Fire a drum. name ∈ "kick"|"snare"|"clap"|"hat"|"ohat". vel 0..1 (default 0.8).
// Wakes the audio graph if it isn't running (same pattern as pluck()).
Synth.drum(name, vel)
// Kit parameters, all normalized 0..1 (pan: 0=hard left, 0.5=center, 1=hard right).
Synth.drumParam(name, param, value) // set
Synth.drumParam(name, param) // get → number
// Ordered knob metadata for the panel, e.g. drumParamList("kick") →
// [{key:"tune",label:"tune"},{key:"punch",label:"punch"},{key:"decay",label:"decay"},
// {key:"level",label:"level"},{key:"pan",label:"pan"}]
Synth.drumParamList(name)
// Whole-kit snapshot for persistence: plain JSON-safe object of {voice:{param:0..1}}.
Synth.drumKit() // → deep copy
Synth.drumKitLoad(obj) // merge a snapshot back in (missing keys keep defaults)
```
**Voice/param schema** (A implements; B renders from `drumParamList`, never hardcodes):
| voice | params (in order) | defaults |
|---|---|---|
| kick | tune, punch, decay, duck, level, pan | .5, .5, .5, .35, .85, .5 |
| snare | tune, snap, decay, level, pan | .5, .6, .4, .7, .5 |
| clap | tone, decay, level, pan | .5, .5, .65, .5 |
| hat | tone, decay, level, pan | .6, .35, .5, .5 |
| ohat | tone, decay, level, pan | .6, .5, .45, .5 |
*(`duck` was added to the kick in Stage 2 (R6) — it dips pad+drone via `duckG` on every kick hit. This table is the current truth.)*
**Seq keys** for drum lanes: `"drum.kick"`, `"drum.snare"`, `"drum.clap"`, `"drum.hat"`, `"drum.ohat"` (Agent B's territory; named here so both sides agree).
**Merge-safety guard (Agent B):** every call into the new API must be guarded — `if (Synth.drum) Synth.drum("kick", v);` — so branch B runs standalone (silent drums) before the merge.
### 0.7 Running & verifying locally
From your worktree, the quickest loop is the static server (the app boots without the Python hub; world feeds just stay silent, which is fine for drums):
```sh
python3 -m http.server 8901 --directory viz # A uses 8901, B uses 8902
# open http://localhost:8901 in a browser
```
If you need the full hub (you shouldn't for Stage 1), the main checkout's venv works: `/Users/m3ultra/Documents/godstrument/.venv/bin/python run.py --no-browser --profile minimal` (serves on 8088 — don't run it from both worktrees at once).
After edits, sanity-check you didn't break the file: load the page, open devtools, confirm **zero console errors** on boot, click `♪`, confirm the world still sounds, press `T` and `O`, confirm the arranger and OMNI still open. That regression check is mandatory for both agents.
### 0.8 Your report (end your run with exactly this)
1. **Branch** name + worktree path + commit hash(es).
2. **What was built** — bullets, with decisions you made and why (especially anywhere the brief left room).
3. **The diff**`git diff main --stat` plus the full diff (paste it; it will be reviewed line by line).
4. **How you verified** — exact commands/clicks, what you heard/saw, plus the §0.7 regression check results.
5. **Deviations & open questions** — anything you changed vs. this brief, anything you're unsure about, anything you want the reviewer to look hard at.
6. **Territory confirmation** — state plainly that you touched nothing outside your region (or explain precisely what and why).
---
## Part A — Agent A: the GODRUM engine
**Territory:** inside the `Synth` IIFE only (viz/index.html lines 77948180).
**Goal:** five synthesized drum voices + the frozen API of §0.6. No UI, no sequencer wiring, no persistence — that's B and later stages.
### A.1 Kit state
Near the top of the IIFE (by the other `let`s around 77967799), add a `KIT` object holding the normalized params of §0.6 with the listed defaults, plus a `DRUM_DEFS` structure that gives each voice its ordered param list (this backs `drumParamList`). Keep it data-driven — the panel renders whatever you declare.
### A.2 Audio routing
In `build()` (7817+), alongside the other buses, create:
- `drumBus` = GainNode, gain ~0.8, connected to `preSat` (so drums ride the same saturation → crush → chorus/reverb/echo world as every other voice — that's the instrument's ethos, keep it).
- Per voice: a persistent `gain` (level) → `StereoPanner` (pan) → `drumBus`. Store them on `N` (e.g. `N.drums.kick = {g, pan}`) like the other nodes. Individual hits are one-shot node graphs (like `pluck()` / `noiseHit()`) that connect into their voice's gain node.
- In `params()` or in the `drumParam` setter (your choice — setter is simpler and cheaper), keep node values in sync with `KIT` via `setTargetAtTime` with a short time-constant. Level maps 0..1 → gain 0..1.2; pan maps 0..1 → -1..1.
### A.3 Voice recipes
All velocities scale amplitude (`vel*vel` feels better than linear for drums — your call, note it). All ramps: remember `exponentialRampToValueAtTime` can't hit 0 — use small floors or `setTargetAtTime`, matching the `hit()` / `pluck()` idioms.
**kick** — sine osc. Base freq `fB = 35 + tune*45` Hz. Pitch envelope: start at `fB * (2 + punch*6)`, exponential-ramp down to `fB` over ~45 ms. Amp: linear ramp 0 → peak in ~3 ms, then `setTargetAtTime(0, t+0.01, τ)` with `τ = 0.05 + decay*0.30`. Add a tiny click for punch: a 2 ms noise burst, highpassed ~1 kHz, level `vel*punch*0.3`. Stop the osc after ~1.2 s.
**snare** — two layers, mixed by `snap` (0 = all tone, 1 = all noise):
- tone: two oscs (sine or triangle) at `f` and `f*1.5`, `f = 140 + tune*120` Hz, fixed short decay ~80 ms;
- noise: buffer noise → bandpass ~1.8 kHz (Q ≈ 0.8) → highpass 400 Hz, decay `τ = 0.05 + decay*0.30`.
**clap** — buffer noise → bandpass centered `700 + tone*1500` Hz (Q ≈ 1.5). Envelope = the classic 909 spikes: three short bursts at ≈ 0/11/23 ms (each with ±2 ms random jitter per hit, so claps breathe) then the main body at ~30 ms with tail `τ = 0.04 + decay*0.25`. One noise source + a stepped gain envelope (setValueAtTime spikes + setTargetAtTime tail) is the cheap clean way.
**hat (closed)** — follow the `noiseHit()` idiom (7965): noise → highpass `5500 + tone*4000` Hz, decay `τ = 0.012 + decay*0.06`. Short and dry.
**ohat (open)** — same topology, decay `τ = 0.10 + decay*0.5`.
**Choke:** track the currently-ringing ohat's envelope gain; when `drum("hat")` OR a new `drum("ohat")` fires, kill the previous ohat with `setTargetAtTime(0, t, 0.012)`. Closed chokes open — the TR rule.
**Relative levels** at default params and vel 0.8 should sit right in the existing mix: kick clearly audible under the pad wall, hats below the existing `perc.density` hat. Tune by ear against the factory world (press `♪`, set a mood, fire drums from console).
### A.4 `drum()` entry point
Mirror `pluck()`'s wake-up exactly (81168122): bail if no `AC`; lazily create `ctx`/`build()`/`loadWorklets()`; `ctx.resume()`; if `!on`, bring `N.out.gain` up so a drum can be the first sound the page makes. Unknown `name` → return silently. Clamp `vel` 0..1.
### A.5 Export
Extend the return object (81758179) with `drum`, `drumParam`, `drumParamList`, `drumKit`, `drumKitLoad`. Keep the existing style — compact, one object literal.
### A.6 Verify
`Synth` isn't on `window`, so for console testing add a TEMPORARY line `window._gsSynth = Synth;` — and **remove it before your final commit** (checklist item; state in your report that it's gone). Then from devtools:
```js
_gsSynth.drum("kick"); _gsSynth.drum("snare",1); _gsSynth.drum("clap",0.6);
_gsSynth.drum("ohat"); setTimeout(()=>_gsSynth.drum("hat"), 300); // choke audible?
// four-on-the-floor sanity loop:
let i=0; const h=setInterval(()=>{ _gsSynth.drum("kick"); if(i%2)_gsSynth.drum("hat"); if(i%4===2)_gsSynth.drum("snare"); if(++i>=16)clearInterval(h); }, 150);
_gsSynth.drumParam("kick","tune",0.9); _gsSynth.drum("kick"); // hear the change
JSON.stringify(_gsSynth.drumKit()); // JSON-safe?
_gsSynth.drumKitLoad({kick:{tune:0.1}}); _gsSynth.drumParam("kick","tune"); // → 0.1
```
Check: drums audible with `♪` OFF (graph wakes), drums sit in the mix with `♪` ON, choke works, params audibly change the sound, kit round-trips, §0.7 regression passes.
---
## Part B — Agent B: drum lanes + the 🥁 beat panel
**Territory:** anywhere in viz/index.html EXCEPT inside the `Synth` IIFE (77948180).
**Goal:** drum seq lanes fired from `seqTick()`, a beat panel to program them, kit persistence. Every `Synth.drum*` call guarded (`Synth.drum && ...`) so your branch runs standalone before merge.
**Before writing anything, read** `openArranger()` (1729+) and especially its grooves tab (1783+), the seq helpers (14881565), `seqTick()` (24132443), the launcher buttons (23992411), `ctxItem` usage (65536631), and the `#arrbtn`/`#omnibtn`/`#arranger` CSS in the `<style>` block. Your panel should look and feel like it grew here.
### B.1 Drum lanes in the sequencer
- Add a `drumSeqFor(key)` helper next to `seqFor()` (1556): identical shape but `steps` fill **0** (a fresh drum lane is EMPTY — `seqFor`'s gate default of all-1s would be an instant four-on-everything). Steps are 0/1 gates.
- In `seqTick()` (2413), the existing loop does `const n = dests.get(k); if (n && n.isNote) {...}` — drum keys aren't in `dests`, so add a branch **before** that lookup:
```js
if (k.slice(0, 5) === "drum.") {
// gate + chance + ratchet + velocity, then Synth.drum — swing came free from the clock
...
continue;
}
```
Reuse the existing idioms exactly: `chance[cur]` roll, ratchet via `sub = (60000/bpm/4)/rat` with `setTimeout` sub-hits, `vel[cur]` plus the `groove.velo` velocity-deviation line (24302432). Voice name = `k.slice(5)`. Guard: `if (Synth.drum) Synth.drum(name, vel);`
- **MIDI out** (cheap, in-idiom win): alongside the synth hit, send GM drums via the existing `midiPluck(ch, note, vel, durMs)` on **channel 9**: kick 36, snare 38, clap 39, hat 42, ohat 46. Follow how 2434 computes duration.
- Give the beat panel its own repaint hook mirroring `arrGrooveRefresh` (declared 1722, called at 2442): declare `beatRefresh`, call it at the end of `seqTick()`.
### B.2 The 🥁 beat panel
A new DOM panel, id `#beat`, opened by:
- a launcher button `🥁 beat` (`#beatbtn`), sibling of `#arrbtn`/`#omnibtn` (23992411) with matching CSS;
- keyboard `B` (add beside the `T` handler ~6361 — `B` is verified free);
- a `ctxItem("🥁 beat — make a beat", () => openBeat(), "B")` in the sky menu near the 🎛 tracks entry (6566).
Layout, top to bottom (model the DOM/CSS on `openArranger`; dark translucent, resizable is nice-to-have, remember state like the arranger does if cheap):
1. **Header:** `🥁 beat` · master **▶/■** toggle (flips `on` for all five `drum.*` seqs at once) · BPM readout bound to `godtime.bpm` (editable number or drag, clamp 40180, respect `godtime.locked`) · close ✕.
2. **Groove row:** the four `GROOVE_PRESETS` as one-click buttons (mirror 18451848) + swing / humanize / amount sliders bound to the existing global `groove` object. This is the same shared groove the arranger edits — that's correct, not a bug; label it "the feel (shared with tracks)".
3. **The grid:** five lanes (kick / snare / clap / hat / ohat) × 16 steps. Click toggles a step. Playhead column follows `seqCurStep` via your `beatRefresh`. Beat-1/5/9/13 columns visually marked (the arranger grid will show you the house pattern). Per-lane left-edge controls: name, mute (toggles that seq's `on`), and a ⬢ Euclid button (prompt for pulses/rotate or reuse the grooves-tab euclid row idiom; call `euclidFill(sq.steps, pulses, 16, rot, false)`).
4. **Expression lane** (one shared strip below the grid, arranger-style): selector for **velocity / chance / ratchet**, editing the selected lane's per-step arrays by click-drag — copy the grooves-tab interaction (1783+) rather than inventing one.
5. **Kit drawer** (collapsible row): for each voice, render knobs/sliders from `Synth.drumParamList(name)` (guard its absence: render nothing pre-merge), read/write via `Synth.drumParam`. Simple styled range inputs in the house look are fine — don't rebuild OMNI's canvas knobs.
**First-open seeding** (arranger precedent: "pre-seeded so it makes music immediately"): if no `drum.*` seqs exist, seed — kick steps 0, 7, 10 · snare 4, 12 · hat all 16 with vel alternating 0.8/0.4 · ohat step 14 · clap empty. All lanes `on: false`; the user's first ▶ press starts the beat. Then a tap on ▶ must produce a beat with zero further setup.
### B.3 Persistence
- Drum patterns: free (they live in `seqs`; `migrateSeq` at 2560 passes any 16-step seq through — verify once by eye, note it in your report).
- Kit: in `serializePatch()` (2621) add `kit: (Synth.drumKit ? Synth.drumKit() : undefined),` and in `loadPatch()` (2634) add `if (p.kit && Synth.drumKitLoad) Synth.drumKitLoad(p.kit);` plus a panel rebuild if `#beat` is open (mirror how loadPatch reopens the arranger at 2653).
- Extend `gsGrooveSelfCheck()` (2569) with one drum check: `migrateSeq` round-trips a gate seq with a 0-filled steps array (fresh drum lane shape) unchanged.
### B.4 Verify
Static-serve (§0.7, port 8902). Pre-merge, `Synth.drum` is undefined — that's expected; you're verifying UI + sequencing logic:
- open panel via button, `B` key, and sky menu; seeded pattern visible; ▶ toggles lanes; playhead sweeps in time and swings when you pick "mpc 8-4";
- steps toggle; velocity/chance/ratchet edit and visibly persist; Euclid fills fill; mute mutes;
- with an IAC/virtual MIDI out selected (if available — otherwise note as untested), channel-9 notes appear;
- save/load round-trip: run `serializePatch()``loadPatch(that)` from console (find how the console can reach them, or verify via the account-less localStorage paths like zero-mode's `gs_prezero`); drum lanes survive;
- §0.7 regression: no console errors, `♪`/`T`/`O` all still work, the arranger's grooves tab still edits normally (you share `groove` and `seqs` with it — don't break it).
---
## Stage roadmap
| stage | agents | scope | status |
|---|---|---|---|
| 1 | A ∥ B | engine + panel, on separate branches | ✅ APPROVED |
| 2 | C | merge, R1R7, audible verify, mix pass, kick-duck | ✅ APPROVED @ fa5aa43 |
| 2.1 | C | hotfix R8R11 (R8 shipped as `cancelAndHoldAtTime` — reviewer-endorsed deviation) | ✅ APPROVED @ b0fad08 |
| 3 | D ∥ E | D: **AMPLER** @ d2ddec1 · E: performance @ bd3340d | ✅ both APPROVED — one known merge conflict, resolution verified (see Stage 4) |
| 4 | F | merge D+E, cross-feature QA, grimoire chapter, `build_manual.py`, deploy checklist | ✅ APPROVED — merge `9bfa0ee` + docs `5314090` |
**BUILD COMPLETE.** `godrum/integrate @ 5314090` holds the whole feature (9 commits, +809 app / +106 manual). Remaining: the human by-ear pass, then merge to `main`, then deploy per F.4 — human-gated.
Backlog (noted, deliberately not scheduled): duck on the echo return · pan/level as world-modulatable matrix dests · metallic osc-bank hats · MIDI ch-9 device-verification on real hardware · GODSPEAK.
Briefs for stage 4 are written by the planning model after reviewing Stage 3's reports. Do not start future-stage work.
---
# Stage 2 — Agent C: integration, review fixes, and the audible verify
> **You are Agent C (Opus).** Stage 1 is complete: `godrum/a-engine` (commit 28be1de, +132/2) and `godrum/b-beat-panel` (commit 9435c78, +358/0) both passed review. The reviewer has **already trial-merged them: zero conflicts, merged file parses** (`node --check` on the extracted app script). Your job: do the real merge, apply the seven review findings below, then run the end-to-end verification Stage 1 couldn't — the *audible* one. Read Part 0 of this brief first; §0.5 hard rules and §0.8 report format still bind you. **The Stage-1 territory split is lifted** — you're a single agent and may edit anywhere in viz/index.html — but stay surgical.
## C.1 Setup & merge
```sh
cd /Users/m3ultra/Documents/godstrument
git worktree add ../godstrument-C -b godrum/integrate main
cd ../godstrument-C
git merge godrum/a-engine --no-edit && git merge godrum/b-beat-panel --no-edit
```
Confirm the merged app script parses (extract the `<script>` body, `node --check`). **Never touch `main`, never deploy.** Your work stays on `godrum/integrate` for review.
## C.2 Review findings to fix (R1R7)
**R1 — kick tail clipped at max decay (engine).** `fireKick` stops its osc at `t + 1.2`, but max amp τ = 0.35 s → only ~3.4τ elapsed, ~3% residual → possible click at high `decay`. Stop at `t + 0.01 + tau * 8` (compute `tau` once, reuse).
**R2 — the seeded open hat gets choked instantly (panel seed).** `seedBeat` puts closed hats on all 16 steps and the open hat on 14 — so the very next closed hat (step 15) chokes it after one 16th. Clear seed steps 14 and 15 on the closed-hat lane (`H.steps[14] = 0; H.steps[15] = 0`) so the open hat breathes into the downbeat — the classic move.
**R3 — stale paint (panel, cosmetic).** (a) The expression lane's `lit` bars don't repaint when grid steps are toggled or euclid-filled — call `paintExpr` from `paintGrid` (store the ref where both can see it). (b) Dragging a groove slider sets `groove.preset = "custom"` but the preset buttons keep their `.on` highlight — repaint them on slider input. Keep both cheap; don't rebuild the whole panel per input event.
**R4 — groove `velocity` slider parity (panel).** The beat panel exposes swing/humanize/amount but not `groove.velo`, which *is* applied to drum velocity. Add the fourth slider (max 1), matching the arranger's grooves tab.
**R5 — moods must not kill the beat (decided by review — implement as stated).** `applyMood` (~line 2458 pre-merge) sets `seqs[k].on = false` for ALL seqs in both the `m.storm` branch and the `m.clearGrooves` branch — switching moods silences the user's playing beat. The mood governs the *world's* voices; the beat belongs to the player. In **both** loops, skip drum lanes: `if (k.slice(0, 5) === "drum.") continue;`. Verify: play the beat → apply "first light" → apply a storm mood → the beat keeps playing throughout.
**R6 — kick-duck: "the kick parts the sea" (engine).** The Jonwayne low-end move, now yours to build:
- Add a `duck` param to the kick's `DRUM_DEFS` entry (order: `tune, punch, decay, duck, level, pan`; default **0.35**). Because the panel renders from `drumParamList` and persistence flows through `drumKit`, the knob and its save/load come free — verify they do.
- In `build()`: create `duckG` (gain 1). Reroute **padAmp** and **droneAmp** through it (`padAmp → duckG → preSat`, same for drone) instead of straight to `preSat`. Leave the echo return alone for v1 (note it as a possible later extension).
- In `fireKick()`: if `KIT.kick.duck > 0`:
`const dg = N.duckG.gain; dg.cancelScheduledValues(t); dg.setValueAtTime(dg.value, t); dg.linearRampToValueAtTime(1 - KIT.kick.duck * 0.85, t + 0.012); dg.setTargetAtTime(1, t + 0.05, 0.15);`
- Verify by offline render (Agent A's method): render a sustained pad + a kick, measure the pad-band RMS dip and recovery.
**R7 — mix pass (engine internals only).** Agent A flagged that the closed hat's default peak (~0.28 into `preSat`) sits *above* the ambient `perc.density` hat (~0.060.13), inverting §A.3's intent. Adjust the **internal peak multipliers** (e.g. hat `0.7`, ohat `0.8` scalars downward) — do **not** change the normalized §0.6 contract defaults. Then balance the whole kit at defaults against the factory world: kick must read clearly under the pad wall, snare/clap sit between kick and hats. Use offline-render measurements plus your ears if audio output exists; document the multipliers you chose and why. Final by-ear judgment is reserved for the human — flag anything you're unsure of.
## C.3 End-to-end verification (the audible pass)
Static-serve your worktree's `viz/` (port 8903). For console access to internals, you may use Agent A's sanctioned pattern — a TEMPORARY `window._gsSynth = Synth` (and, if needed, `window._gsPatch = {serializePatch, loadPatch}`) hook, **removed before your final commit** (grep-verified, stated in your report).
1. **First-sound wake:** fresh load, `♪` OFF → open 🥁 (B key) → press ▶ → the beat *sounds*. No console errors.
2. **Kit drawer, post-merge:** sliders render for all five voices **including the new `duck`**; each audibly/measurably changes its voice (offline-render ZCR/decay deltas are acceptable proof); `▷ hear` fires each voice.
3. **Groove:** mpc 8-4 audibly swings (or measure onset deltas); ratchet 4 = a roll; chance <1 drops hits probabilistically.
4. **Choke** works in the live page (open hat rings; closed hat cuts it).
5. **Persistence round-trip:** with the hook, `loadPatch(JSON.parse(JSON.stringify(serializePatch())))` drum lanes, kit values (set a few non-defaults incl. `duck`), groove all survive; panel rebuilds correctly if open. Also drive the **zero-mode** enter/exit cycle and confirm the beat + kit survive it.
6. **Moods:** R5 verified live (beat plays through mood changes; storm included).
7. **MIDI:** if a virtual out port is available, verify ch-9 notes (0x99, GM 36/38/39/42/46); otherwise code-inspect and say so.
8. **Full §0.7 regression:** zero console errors throughout; `♪` `T` `O` `B` all work; arranger grooves tab edits fine; beatarranger groove stays bidirectional; mutual exclusion of the two drawers works both ways.
## C.4 Deliverables
Commit(s) on `godrum/integrate` (house style: `🥁 godrum: integrate — <what>`). Do NOT merge to main. End with the §0.8 report, plus: the R1R7 fix list each marked done/deviated, your chosen R7 multipliers, and the offline-render measurement table (per voice + duck dip).
---
# Stage 2.1 — Agent C: hotfix R8R11
> **You are Agent C again**, back in your existing worktree (`/Users/m3ultra/Documents/godstrument-C`, branch `godrum/integrate`). An independent 4-lens adversarial review of your integration (16 agents; every finding confirmed by a 3-refuter majority against the actual code) surfaced four genuine defects your behavioral verification couldn't see — same-task timing, hostile input, hostile data, and a zero-mode interplay. Fix exactly these four, verify as specified, commit on the same branch. §0.5 and §0.8 still bind.
**R8 — choke: anchor before you cancel (engine; the same-task noise pop).**
*(CORRECTION, post-execution: the fix specified below was WRONG and Agent C proved it by measurement — `AudioParam.value` only updates at render-quantum boundaries, so read-before-cancel reads the 1.0 default for a never-rendered envelope and still pops. The shipped fix is `cancelAndHoldAtTime` via a `cancelHold()` helper (read-value kept only as legacy fallback), applied to both `choke()` and the duck. The reviewer endorses the deviation; the text below is preserved as history — do not re-implement it.)*
`choke(t)` does `cancelScheduledValues(t)` then `setTargetAtTime(0, t, 0.012)` with no value anchor. When the choked ohat was fired at the SAME `currentTime` (two hat-family hits in one task real under ratchet-roll timers coalescing on main-thread jank), the cancel wipes the just-scheduled envelope *including its `setValueAtTime(0, t)` anchor*; the never-rendered gain falls back to the GainNode default **1.0** and a full-scale looped-noise burst (~+7 dB over a max ohat) decays over 12 ms instead of a silent choke.
Fix **read the value BEFORE cancelling** (read-after-cancel re-creates the bug: with the events wiped, `.value` reads the 1.0 default):
```js
function choke(t) { if (ohatEnv) { const g = ohatEnv.gain, cur = g.value;
g.cancelScheduledValues(t); g.setValueAtTime(cur, t); g.setTargetAtTime(0, t, 0.012); ohatEnv = null; } }
```
(In the same-task case `cur` reads 0 the pre-cancel timeline still holds the 0-anchor so the un-sounded hit stays silent; mid-ring it chokes smoothly from the current level.) Apply the same read-before-cancel ordering to the duckG anchor in `fireKick` for consistency (benign there today, same latent pattern).
Verify: `Synth.drum("ohat"); Synth.drum("ohat")` **synchronously in one task** reproduces the pop deterministically pre-fix offline-render it, show peak the intended ohat peak post-fix, and confirm the normal (different-task) choke still cuts the tail as before.
**R9 — euclid prompt: junk input must not erase the pattern (panel).**
`String(ans).split(/[ ,]+/)` on `" 4"` or `",4"` yields a leading empty token `parseInt("")` = NaN `|| 0` 0 pulses `euclidFill` zeroes all 16 steps: a mistyped confirm destroys the lane. Fix: trim first, split on `/[\s,]+/`, use `Number(...)`, and **bail (no-op) unless `Number.isFinite(parts[0])`**. An explicit `"0"` still clears (that's a legitimate request); junk does nothing. Verify: `" 4"` E(4,16) with rot 0; `"abc"` / `""` pattern untouched; `"0"` cleared; `"5,2"` E(5,16) rot 2.
**R10 — prototype pollution via kit data (engine; MAJOR — patches and vibes are cross-user data).**
`drumKitLoad` guards voices with truthy `KIT[v]` and params with `p in KIT[v]` both resolve through the prototype chain, so a kit object with a `"__proto__"` key (which `JSON.parse` happily creates as an own property) reaches `KIT[v][p] = number` and writes onto `Object.prototype`, corrupting the page runtime. `drumParam` has the identical gap (`const kv = KIT[name]`), and `drumSync` would throw on `dv.g.gain`. Fix: own-property checks everywhere a caller-supplied name indexes `KIT` or `N.drums` e.g. `const own = (o, k) => Object.prototype.hasOwnProperty.call(o, k);` then `if (!own(KIT, name)) return;` / `if (!own(KIT[v], p)) continue;` (also in `drum()`'s name check and `drumParamList`). Verify: `drumKitLoad(JSON.parse('{"__proto__":{"toString":0.5}}'))` and `drumParam("__proto__","toString",0.5)` leave `Object.prototype` untouched (`({}).toString` still a function) and return/no-op cleanly; a normal kit round-trip still works.
**R11 — a beat built in zero mode must count as a build (app scope).**
`zeroHasBuild()` checks routes/orbs/tweaks only. A player who enters zero and builds *only a beat* exits via a mood no keep/weave card `finishZeroExit("old", …)` `loadPatch(gs_prezero)` wipes their playing beat silently. Fix: hoist the canonical seed pattern into one shared structure (so `seedBeat` and this check can't drift), and extend `zeroHasBuild()` to also return true when any `drum.*` lane is **on**, or any lane's step gates **differ from the canonical seed** (an untouched, never-played seed is not a build don't nag someone who merely opened the panel). Verify: in zero (a) open panel, do nothing, exit no card (as today); (b) draw one step OR press ▶, exit via a mood the keep/weave card appears and "keep" preserves the beat; (c) the normal non-zero flow unchanged.
**Deliverables:** one commit on `godrum/integrate` (`🥁 godrum: 2.1 choke anchor, euclid guard, proto-pollution, zero-mode beat`), §0.8 report with each R marked done + the R8 offline measurement + R10 negative tests. Temporary hooks removed, grep-verified. No merge to main, no deploy.
---
# Stage 3 — Agents D ∥ E: AMPLER and the performance layer
> Stage 2 is APPROVED and the Stage 2.1 hotfix (R8R11) must already be on `godrum/integrate` before you branch — confirm its commit is present (`git log --oneline godrum/integrate` should show the 2.1 commit above fa5aa43). Stage 3 runs **two parallel lanes again**, with a territory split like Stage 1. Both agents: read Part 0 of this brief first — §0.5 hard rules and §0.8 report format still bind you. Branch **from `godrum/integrate`** (or from `main` if GODRUM has already landed there when you start):
>
> ```sh
> cd /Users/m3ultra/Documents/godstrument
> # Agent D:
> git worktree add ../godstrument-D -b godrum/d-ampler godrum/integrate
> # Agent E:
> git worktree add ../godstrument-E -b godrum/e-performance godrum/integrate
> ```
>
> Line numbers have shifted since Stage 1 — the Synth IIFE now spans roughly 81508690. **Grep for names, don't trust raw line numbers.**
## Territory (the merge-safety mechanism — hold to it)
- **Agent D owns:** (1) the inside of the `Synth` IIFE; (2) a NEW self-contained AMPLER UI block (its own functions, inserted directly AFTER the `openBeat` function's closing brace); (3) ONE `ctxItem` line placed directly after the GODSONIQ entry in the sky menu; (4) a NEW `#ampler` CSS block placed directly after the `#beatbtn` rules. D must NOT touch `openBeat`/`seedBeat`/`seqTick`/the keydown handler/`exportMid`/`serializePatch`.
- **Agent E owns:** `openBeat` (header additions), the keydown handler, `seqTick`, `exportMid`. E must NOT touch the Synth IIFE, must add no ctx-menu entries, and should reuse existing CSS classes (if a new rule is truly needed, add it inside the existing `#beat` block).
- Every `Synth.ampler*` call in UI code is D's own (inside D's block); E never calls the AMPLER API in this stage.
## The frozen AMPLER contract (D implements; the panel may consume it in Stage 4)
```js
Synth.amplerArm(secs) // → bool. Capture `secs` (default 3) of the live master mix.
// Reuses the Recorder worklet; wakes/starts the graph like godsoniq().
Synth.amplerStatus() // → {has, secs, slices}
Synth.amplerChop(n) // → bool. Slice the capture into n equal slices (8 or 16).
Synth.amplerPlay(idx, vel) // audition slice idx (vel default 0.9)
Synth.amplerReverse(idx, on) // toggle (on omitted) or set; → new boolean state
Synth.amplerAssign(voice, idx) // voice ∈ kick|snare|clap|hat|ohat — that drum now PLAYS this slice
Synth.amplerClear(voice) // back to the synth recipe
Synth.amplerMap() // → {voice: {slice, reverse}} — session-only, NOT persisted in v1
```
## Part D — Agent D: 🌾 AMPLER — "there is ample, for god is abundant"
The SP-404 move, Godstrument-style: GODSONIQ already captures ~3s of the live master mix (which can itself be a YouTube tab via the tab feed) AMPLER chops that capture into slices and seats them **onto the existing drum voices**, so the beat panel's lanes suddenly play the world instead of synthesis. Sample anything playing on the machine, chop it, and it's a kit.
**D.1 Engine (inside the Synth IIFE):**
- **Capture without hijacking GODSONIQ.** The Recorder worklet's `port.onmessage` currently always lands in `sampleBuf`/`sampleMode` (the keys-replay path). Add a routing flag (e.g. `recTarget = "godsoniq" | "ampler"`) set by whoever armed last; AMPLER captures land in a separate `amplerBuf` and must NOT set `sampleMode` or touch `sampleBuf`. GODSONIQ's user-visible behavior stays byte-identical regression-test it (arm GODSONIQ, keys replay the capture; arm AMPLER, keys do NOT change).
- **Slices** = equal divisions of `amplerBuf`. Reverse = per-slice reversed copy, computed lazily on first toggle and cached.
- **Assigned playback:** in `drum()`, after clamping `vel`, check the assignment map first: if the voice has a slice, play the (possibly reversed) buffer segment through that voice's existing `N.drums[voice].g → pan → drumBus` chain (so kit `level`/`pan` knobs and patch persistence of those knobs keep working), gain scaled `vel*vel`, playbackRate 1.0 (no pitching in v1). THEN:
- a slice-assigned **kick still runs the duck block** (the duck is a property of the kick hit, not of the synth recipe factor it so both paths share it);
- a slice-assigned **ohat still registers its envelope gain as `ohatEnv`**, and hat/ohat (synth OR slice) still calls `choke()` the TR rule is source-agnostic.
- Export the eight `ampler*` methods on the Synth return object, exactly per the contract.
**D.2 UI (D's own new block):**
- `ctxItem("🌾 AMPLER — there is ample (sample the world into the kit)", () => openAmpler())` directly after the GODSONIQ entry.
- `#ampler` panel: fixed, RIGHT side of the screen (the bottom is the beat/arranger drawers' turf), styled with the same palette/idioms as `#beat`. Contents: title · ** capture** button with a small length select (1.5 / 3 / 6 s) · a waveform canvas of the capture (simple min/max column render) · chop select (8 / 16) · the slice grid tap a slice to audition, a per-slice **⇄** toggles reverse (badge it visibly) · an assignment strip showing the live map per voice (e.g. `kick ← s3ʳ`) with per-voice **assign** (pick a slice) and ** clear**. Exact interaction pattern is your call keep it house-style, no drag-and-drop needed.
- Empty states matter: no capture yet the button and a one-line hint; captured but unchopped waveform + chop.
**D.3 Verify** 0.7 regression mandatory; temporary hooks allowed, removed + grep-verified):
live capture of the sounding world chop 16 audition (measure RMS per slice) reverse a slice (verify sample-order inversion) assign to snare the snare lane plays the slice through level/pan (knobs still act) assigned kick still ducks (measure `duckG` dip) slice-ohat still choked by hat GODSONIQ unbroken (its capture still lands on the keys) tab feed unbroken zero console errors.
## Part E — Agent E: the performance layer
Jonwayne taps a live hi-hat pattern into a quiet pocket that's this lane: play the kit from the keyboard, record the taps into the grid, and let the beat leave the building as MIDI.
**E.1 Finger drumming.** While the beat panel is open, number keys **15** fire kick/snare/clap/hat/ohat (`vel` 0.9; with Shift held, 0.5 the ghost note). FIRST verify 15 are genuinely free in the keydown handler and anywhere else keys are consumed (mind the INPUT-focus guard; if they collide with something, choose replacements and flag it in your report). Extract the guarded fire logic (`Synth.drum` + `midiPluck(9, gm, …)`) from `seqTick`'s drum branch into one shared app-scope helper e.g. `fireDrum(name, vel, durMs)` used by both `seqTick` and the keys, so the two paths can't drift. (`seqTick` is your territory; this refactor is in-bounds. Behavior must stay identical same vel deviation, same MIDI.)
**E.2 Live record.** A ** rec** toggle in the beat panel header (state on `beat.rec`; red when armed, `.midibtn` styling). While armed, each finger-drum hit quantizes to the NEAREST 16th round `seqPhase` (fraction > .5 → the upcoming step, else the current one), modulo 16 — and writes that lane's `steps[n] = 1`, `vel[n] =` the hit velocity, then repaints via the existing `beatRefresh`. Recording works with the transport playing (overdub into the loop) and also silent (step-programming by touch). Panel hint text: `keys 15 play the kit · ⏺ rec taps them into the grid`.
**E.3 Drums in the MIDI export.** Study `exportMid()` and add a channel-9 drum track to the SMF: for each bar of the arrangement's length, every lit drum step becomes a GM note (36/38/39/42/46), one 16th long, velocity from `vel[]`. Ratchet sub-hits may be omitted in v1 — say so in your report. Verify by parsing the generated bytes (locate the `0x99` events and check their tick positions), not by eyeballing.
**E.4 Verify** (§0.7 regression mandatory): keydown logic (note: synthetic keys don't route to hidden tabs — verify via direct handler-function calls plus one real foreground keypress if you can, and document the env caveat honestly, as Agent C did) · record quantization at fraction boundaries (test both sides of .5 by driving `seqPhase`) · the shared `fireDrum` helper produces identical seqTick behavior (chance/ratchet/vel-deviation unchanged) · exportMid byte-verified · zero console errors.
## Reports
Per §0.8, one each. Merge order for Stage 4 will be D then E (or either — territories are disjoint; the reviewer trial-merges as before). Nothing touches `main`, nothing deploys.
---
# Stage 4 — Agent F: final integration, the grimoire chapter, and QA
> **You are Agent F (Opus).** All build lanes are done and approved: `godrum/integrate` @ `b0fad08` (Stages 12.1), `godrum/d-ampler` @ `d2ddec1` (🌾 AMPLER), `godrum/e-performance` @ `bd3340d` (performance layer). You close the feature: merge, cross-feature QA, documentation, deploy prep. Read Part 0 first — §0.5 hard rules and §0.8 report format still bind, with ONE amendment: **in this stage you MUST regenerate and commit `viz/manual.html`** (the earlier "never commit manual.html" rule existed to keep mid-feature noise out; Stage 4 is exactly when it updates). Deploying itself remains forbidden — that happens after the human review.
## F.1 Merge (one known conflict, resolution pre-verified)
```sh
cd /Users/m3ultra/Documents/godstrument
git worktree add ../godstrument-F godrum/integrate
cd ../godstrument-F
git merge godrum/d-ampler --no-edit # clean
git merge godrum/e-performance --no-edit # CONFLICTS — expected, see below
```
The E merge conflicts in exactly one region: **both D and E inserted a block at the seam between `openBeat`'s closing brace and `seqTick`** (D: the AMPLER UI block; E: the `fireDrum` helper). The reviewer has already trial-merged and verified the resolution: **keep BOTH sides** — AMPLER block first, then `fireDrum` — delete only the conflict markers. Nothing else conflicts. After resolving: extract the `<script>` body, `node --check`, commit the merge. If you find any OTHER conflict, stop and flag it in your report instead of improvising.
## F.2 Cross-feature QA (the interactions no single lane could test)
Static-serve and drive the real page (temporary hooks allowed, removed + grep-verified). The novel surface is where D's and E's features meet each other and the older stages:
1. **Slice + finger drumming:** assign an AMPLER slice to the kick → press key `1` → the slice plays (E's `fireDrum``Synth.drum` → D's slice branch). With ⏺ rec armed, the tap records into the grid and playback then fires the slice.
2. **Slice + sequencer + duck/choke:** seeded beat playing with a slice-kick → duck still dips (measure `duckG`); slice-ohat still choked by the closed hat.
3. **Slice + kit knobs:** a slice-assigned voice's `level`/`pan` knobs still act; `duck` knob still governs the slice-kick's dip.
4. **exportMid with slices assigned:** the ch-9 export is unchanged by assignments (GM notes, not audio) — confirm no throw and byte-sanity (0x99 events present).
5. **Panels:** `#ampler` (right side) coexists with BOTH bottom drawers; beat↔arranger mutual exclusion unchanged; `♪ T O B` regression; AMPLER open + beat open simultaneously works.
6. **GODSONIQ + AMPLER together:** GODSONIQ capture then AMPLER capture then GODSONIQ again — keys-replay and `amplerBuf` stay independent throughout.
7. **Persistence:** patch save/load with slices assigned — kit + drum lanes round-trip; AMPLER map is session-only (expected: assignments do NOT survive a reload — confirm it degrades to synth, no throw).
8. **Moods/zero:** mood change mid-beat with a slice-kick (beat survives, slice keeps playing); zero-mode enter/exit with a drawn beat (keep/weave card appears, beat survives "keep").
9. **Full console-error sweep** across all of the above: zero errors.
## F.3 The grimoire chapter (canon) + manual
Write the GODRUM chapter into the grimoire prose in `viz/index.html` (the `#grimoire` content region, ~lines 5381200 — find the groove-grids/tracks entries and add alongside them, matching the house voice: poetic, precise, second person, no marketing). Cover, briefly but completely:
- **🥁 the beat** — press `B` (or the 🥁 button): five drum lanes on the godtime clock; click to draw; ▶ starts them; velocity/chance/ratchet lanes; ⬢ euclid; the feel shared with tracks; the kit drawer (tune/punch/decay/**duck** — "the kick parts the sea" — level/pan); beats save with your patch; moods never silence your beat; a beat built in zero counts as a build.
- **keys 15 & ⏺ rec** — finger-drum the kit (⇧ for ghosts); armed rec taps into the grid, quantized to the nearest 16th.
- **🌾 AMPLER** — "there is ample, for god is abundant": right-click the sky → AMPLER; ⏺ capture the sounding world (the tab feed counts), ✂ chop into 8/16, tap to audition, ⇄ to reverse, seat slices onto the drum voices — the beat plays the world. Session-only: slices don't save with the patch (yet).
- **the beat leaves the building** — drum lanes ride MIDI out on channel 10 (GM notes) and land in the .mid export.
Then `python3 build_manual.py` and **commit the regenerated `viz/manual.html`** together with the grimoire edit. `GODSTRUMENT_MANUAL_SOURCE.md` stays untouched (it drifts; viz/index.html is truth).
## F.4 Deploy checklist (prepare, do NOT execute)
End your report with a checklist the humans will run after the by-ear pass: merge `godrum/integrate``main` · rsync per CLAUDE.md (git-tracked minus dev docs — note `GODRUM_BRIEF.md` is already excluded by the `.*_BRIEF\.md` filter) · `sudo -n systemctl restart godstrument` · cold-start wait · smoke-test godstrument.pro (B key, ▶, a capture) · manual.html live. Do not run any of it.
## F.5 Deliverables
Commits on `godrum/integrate` (merge commit(s) + one docs commit). §0.8 report + the F.2 matrix each ✓/✗ + anything you had to decide. Nothing to `main`, nothing deployed.