From fa5aa434d1acb1cdc9dcfb2fd81cb29363bc09f4 Mon Sep 17 00:00:00 2001 From: monsterrobotparty Date: Sun, 12 Jul 2026 22:44:41 +1000 Subject: [PATCH] =?UTF-8?q?=F0=9F=A5=81=20godrum:=20integrate=20=E2=80=94?= =?UTF-8?q?=20merge=20A+B,=20review=20fixes=20R1=E2=80=93R7,=20mix=20pass?= =?UTF-8?q?=20+=20kick-duck?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- viz/index.html | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/viz/index.html b/viz/index.html index decf75c..4bc1278 100644 --- a/viz/index.html +++ b/viz/index.html @@ -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));