🥁 godrum: integrate — merge A+B, review fixes R1–R7, mix pass + kick-duck

Stage 2 (Agent C). Real merge of godrum/a-engine + godrum/b-beat-panel onto
godrum/integrate (zero conflicts; extracted app script passes node --check),
then the seven review findings:

R1  kick tail — stop the osc at t+0.01+8τ (τ computed once); no click at max decay.
R2  seed — clear closed hats on steps 14/15 so the seeded open hat rings into the downbeat.
R3  stale paint — paintGrid now drives paintExpr (lit-bars track the grid); groove-slider drags relight the preset buttons.
R4  groove — add the fourth "velocity" slider (groove.velo), matching the arranger's grooves tab.
R5  moods — applyMood's storm + clearGrooves loops skip drum.* lanes; a mood never silences the beat.
R6  kick-duck — duck param on the kick (default .35); pad+drone reroute through a new duckG; fireKick dips it and lets it flood back ("the kick parts the sea").
R7  mix — closed/open hat internal peak scalars 0.70/0.80 → 0.35/0.45 so the hats sit at/just under the ambient perc.density shimmer (§0.6 contract defaults unchanged).

Verified end-to-end on a static serve via offline-render measurements plus a live
analyser tap on the real graph: first-press wake with ♪ off, every voice audible,
choke (95% tail cut), duck −3.07 dB → recover, chance/ratchet, R5 through a storm,
kit+beat persistence incl. duck, zero-mode round-trip, arranger↔beat shared groove,
mutual-exclusion, zero console errors. Temporary verify hooks removed (grep-clean).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
monsterrobotparty 2026-07-12 22:44:41 +10:00
parent 4990e9244f
commit fa5aa434d1

View File

