Merge branch 'godrum/b-beat-panel' into godrum/integrate
This commit is contained in:
commit
4990e9244f
358
viz/index.html
358
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";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user