From bd3340d4a25767503a79f48e7baddc880bc287ed Mon Sep 17 00:00:00 2001 From: monsterrobotparty Date: Mon, 13 Jul 2026 02:10:01 +1000 Subject: [PATCH] =?UTF-8?q?=F0=9F=A5=81=20godrum:=20finger-drumming=20keys?= =?UTF-8?q?,=20live=20record,=20drums=20in=20the=20MIDI=20export?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stage 3 / Agent E — the performance layer. - fireDrum(name,vel,durMs): one shared fire path for the sequenced beat and the played keys; seqTick's drum branch routed through it (behavior identical). - Keys 1–5 play kick/snare/clap/hat/ohat while the beat panel is open (vel 0.9, ⇧ = 0.5 ghost); ev.code with an ev.key fallback for code-less environments. - ⏺ rec toggle (beat.rec, red when armed): each tap quantizes to the nearest 16th (round seqPhase % 16) and writes steps[n]/vel[n], repaint via beatRefresh. - exportMid: a channel-9 GM drum track (36/38/39/42/46), the beat looped across the arrangement's bars, one 16th long; ratchets omitted in v1. Added before the bail check so a beat-only export still produces a file. Territory: openBeat header, keydown, seqTick, exportMid + one #beat CSS rule. Nothing inside the Synth IIFE; no ctx-menu entries. Co-Authored-By: Claude Opus 4.8 --- viz/index.html | 66 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/viz/index.html b/viz/index.html index 58f401d..da83d91 100644 --- a/viz/index.html +++ b/viz/index.html @@ -257,6 +257,7 @@ #beat .midibtn { background: rgba(40,52,74,0.85); color: #b9c8e2; border: 1px solid rgba(120,150,200,0.25); border-radius: 6px; padding: 3px 10px; font-size: 11px; cursor: pointer; } #beat .midibtn.on { background: rgba(120,220,150,0.22); color: #bff0c9; border-color: rgba(130,230,160,0.55); } + #beat .midibtn.rec-on { background: rgba(230,90,90,0.28); color: #ffd2d2; border-color: rgba(240,110,110,0.6); } #beat .bmini { padding: 2px 7px; font-size: 11px; line-height: 1.1; } #beat .grow { gap: 12px; flex-wrap: wrap; align-items: center; margin: 4px 0 2px; } #beat .gctl { display: inline-flex; align-items: center; gap: 5px; } @@ -1769,8 +1770,33 @@ bytes.push(0, 0xff, 0x2f, 0x00); trks.push([bytes]); } + // channel-9 GM drum track — every lit drum step, the beat looped across the arrangement's bars + { + const dev = []; + for (const v of DRUM_VOICES) { + const sq = seqs["drum." + v]; + if (!sq) continue; + const gm = DRUM_GM[v]; + for (let b = 0; b < arr.bars; b++) { + for (let s2 = 0; s2 < 16; s2++) { + if (!sq.steps[s2]) continue; + const dvel = Math.round(clamp(sq.vel ? sq.vel[s2] : 0.8, 0.05, 1) * 127); + const t0 = (b * 16 + s2) * TICKS16, t1 = Math.max(t0 + 1, t0 + TICKS16 - 2); // one 16th long + dev.push([t0, 0x99, gm, dvel], [t1, 0x89, gm, 0]); // ratchet sub-hits omitted in v1 + } + } + } + if (dev.length) { + dev.sort((a, b2) => a[0] - b2[0]); + const bytes = []; + let last = 0; + for (const e of dev) { bytes.push(...midVarlen(e[0] - last), ...e.slice(1)); last = e[0]; } + bytes.push(0, 0xff, 0x2f, 0x00); + trks.push([bytes]); + } + } if (trks.length === 1) { // only the tempo track — nothing to hear yet - eventTicker.unshift({ text: "no note clips in the arrangement yet — place some A/B/C cells on lead or bass first", tAdded: now(), src: "sys" }); + eventTicker.unshift({ text: "nothing to export yet — place A/B/C clips on a note lane, or draw a beat in 🥁 first", tAdded: now(), src: "sys" }); return; } const flat = (t2) => t2.length === 1 ? t2[0] : t2.reduce((a, e) => a.concat(e), []); @@ -1805,7 +1831,7 @@ document.body.appendChild(beatEl); let arrGrooveRefresh = null; // grooves-tab playhead painter let beatRefresh = null; // 🥁 beat-panel playhead painter (mirrors arrGrooveRefresh) - const beat = { open: false, sel: "kick", exprMode: "velocity", kitOpen: false }; + const beat = { open: false, sel: "kick", exprMode: "velocity", kitOpen: false, rec: false }; let arrTab = "clips", arrSelKey = null, arrSelLetter = "A"; function closeArranger() { arr.open = false; arrEl.classList.remove("open"); @@ -2547,6 +2573,11 @@ const play = document.createElement("button"); play.className = "midibtn"; const paintPlay = () => { const p = anyOn(); play.textContent = p ? "■ stop" : "▶ play"; play.classList.toggle("on", p); }; paintPlay(); + const rec = document.createElement("button"); rec.className = "midibtn"; rec.textContent = "⏺ rec"; + rec.title = "record — finger-drum hits (keys 1–5) tap into the grid, quantized to the nearest 16th"; + const paintRec = () => rec.classList.toggle("rec-on", beat.rec); + paintRec(); + rec.onclick = () => { beat.rec = !beat.rec; paintRec(); }; const bpmL = document.createElement("span"); bpmL.className = "glbl"; bpmL.style.minWidth = "auto"; bpmL.textContent = "bpm"; const bpm = document.createElement("input"); bpm.type = "number"; bpm.className = "eunum"; bpm.min = "40"; bpm.max = "180"; bpm.value = String(Math.round(godtime.bpm)); @@ -2556,8 +2587,8 @@ bpm.value = String(Math.round(godtime.bpm)); }; const x2 = document.createElement("span"); x2.className = "aclose"; x2.textContent = "×"; x2.onclick = closeBeat; const hint = document.createElement("span"); hint.className = "albl"; - hint.textContent = "▶ starts all five lanes · click a step to draw · the feel is shared with tracks"; - top.appendChild(title); top.appendChild(play); top.appendChild(bpmL); top.appendChild(bpm); + hint.textContent = "keys 1–5 play the kit · ⏺ rec taps them into the grid · the feel is shared with tracks"; + top.appendChild(title); top.appendChild(play); top.appendChild(rec); top.appendChild(bpmL); top.appendChild(bpm); top.appendChild(hint); top.appendChild(x2); beatEl.appendChild(top); @@ -2739,6 +2770,13 @@ beatEl.classList.add("open"); } + // the one place a drum voice sounds — shared by seqTick AND the finger-drum keys (E.1), + // so the sequenced path and the played path can never drift: same guard, same GM MIDI. + function fireDrum(name, vel, durMs) { + if (Synth.drum) Synth.drum(name, vel); // guarded — silent if the engine isn't present + const gm = DRUM_GM[name]; + if (gm != null) midiPluck(9, gm, vel, durMs); // GM drums out on channel 9 (0x99) + } function seqTick() { for (const k in seqs) { const sq = seqs[k]; @@ -2748,15 +2786,14 @@ if (sq.steps[cur]) { // 0/1 gate const chc = sq.chance ? sq.chance[cur] : 1; // chance: does this step fire? if (chc >= 1 || Math.random() < chc) { - const name = k.slice(5), gm = DRUM_GM[name]; + const name = k.slice(5); const rat = clamp(Math.round((sq.rat && sq.rat[cur]) || 1), 1, 4); // ratchet: sub-hits per step const sub = (60000 / Math.max(40, godtime.bpm) / 4) / rat; // ms between sub-hits const fireHit = () => { let vel = sq.vel ? sq.vel[cur] : 0.8; if (groove.velo) // shared Velocity Deviation (§10.5.11) vel = clamp(vel + (Math.random() * 2 - 1) * groove.velo * groove.amount * 0.5, 0.05, 1); - if (Synth.drum) Synth.drum(name, vel); // guarded — silent until Agent A merges - if (gm != null) midiPluck(9, gm, vel, sub * 0.9); // GM drums out on channel 9 (0x99) + fireDrum(name, vel, sub * 0.9); // the shared fire path (finger-keys use it too) }; fireHit(); for (let h = 1; h < rat; h++) setTimeout(fireHit, h * sub); @@ -6727,6 +6764,21 @@ if (ev.key === "b" || ev.key === "B") { beat.open ? closeBeat() : openBeat(); // 🥁 GODRUM — make a beat } + if (beat.open && !ev.repeat && !ev.metaKey && !ev.ctrlKey && !ev.altKey) { // finger-drum the kit (1–5) + const byCode = ["Digit1", "Digit2", "Digit3", "Digit4", "Digit5"].indexOf(ev.code); // ev.code keeps ⇧+digit mapped + const di = byCode >= 0 ? byCode : "12345".indexOf(ev.key); // …with an ev.key fallback for code-less envs + if (di >= 0) { + const name = DRUM_VOICES[di], vel = ev.shiftKey ? 0.5 : 0.9; // ⇧ = the ghost note + fireDrum(name, vel, 60000 / Math.max(40, godtime.bpm) / 4 * 0.9); + if (beat.rec) { // record: quantize the tap to the nearest 16th, write the step + const step = Math.round(seqPhase) % 16; // frac > .5 rounds to the upcoming 16th (raw grid; swing rides at playback) + const sq = drumSeqFor("drum." + name); + sq.steps[step] = 1; sq.vel[step] = vel; + if (beatRefresh) beatRefresh(); + } + return; + } + } }); // ---------------------------------------------------------------------------