🥁 godrum: finger-drumming keys, live record, drums in the MIDI export
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 <noreply@anthropic.com>
This commit is contained in:
parent
b0fad08556
commit
bd3340d4a2
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Loading…
Reference in New Issue
Block a user