@ -2461,12 +2461,12 @@
try { localStorage.setItem("gs_mood", name); } catch (e) {} // your vibe survives a reload
for (const k of MOOD_KEYS) { const t2 = tw(k); t2.mute = 0; t2.gain = 1; t2.offset = 0; }
if (m.storm) {
for (const k in seqs) seqs[k].on = false;
for (const k in seqs) { if (k.slice(0, 5) === "drum.") continue; seqs[k].on = false; } // the beat belongs to the player — a mood never silences it
godtime.locked = false;
} else {
for (const k in (m.mutes || {})) tw(k).mute = m.mutes[k];
for (const k in (m.gains || {})) tw(k).gain = m.gains[k];
if (m.clearGrooves) for (const k in seqs) seqs[k].on = false;
if (m.clearGrooves) for (const k in seqs) { if (k.slice(0, 5) === "drum.") continue; seqs[k].on = false; } // drum lanes survive a mood change
for (const k in (m.grooves || {})) {
const n = dests.get(k);
const sq = seqFor(k, n ? n.isNote : /note/.test(k));
@ -2514,6 +2514,7 @@
[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; }
H.steps[14] = 0; H.steps[15] = 0; // clear the closed hats around the open hat so it rings into the downbeat
O.steps[14] = 1;
// all lanes stay on:false — the user's first ▶ starts the beat
}
@ -2571,12 +2572,14 @@
const gLbl = document.createElement("span"); gLbl.className = "glbl"; gLbl.style.minWidth = "auto";
gLbl.textContent = "the feel (shared with tracks)";
gr.appendChild(gLbl);
const presetBtns = [];
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);
gr.appendChild(pb); presetBtns.push({ pb, pn });
}
const paintPresets = () => presetBtns.forEach(({ pb, pn }) => pb.classList.toggle("on", groove.preset === pn));
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;
@ -2584,16 +2587,17 @@
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(); };
sl.oninput = () => { set(clamp(+sl.value / 1000 * max, 0, max)); show(); paintPresets(); }; // a drag drops the preset to "custom" — relight the buttons to match
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("velocity", () => groove.velo, v => { groove.velo = v; groove.preset = "custom"; }, 1)); // the fourth feel dial — velocity deviation, which drum lanes obey
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)
let paintGrid, paintExpr; // both defined below; paintGrid also drives paintExpr so lit-bars track the steps
const euclidPrompt = (sq, v) => {
const ans = prompt("⬢ euclid " + v + " — pulses across 16 steps (or pulses,rotate ):", "4");
if (ans == null) return;
@ -2642,6 +2646,7 @@
L.cells[s].classList.toggle("cur", playing && s === seqCurStep);
}
}
if (paintExpr) paintExpr(); // keep the expression lane's lit-bars in step with the grid
};
paintGrid();
beatRefresh = paintGrid;
@ -2679,7 +2684,7 @@
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) {
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]);
@ -8162,7 +8167,7 @@
// boundary and mapped to real units inside its voice. DRUM_DEFS is the single
// source of truth the panel renders from; KIT holds the live, editable values.
const DRUM_DEFS = {
kick: { params: ["tune", "punch", "decay", "level", "pan"], def: [0.5, 0.5, 0.5, 0.85, 0.5] },
kick: { params: ["tune", "punch", "decay", "duck", "level", "pan"], def: [0.5, 0.5, 0.5, 0.35, 0.85, 0.5] },
snare: { params: ["tune", "snap", "decay", "level", "pan"], def: [0.5, 0.6, 0.4, 0.7, 0.5] },
clap: { params: ["tone", "decay", "level", "pan"], def: [0.5, 0.5, 0.65, 0.5] },
hat: { params: ["tone", "decay", "level", "pan"], def: [0.6, 0.35, 0.5, 0.5] },
@ -8282,9 +8287,12 @@
bassOsc.connect(bassFilt); bassFilt.connect(bassAmp); bassAmp.connect(preSat);
bassOsc.frequency.value = mtof(38); bassOsc.start();
// GODRUM kick-duck bus: pad + drone route through duckG, so a kick can dip the
// low-mid wall out from under itself ("the kick parts the sea"). Rides at 1.
const duckG = ctx.createGain(); duckG.gain.value = 1; duckG.connect(preSat);
const padFilt = ctx.createBiquadFilter(); padFilt.type = "lowpass"; padFilt.frequency.value = 800;
const padAmp = ctx.createGain(); padAmp.gain.value = 0;
padFilt.connect(padAmp); padAmp.connect(preSat); lfoAmt.connect(padFilt.frequency);
padFilt.connect(padAmp); padAmp.connect(duckG); lfoAmt.connect(padFilt.frequency);
// lush supersaw pad — each chord tone is a cluster of detuned saws (JP-8000 style).
// Spectrally verified: lush (133 partials), no clip, no aliasing, no DC.
const padOscs = [];
@ -8296,7 +8304,7 @@
const droneFilt = ctx.createBiquadFilter(); droneFilt.type = "lowpass"; droneFilt.frequency.value = 380;
const droneAmp = ctx.createGain(); droneAmp.gain.value = 0;
droneFilt.connect(droneAmp); droneAmp.connect(preSat);
droneFilt.connect(droneAmp); droneAmp.connect(duckG);
const droneOscs = [0, 7].map((iv) => {
const o = ctx.createOscillator(); o.type = "sawtooth"; o.frequency.value = mtof(26 + iv);
o.connect(droneFilt); o.start(); return o;
@ -8342,7 +8350,7 @@
return { out, preSat, glitch, revSend, fb, satDry, satWet, leadA, leadB, morphA, morphB, leadFilt, leadAmp,
bassOsc, bassAmp, padOscs, padFilt, padAmp, droneOscs, droneAmp,
echoDelay, echoFilt, echoFb, echoWet, echoFlutAmt, hissGain, chorusWet,
drumBus, drums, noiseBuf };
drumBus, drums, noiseBuf, duckG };
}
function hit(node, peak, dec) { // pluck envelope
@ -8378,20 +8386,24 @@
function fireKick(vel) {
const t = ctx.currentTime, K = KIT.kick, dst = N.drums.kick.g;
const fB = 35 + K.tune * 45, peak = vel * vel;
const fB = 35 + K.tune * 45, peak = vel * vel, tau = 0.05 + K.decay * 0.30;
const o = ctx.createOscillator(); o.type = "sine";
o.frequency.setValueAtTime(fB * (2 + K.punch * 6), t);
o.frequency.exponentialRampToValueAtTime(fB, t + 0.045); // the pitch drop IS the thump
const g = ctx.createGain();
g.gain.setValueAtTime(0, t);
g.gain.linearRampToValueAtTime(peak, t + 0.003);
g.gain.setTargetAtTime(0, t + 0.01, 0.05 + K.decay * 0.30);
o.connect(g); g.connect(dst); o.start(t); o.stop(t + 1.2);
g.gain.setTargetAtTime(0, t + 0.01, tau);
o.connect(g); g.connect(dst); o.start(t); o.stop(t + 0.01 + tau * 8); // ~8τ: let the tail ring fully out — no click at high decay
if (K.punch > 0) { // a 2ms highpassed noise click for attack
const cs = dNoise(), hp = ctx.createBiquadFilter(); hp.type = "highpass"; hp.frequency.value = 1000;
const cg = ctx.createGain(); cg.gain.setValueAtTime(vel * K.punch * 0.3, t); cg.gain.setTargetAtTime(0, t, 0.002);
cs.connect(hp); hp.connect(cg); cg.connect(dst); cs.start(t); cs.stop(t + 0.05);
}
if (K.duck > 0 && N.duckG) { // the kick parts the sea — dip pad+drone, then let them flood back
const dg = N.duckG.gain; dg.cancelScheduledValues(t); dg.setValueAtTime(dg.value, t);
dg.linearRampToValueAtTime(1 - K.duck * 0.85, t + 0.012); dg.setTargetAtTime(1, t + 0.05, 0.15);
}
}
function fireSnare(vel) {
const t = ctx.currentTime, K = KIT.snare, dst = N.drums.snare.g;
@ -8423,7 +8435,7 @@
choke(t); // a closed OR a new open hat chokes the ringing open hat — the TR rule
const ns = dNoise(), hp = ctx.createBiquadFilter();
hp.type = "highpass"; hp.frequency.value = 5500 + K.tone * 4000;
const g = ctx.createGain(), peak = vel * vel * (open ? 0.8 : 0.7);
const g = ctx.createGain(), peak = vel * vel * (open ? 0.45 : 0.35); // mix: sit the hats at/just under the ambient perc.density shimmer (§A.3 intent)
const tau = open ? (0.10 + K.decay * 0.5) : (0.012 + K.decay * 0.06);
g.gain.setValueAtTime(0, t); g.gain.linearRampToValueAtTime(peak, t + 0.001); g.gain.setTargetAtTime(0, t + 0.002, tau);
ns.connect(hp); hp.connect(g); g.connect(dst); ns.start(t); ns.stop(t + Math.max(0.1, tau * 6));