From 9435c78feb7d09c90515d3759a772a30e132a9a0 Mon Sep 17 00:00:00 2001 From: monsterrobotparty Date: Sun, 12 Jul 2026 21:47:57 +1000 Subject: [PATCH] =?UTF-8?q?=F0=9F=A5=81=20godrum:=20drum=20lanes=20+=20the?= =?UTF-8?q?=20=F0=9F=A5=81=20beat=20panel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stage 1 / Agent B. Drum lanes fire from seqTick under "drum.*" seq keys (gate + chance + ratchet + velocity, inheriting swing/humanize from the groove clock), guarded Synth.drum + GM MIDI on channel 9. New #beat panel modelled on the arranger: master play, shared groove, 5ร—16 grid with playhead, vel/chance/ratchet expression lane, per-lane mute + euclid, guarded kit drawer. Kit rides serializePatch/loadPatch; drum patterns ride seqs. B key + sky menu + launcher button. Nothing touched inside the Synth IIFE. Co-Authored-By: Claude Opus 4.8 --- viz/index.html | 358 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 358 insertions(+) diff --git a/viz/index.html b/viz/index.html index 4dd1edc..063295d 100644 --- a/viz/index.html +++ b/viz/index.html @@ -235,6 +235,67 @@ .seqcell.on { background: #78a0ff; box-shadow: 0 0 6px rgba(120,160,255,0.6); } .seqcell.cur { outline: 1px solid rgba(255,238,150,0.85); } + /* ๐Ÿฅ GODRUM โ€” the beat panel: a sibling of the arranger, sharing its groove & clock */ + #beat { + position: fixed; left: 12px; right: 12px; bottom: 54px; z-index: 13; + background: rgba(10,15,26,0.95); border: 1px solid rgba(120,150,200,0.35); + border-radius: 12px; padding: 8px 10px; display: none; color: #c7d4ea; + font-size: 11px; -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); + max-height: 46vh; + } + #beat.open { display: flex; flex-direction: column; } + #beat .agrip { height: 8px; margin: -8px -10px 2px; cursor: ns-resize; border-radius: 12px 12px 0 0; } + #beat .agrip:hover { background: rgba(150,180,230,0.25); } + #beat .atop { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; flex-wrap: wrap; } + #beat .abody { flex: 1; min-height: 0; overflow-y: auto; } + #beat .arow { display: flex; align-items: center; gap: 3px; margin: 2px 0; } + #beat .albl { color: rgba(150,170,200,0.65); font-size: 10px; flex: 1; } + #beat .aclose { cursor: pointer; color: #8fa4c8; font-size: 16px; padding: 0 4px; } + #beat .asel { background: rgba(30,40,60,0.85); color: #dbe6f5; border: 1px solid rgba(120,150,200,0.3); + border-radius: 6px; padding: 3px 8px; font-size: 11px; } + #beat .btitle { color: #cfe0ff; font-size: 12px; font-weight: 600; flex: 0 0 auto; } + #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 .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; } + #beat .gctl .glbl { color: rgba(160,180,215,0.8); font-size: 10px; min-width: 48px; text-align: right; } + #beat .gctl input[type=range] { width: 74px; accent-color: #7fa0e6; height: 3px; } + #beat .gctl .gval { color: #aebbdc; font-size: 10px; min-width: 30px; } + #beat .glbl { color: rgba(160,180,215,0.8); font-size: 10px; } + #beat .gpreset { font-size: 10px; padding: 2px 7px; opacity: 0.85; } + #beat .eunum { width: 52px; background: rgba(20,26,40,0.8); color: #cfe0f5; + border: 1px solid rgba(120,150,220,0.3); border-radius: 4px; font-size: 11px; padding: 1px 4px; } + #beat .emrow { gap: 6px; align-items: center; margin-top: 6px; } + #beat .blane { display: flex; align-items: center; gap: 4px; margin: 3px 0; } + #beat .blead { display: flex; align-items: center; gap: 4px; width: 104px; min-width: 104px; } + #beat .bname { flex: 1; color: #aebfda; font-size: 10px; text-align: right; overflow: hidden; + white-space: nowrap; text-transform: uppercase; letter-spacing: 0.4px; } + #beat .bsteps { display: grid; grid-template-columns: repeat(16, 26px); gap: 3px; } + #beat .seqcell { width: 26px; height: 20px; border-radius: 4px; } + #beat .exprlane { display: grid; grid-template-columns: repeat(16, 26px); gap: 3px; + height: 46px; align-items: end; margin: 4px 0 8px 108px; } + #beat .exprcell { width: 26px; height: 46px; display: flex; align-items: flex-end; + background: rgba(30,36,54,0.6); border-radius: 4px; cursor: ns-resize; } + #beat .exprcell.bar { background: rgba(40,48,72,0.7); } + #beat .exprbar { width: 100%; background: rgba(120,150,230,0.35); border-radius: 4px; min-height: 2px; } + #beat .exprbar.lit { background: rgba(140,175,255,0.85); } + #beat .bkit { gap: 8px; align-items: flex-start; } + #beat .bvoice { display: inline-flex; flex-direction: column; gap: 2px; padding: 4px 6px; + background: rgba(20,26,40,0.5); border-radius: 6px; } + #beat .bvn { color: #9fb6dc; font-size: 10px; text-align: center; margin-bottom: 1px; + text-transform: uppercase; letter-spacing: 0.4px; } + #beatbtn { + position: fixed; right: 14px; bottom: 128px; z-index: 12; padding: 6px 12px; + border-radius: 9px; background: rgba(20,28,44,0.85); color: #9fb4d8; + border: 1px solid rgba(120,150,200,0.3); cursor: pointer; font-size: 12px; + -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); user-select: none; + } + #beatbtn:hover { color: #dbe8ff; border-color: rgba(150,180,230,0.6); } + #beatbtn.on { color: #cfe0ff; border-color: rgba(150,200,255,0.7); background: rgba(30,42,66,0.9); } + #beatbtn.hidden { display: none; } + /* right-click context menu โ€” every setting, on everything */ #ctxmenu { position: fixed; z-index: 30; display: none; width: 240px; max-height: 72vh; @@ -1563,6 +1624,21 @@ lock: new Array(16).fill(-1), // per-step parameter lock (-1 = unlocked) }); } + // GODRUM drum lanes โ€” same seq shape as seqFor, but steps are 0/1 gates that fill + // EMPTY (a fresh drum lane makes no sound; seqFor's all-1s default would be an + // instant four-on-everything). Fired straight from seqTick, so swing/humanize ride free. + const DRUM_VOICES = ["kick", "snare", "clap", "hat", "ohat"]; + const DRUM_GM = { kick: 36, snare: 38, clap: 39, hat: 42, ohat: 46 }; // General MIDI drum notes (channel 9) + function drumSeqFor(key) { + return seqs[key] || (seqs[key] = { + on: false, + steps: new Array(16).fill(0), // 0/1 gate โ€” empty until you draw a beat + vel: new Array(16).fill(0.8), + chance: new Array(16).fill(1), + rat: new Array(16).fill(1), + lock: new Array(16).fill(-1), // unused by drums, kept for seq-shape parity (migrateSeq) + }); + } // ---- the arranger โ€” tracks & clips for the casuals ----------------------- // Ableton-shaped, not Ableton-sized: tracks are the voices, a clip is a bar // of groove (A/B/C patterns on the existing grids), "live" means the world @@ -1719,7 +1795,14 @@ const arrEl = document.createElement("div"); arrEl.id = "arranger"; document.body.appendChild(arrEl); + + // the ๐Ÿฅ beat panel โ€” GODRUM's drum lanes, a sibling of the arranger + const beatEl = document.createElement("div"); + beatEl.id = "beat"; + 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 }; let arrTab = "clips", arrSelKey = null, arrSelLetter = "A"; function closeArranger() { arr.open = false; arrEl.classList.remove("open"); @@ -1728,6 +1811,7 @@ } function openArranger() { arr.open = true; + if (beat.open) closeBeat(); // the two bottom drawers share the screen โ€” only one at a time arrBtn.classList.add("hidden"); arrEl.innerHTML = ""; @@ -2410,10 +2494,272 @@ omniBtn.onclick = () => OMNI.toggle(); document.body.appendChild(omniBtn); + const beatBtn = document.createElement("div"); // ๐Ÿฅ beat โ€” the GODRUM panel (B), sibling of ๐ŸŽน OMNI + beatBtn.id = "beatbtn"; + beatBtn.textContent = "๐Ÿฅ beat"; + beatBtn.title = "GODRUM โ€” make a beat: drum lanes, groove & kit (B)"; + beatBtn.onclick = () => { beat.open ? closeBeat() : openBeat(); }; + document.body.appendChild(beatBtn); + + // ---- ๐Ÿฅ GODRUM โ€” the beat panel ------------------------------------------ + // Drum lanes live in `seqs` under "drum.*" keys and fire from seqTick, so they + // inherit swing / humanize / chance / ratchet for free. The panel is modelled on + // the arranger's grooves tab; the feel (`groove`) is the SAME shared object the + // arranger edits โ€” one groove for the whole machine. + function seedBeat() { // first open: a pattern so โ–ถ makes music at once + if (DRUM_VOICES.some(v => seqs["drum." + v])) return; + const K = drumSeqFor("drum.kick"), S = drumSeqFor("drum.snare"), + H = drumSeqFor("drum.hat"), O = drumSeqFor("drum.ohat"); + drumSeqFor("drum.clap"); // clap lane exists but stays empty + [0, 7, 10].forEach(i => K.steps[i] = 1); + [4, 12].forEach(i => S.steps[i] = 1); + for (let i = 0; i < 16; i++) { H.steps[i] = 1; H.vel[i] = (i % 2) ? 0.4 : 0.8; } + O.steps[14] = 1; + // all lanes stay on:false โ€” the user's first โ–ถ starts the beat + } + function closeBeat() { + beat.open = false; beatEl.classList.remove("open"); + beatRefresh = null; + beatBtn.classList.remove("hidden"); + } + function openBeat() { + beat.open = true; + if (arr.open) closeArranger(); // the two bottom drawers share the screen โ€” only one at a time + beatBtn.classList.add("hidden"); + seedBeat(); + beatEl.innerHTML = ""; + + // grab-handle โ€” drag to resize, remembered (mirrors the arranger) + const grip = document.createElement("div"); grip.className = "agrip"; grip.title = "drag to resize"; + grip.addEventListener("mousedown", (e0) => { + e0.preventDefault(); + const startY = e0.clientY, startH = beatEl.getBoundingClientRect().height; + const move = (e2) => { beatEl.style.height = clamp(startH + (startY - e2.clientY), 140, window.innerHeight * 0.8) + "px"; }; + const up = () => { window.removeEventListener("mousemove", move); window.removeEventListener("mouseup", up); + try { localStorage.setItem("gs_beath", parseInt(beatEl.style.height) || ""); } catch (e) {} }; + window.addEventListener("mousemove", move); window.addEventListener("mouseup", up); + }); + beatEl.appendChild(grip); + try { const h0 = parseInt(localStorage.getItem("gs_beath")); if (h0) beatEl.style.height = h0 + "px"; } catch (e) {} + + // ---- header: title ยท master โ–ถ/โ–  ยท bpm ยท close ---- + const top = document.createElement("div"); top.className = "atop"; + const title = document.createElement("span"); title.className = "btitle"; title.textContent = "๐Ÿฅ beat"; + const anyOn = () => DRUM_VOICES.some(v => seqs["drum." + v] && seqs["drum." + v].on); + 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 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)); + bpm.disabled = godtime.locked; + bpm.title = godtime.locked ? "godtime is locked (๐Ÿ”’) โ€” unlock it on the dial" : "beats per minute (40โ€“180)"; + bpm.onchange = () => { if (!godtime.locked) godtime.bpm = clamp(Math.round(+bpm.value || godtime.bpm), 40, 180); + 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); + top.appendChild(hint); top.appendChild(x2); + beatEl.appendChild(top); + + const body = document.createElement("div"); body.className = "abody"; + beatEl.appendChild(body); + + // ---- the feel: GROOVE presets + swing / humanize / amount (the shared `groove`) ---- + const gr = document.createElement("div"); gr.className = "arow grow"; + const gLbl = document.createElement("span"); gLbl.className = "glbl"; gLbl.style.minWidth = "auto"; + gLbl.textContent = "the feel (shared with tracks)"; + gr.appendChild(gLbl); + for (const pn of Object.keys(GROOVE_PRESETS)) { + const pb = document.createElement("button"); pb.className = "midibtn gpreset"; pb.textContent = pn; + pb.classList.toggle("on", groove.preset === pn); + pb.onclick = () => { Object.assign(groove, GROOVE_PRESETS[pn]); groove.preset = pn; openBeat(); }; + gr.appendChild(pb); + } + const mkGS = (label, get, set, max) => { + const w = document.createElement("span"); w.className = "gctl"; + const l = document.createElement("span"); l.className = "glbl"; l.textContent = label; + const sl = document.createElement("input"); sl.type = "range"; sl.min = "0"; sl.max = "1000"; + const vv = document.createElement("span"); vv.className = "gval"; + const show = () => vv.textContent = Math.round(get() / max * 100) + "%"; + sl.value = String(Math.round(get() / max * 1000)); show(); + sl.oninput = () => { set(clamp(+sl.value / 1000 * max, 0, max)); show(); }; + w.appendChild(l); w.appendChild(sl); w.appendChild(vv); return w; + }; + gr.appendChild(mkGS("swing", () => groove.swing, v => { groove.swing = v; groove.preset = "custom"; }, 1)); + gr.appendChild(mkGS("humanize", () => groove.humanize, v => { groove.humanize = v; groove.preset = "custom"; }, 1)); + gr.appendChild(mkGS("amount", () => groove.amount, v => { groove.amount = v; }, 1.3)); + body.appendChild(gr); + + // ---- the grid: five lanes ร— 16 steps ---- + let paintGrid; // (defined after the lanes are built) + const euclidPrompt = (sq, v) => { + const ans = prompt("โฌข euclid " + v + " โ€” pulses across 16 steps (or pulses,rotate ):", "4"); + if (ans == null) return; + const parts = String(ans).split(/[ ,]+/).map(x => parseInt(x)); + const pulses = clamp(Math.round(parts[0] || 0), 0, 16), rot = clamp(Math.round(parts[1] || 0), 0, 15); + euclidFill(sq.steps, pulses, 16, rot, false); + }; + const lanes = []; + for (const v of DRUM_VOICES) { + const sq = drumSeqFor("drum." + v); + const lane = document.createElement("div"); lane.className = "blane"; + const lead = document.createElement("div"); lead.className = "blead"; + const name = document.createElement("span"); name.className = "bname"; name.textContent = v; + const mute = document.createElement("button"); mute.className = "midibtn bmini"; + const paintMute = () => { const on = sq.on; if (mute._on === on) return; + mute._on = on; mute.classList.toggle("on", on); mute.textContent = on ? "โ—" : "โ—‹"; }; + mute._on = null; paintMute(); + mute.title = "mute / unmute this lane"; + mute.onclick = () => { sq.on = !sq.on; paintMute(); paintPlay(); }; + const eu = document.createElement("button"); eu.className = "midibtn bmini"; eu.textContent = "โฌข"; + eu.title = "euclid โ€” spread N pulses evenly across the 16 steps"; + eu.onclick = () => { euclidPrompt(sq, v); paintGrid(); }; + lead.appendChild(name); lead.appendChild(mute); lead.appendChild(eu); + lane.appendChild(lead); + const steps = document.createElement("div"); steps.className = "bsteps"; + const cells = []; + for (let stp = 0; stp < 16; stp++) { + const cell = document.createElement("div"); cell.className = "seqcell"; + if (stp % 4 === 0) cell.classList.add("bar"); // beats 1 / 5 / 9 / 13 + cell.title = v + " ยท step " + (stp + 1); + cell.onclick = () => { sq.steps[stp] = sq.steps[stp] ? 0 : 1; paintGrid(); }; + cells.push(cell); steps.appendChild(cell); + } + lane.appendChild(steps); body.appendChild(lane); + lanes.push({ v, sq, cells, paintMute }); + } + paintGrid = () => { + const playing = anyOn(); + paintPlay(); + if (bpm.disabled !== godtime.locked) bpm.disabled = godtime.locked; + const bv = String(Math.round(godtime.bpm)); + if (document.activeElement !== bpm && bpm.value !== bv) bpm.value = bv; + for (const L of lanes) { L.paintMute(); + for (let s = 0; s < 16; s++) { + L.cells[s].classList.toggle("on", !!L.sq.steps[s]); + L.cells[s].classList.toggle("cur", playing && s === seqCurStep); + } + } + }; + paintGrid(); + beatRefresh = paintGrid; + + play.onclick = () => { // master โ–ถ โ€” flip all five lanes at once + const turnOn = !anyOn(); + lanes.forEach(L => { L.sq.on = turnOn; L.paintMute(); }); + paintGrid(); + }; + + // ---- expression: velocity / chance / ratchet for one selected lane (arranger-style drag) ---- + const em = document.createElement("div"); em.className = "arow emrow"; + const emL = document.createElement("span"); emL.className = "glbl"; emL.style.minWidth = "auto"; emL.textContent = "shape"; + em.appendChild(emL); + const laneSel = document.createElement("select"); laneSel.className = "asel"; + DRUM_VOICES.forEach(v => { const o = document.createElement("option"); o.value = v; o.textContent = v; + if (v === beat.sel) o.selected = true; laneSel.appendChild(o); }); + laneSel.onchange = () => { beat.sel = laneSel.value; openBeat(); }; + em.appendChild(laneSel); + for (const mode of ["velocity", "chance", "ratchet"]) { + const b = document.createElement("button"); b.className = "midibtn"; b.textContent = mode; + b.classList.toggle("on", beat.exprMode === mode); + b.onclick = () => { beat.exprMode = mode; openBeat(); }; + em.appendChild(b); + } + const eh = document.createElement("span"); eh.className = "albl"; + eh.textContent = beat.exprMode === "chance" ? "drag a bar โ€” how likely each " + beat.sel + " step fires" + : beat.exprMode === "ratchet" ? "drag a bar โ€” how many times each " + beat.sel + " step re-fires (1โ€“4): rolls" + : "drag a bar โ€” how hard each " + beat.sel + " step hits"; + em.appendChild(eh); + body.appendChild(em); + + const selSq = drumSeqFor("drum." + beat.sel); + const isRat = beat.exprMode === "ratchet"; + const arrv = isRat ? selSq.rat : (beat.exprMode === "chance" ? selSq.chance : selSq.vel); + const exlane = document.createElement("div"); exlane.className = "exprlane"; + const bars = []; + const paintExpr = () => { for (const b of bars) { + const h = isRat ? (clamp(arrv[b.stp], 1, 4) - 1) / 3 : clamp(arrv[b.stp], 0, 1); + b.fill.style.height = Math.round(h * 100) + "%"; + b.fill.classList.toggle("lit", !!selSq.steps[b.stp]); + } }; + for (let stp = 0; stp < 16; stp++) { + const cell = document.createElement("div"); cell.className = "exprcell"; + if (stp % 4 === 0) cell.classList.add("bar"); + const fill = document.createElement("div"); fill.className = "exprbar"; cell.appendChild(fill); + const setY = (cy) => { const r = cell.getBoundingClientRect(); + const frac = clamp(1 - (cy - r.top) / r.height, 0, 1); + arrv[stp] = isRat ? (1 + Math.round(frac * 3)) : frac; paintExpr(); }; + cell.addEventListener("mousedown", (e0) => { e0.preventDefault(); setY(e0.clientY); + const mv = (e2) => setY(e2.clientY); + const up = () => { window.removeEventListener("mousemove", mv); window.removeEventListener("mouseup", up); }; + window.addEventListener("mousemove", mv); window.addEventListener("mouseup", up); }); + bars.push({ fill, stp }); exlane.appendChild(cell); + } + paintExpr(); + body.appendChild(exlane); + + // ---- kit drawer: knobs rendered from Synth.drumParamList (guarded โ€” nothing pre-merge) ---- + const kitRow = document.createElement("div"); kitRow.className = "arow bkit"; + const kitTgl = document.createElement("button"); kitTgl.className = "midibtn"; + kitTgl.textContent = "๐ŸŽ› kit " + (beat.kitOpen ? "โ–พ" : "โ–ธ"); + kitTgl.onclick = () => { beat.kitOpen = !beat.kitOpen; openBeat(); }; + kitRow.appendChild(kitTgl); + if (beat.kitOpen) { + if (Synth.drumParamList && Synth.drumParam) { + for (const v of DRUM_VOICES) { + const box = document.createElement("div"); box.className = "bvoice"; + const vn = document.createElement("div"); vn.className = "bvn"; vn.textContent = v; box.appendChild(vn); + for (const p of (Synth.drumParamList(v) || [])) { + const w = document.createElement("span"); w.className = "gctl"; + const l = document.createElement("span"); l.className = "glbl"; l.style.minWidth = "44px"; l.textContent = p.label; + const sl = document.createElement("input"); sl.type = "range"; sl.min = "0"; sl.max = "1000"; + sl.value = String(Math.round(clamp(Synth.drumParam(v, p.key), 0, 1) * 1000)); + sl.oninput = () => Synth.drumParam(v, p.key, clamp(+sl.value / 1000, 0, 1)); + w.appendChild(l); w.appendChild(sl); box.appendChild(w); + } + const au = document.createElement("button"); au.className = "midibtn bmini"; au.textContent = "โ–ท hear"; + au.onclick = () => { if (Synth.drum) Synth.drum(v, 0.9); }; + box.appendChild(au); + kitRow.appendChild(box); + } + } else { + const note = document.createElement("span"); note.className = "albl"; + note.textContent = "kit knobs appear once the drum engine (Agent A) is merged in"; + kitRow.appendChild(note); + } + } + body.appendChild(kitRow); + + beatEl.classList.add("open"); + } + function seqTick() { for (const k in seqs) { const sq = seqs[k]; if (!sq.on) continue; + if (k.slice(0, 5) === "drum.") { // a GODRUM lane โ€” fire a synth drum + const cur = seqCurStep; + 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 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) + }; + fireHit(); + for (let h = 1; h < rat; h++) setTimeout(fireHit, h * sub); + } + } + continue; // swing came free from the clock + } const n = dests.get(k); if (n && n.isNote) { const cur = seqCurStep, sv = sq.steps[cur]; @@ -2440,6 +2786,7 @@ } if (seqGridRefresh) seqGridRefresh(); if (arrGrooveRefresh) arrGrooveRefresh(); + if (beatRefresh) beatRefresh(); } function groupsOf(key) { const out = []; @@ -2614,6 +2961,9 @@ if (applyGroove(lsq, 0, 0.7) !== 0.7) f.push("plock-lit-unlocked"); // lit + unlocked โ†’ world if (applyGroove(lsq, 1, 0.7) !== 0) f.push("plock-dark"); // dark step โ†’ 0 if (applyGroove(lsq, 2, 0.7) !== 0.25) f.push("plock-locked"); // lit + locked โ†’ the lock value + // GODRUM โ€” a fresh drum lane (0-filled gates) round-trips through migrateSeq unchanged + const drm = migrateSeq({ on: false, steps: new Array(16).fill(0) }); + if (!drm || drm.on !== false || drm.steps.length !== 16 || drm.steps.some(x => x !== 0)) f.push("drum-lane-shape"); if (f.length) console.error("โš  groove self-check FAILED:", f); else console.log("โœ“ groove self-check passed"); return f.length === 0; } @@ -2629,6 +2979,7 @@ bpm: godtime.bpm, groove: Object.assign({}, groove), // the feel travels with the patch scale: Object.assign({}, gScale), // the key travels too + kit: (Synth.drumKit ? Synth.drumKit() : undefined), // the GODRUM kit travels too (undefined pre-merge) }; } function loadPatch(p) { @@ -2646,6 +2997,7 @@ } if (p.groove) Object.assign(groove, p.groove); if (p.scale) { Object.assign(gScale, p.scale); SEQ_ROWS = scaleRows(gScale.name); } + if (p.kit && Synth.drumKitLoad) Synth.drumKitLoad(p.kit); // the GODRUM kit travels with the patch if (p.arr && p.arr.grid && p.arr.banks) { // the arrangement travels too arr.grid = JSON.parse(JSON.stringify(p.arr.grid)); arr.banks = JSON.parse(JSON.stringify(p.arr.banks)); @@ -2653,6 +3005,7 @@ if (arr.open) openArranger(); // rebuild the panel on the new song } if (typeof p.bpm === "number") godtime.bpm = clamp(p.bpm, 40, 180); + if (beat.open) openBeat(); // rebuild the beat panel on the new song layoutDirty = true; } @@ -6361,6 +6714,9 @@ if (ev.key === "t" || ev.key === "T") { arr.open ? closeArranger() : openArranger(); // tracks, for the casuals } + if (ev.key === "b" || ev.key === "B") { + beat.open ? closeBeat() : openBeat(); // ๐Ÿฅ GODRUM โ€” make a beat + } }); // --------------------------------------------------------------------------- @@ -6565,6 +6921,8 @@ ctxItem(zenMode ? "leave zen" : "โ˜พ zen โ€” only the visuals", () => setZen(!zenMode), "Z"); ctxItem(arr.open ? "close tracks" : "๐ŸŽ› tracks โ€” arrange clips", () => { arr.open ? closeArranger() : openArranger(); }, "T"); + ctxItem(beat.open ? "close beat" : "๐Ÿฅ beat โ€” make a beat", + () => { beat.open ? closeBeat() : openBeat(); }, "B"); const moodSub = ctxSub("๐ŸŒ— mood โ€” how much world"); const z0 = document.createElement("div"); z0.className = "it"; z0.textContent = zeroMode ? "0 zero โ€” you're building it (a mood leaves)" : "0 zero โ€” build from nothing";