📋 godrum: build brief + deploy-filter update (all *_BRIEF.md excluded)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
5314090e84
commit
0fb6410be7
@ -1,6 +1,6 @@
|
|||||||
# Godstrument — Claude context
|
# Godstrument — Claude context
|
||||||
|
|
||||||
Music-instrument web app; hosted at godstrument.pro on the **botchat** VPS (`humanjing@100.71.119.27`, exposed via Cloudflare Tunnel — NOT dealgod, despite earlier notes). App dir `/home/humanjing/godstrument` (not a git checkout). Deploy = rsync git-tracked files only, **minus dev docs/scripts** (`--files-from=<(git ls-files | grep -vE '^(CLAUDE\.md|README\.md|ZERO_OMNI_BRIEF\.md|LIFESTRUMENT_BRIEF\.md|GODSTRUMENT_MANUAL_SOURCE\.md|godstrument\.txt|build_manual\.py|test_.*\.py)$')`, never `--delete`, never push `godstrument_users.db`/`auth_secret`/`patches/`) then `sudo -n systemctl restart godstrument`. The **user manual** ships as `viz/manual.html` (served at godstrument.pro/manual.html), generated from the in-app grimoire by `python3 build_manual.py` — re-run it after editing the grimoire. Working notes/planning docs stay OUT of the deploy. Origin is Gitea `ssh://git@100.71.119.27:222/monster/Godstrument.git`. See [[godstrument-pro-deploy]].
|
Music-instrument web app; hosted at godstrument.pro on the **botchat** VPS (`humanjing@100.71.119.27`, exposed via Cloudflare Tunnel — NOT dealgod, despite earlier notes). App dir `/home/humanjing/godstrument` (not a git checkout). Deploy = rsync git-tracked files only, **minus dev docs/scripts** (`--files-from=<(git ls-files | grep -vE '^(CLAUDE\.md|README\.md|.*_BRIEF\.md|GODSTRUMENT_MANUAL_SOURCE\.md|godstrument\.txt|build_manual\.py|test_.*\.py)$')`, never `--delete`, never push `godstrument_users.db`/`auth_secret`/`patches/`) then `sudo -n systemctl restart godstrument`. The **user manual** ships as `viz/manual.html` (served at godstrument.pro/manual.html), generated from the in-app grimoire by `python3 build_manual.py` — re-run it after editing the grimoire. Working notes/planning docs stay OUT of the deploy. Origin is Gitea `ssh://git@100.71.119.27:222/monster/Godstrument.git`. See [[godstrument-pro-deploy]].
|
||||||
|
|
||||||
- **Canon gotcha:** the real spec prose lives in `viz/index.html` (grimoire text around line ~950); `GODSTRUMENT_MANUAL_SOURCE.md` claims to be exhaustive but has drifted before (Earth Echo / Schumann layer was missing). When they disagree, **viz/index.html is truth** — update the manual to match, not the other way.
|
- **Canon gotcha:** the real spec prose lives in `viz/index.html` (grimoire text around line ~950); `GODSTRUMENT_MANUAL_SOURCE.md` claims to be exhaustive but has drifted before (Earth Echo / Schumann layer was missing). When they disagree, **viz/index.html is truth** — update the manual to match, not the other way.
|
||||||
- Secrets (Cloudflare token etc.): read from `.env`/local files, never from chat. A CF token was pasted in chat once and rolled — don't repeat that.
|
- Secrets (Cloudflare token etc.): read from `.env`/local files, never from chat. A CF token was pasted in chat once and rolled — don't repeat that.
|
||||||
|
|||||||
462
GODRUM_BRIEF.md
Normal file
462
GODRUM_BRIEF.md
Normal file
@ -0,0 +1,462 @@
|
|||||||
|
# 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 ~425–1200 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 7794–8180**. 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 (4068–4074); 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 2399–2411 (`#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 6553–6631. 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 ~6340–6362): `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 ~425–1200) 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 7794–8180).
|
||||||
|
**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 7796–7799), 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 (8116–8122): 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 (8175–8179) 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 (7794–8180).
|
||||||
|
**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 (1488–1565), `seqTick()` (2413–2443), the launcher buttons (2399–2411), `ctxItem` usage (6553–6631), 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 (2430–2432). 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` (2399–2411) 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 40–180, respect `godtime.locked`) · close ✕.
|
||||||
|
2. **Groove row:** the four `GROOVE_PRESETS` as one-click buttons (mirror 1845–1848) + 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, R1–R7, audible verify, mix pass, kick-duck | ✅ APPROVED @ fa5aa43 |
|
||||||
|
| 2.1 | C | hotfix R8–R11 (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 (R1–R7)
|
||||||
|
|
||||||
|
**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.06–0.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; beat↔arranger 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 R1–R7 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 R8–R11
|
||||||
|
|
||||||
|
> **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 (R8–R11) 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 8150–8690. **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 **1–5** fire kick/snare/clap/hat/ohat (`vel` 0.9; with Shift held, 0.5 — the ghost note). FIRST verify 1–5 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 1–5 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 1–2.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 538–1200 — 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 1–5 & ⏺ 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.
|
||||||
Loading…
Reference in New Issue
Block a user