🥁 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:
parent
4990e9244f
commit
fa5aa434d1
@ -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));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user