diff --git a/viz/index.html b/viz/index.html
index 7258128..decf75c 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";