From b0fad08556aa55b5ec75e0eace5c849fdff9c890 Mon Sep 17 00:00:00 2001 From: monsterrobotparty Date: Sun, 12 Jul 2026 23:57:31 +1000 Subject: [PATCH] =?UTF-8?q?=F0=9F=A5=81=20godrum:=202.1=20=E2=80=94=20chok?= =?UTF-8?q?e=20anchor,=20euclid=20guard,=20proto-pollution,=20zero-mode=20?= =?UTF-8?q?beat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four defects from the independent 4-lens adversarial review of Stage 2. R8 choke same-task noise pop — DEVIATION from the brief's fix, verified by measurement. Read-before-cancel does NOT work: reading gain.value right after setValueAtTime(0,t) in the same task returns the 1.0 default (the envelope hasn't rendered), both offline AND live — so the anchor is 1.0 and it still pops (measured 0.45 vs a 0.15 ohat). Correct primitive is cancelAndHoldAtTime, which holds the timeline value at t (0 same-task, decaying level mid-ring); read-value kept as the legacy fallback. Same idiom applied to fireKick's duck. Verified live: same-task double-ohat 0.152 ~= single 0.149 (no pop); normal different-task choke still cuts the tail 70%. R9 euclid junk input — trim, split /[\s,]+/, drop empty tokens, Number(), bail unless the first token is finite. " 4"/",4" -> E(4,16); "abc"/"" -> untouched; "0" -> cleared; "5,2" -> E(5,16) rot 2. (Empty-token filter also fixes the "" vs "0" distinction a bare Number() can't.) R10 prototype pollution via kit data (MAJOR — patches/vibes are cross-user) — own-property guard (hasOwnProperty) everywhere a caller-supplied name indexes KIT / N.drums / DRUM_DEFS: drum, drumParam, drumParamList, drumKit, drumKitLoad, drumSync. Verified: drumKitLoad({"__proto__":{toString:0.5}}) and drumParam("__proto__",…) leave Object.prototype intact; legit keys still apply; normal round-trip works. R11 a beat is a build — hoist the canonical seed into DRUM_SEED (one source of truth for seedBeat + zeroHasBuild); zeroHasBuild() now returns true when any drum lane is on or its gates differ from the seed. Untouched seed still exits quietly; ▶ or a drawn step now shows the keep/weave card so the beat survives. Temporary verify hooks removed (grep-clean). No merge to main, no deploy. Co-Authored-By: Claude Opus 4.8 --- viz/index.html | 54 +++++++++++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/viz/index.html b/viz/index.html index 4bc1278..58f401d 100644 --- a/viz/index.html +++ b/viz/index.html @@ -1628,6 +1628,9 @@ // 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"]; + // the canonical first-open seed — ONE source of truth for seedBeat() and zeroHasBuild() (they must not drift) + const DRUM_SEED = { kick: [0, 7, 10], snare: [4, 12], clap: [], + hat: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], ohat: [14] }; 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] = { @@ -2508,14 +2511,8 @@ // 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; } - 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; + for (const v of DRUM_VOICES) { const sq = drumSeqFor("drum." + v); for (const i of DRUM_SEED[v]) sq.steps[i] = 1; } + const H = seqs["drum.hat"]; for (let i = 0; i < 16; i++) H.vel[i] = (i % 2) ? 0.4 : 0.8; // seed hat dynamics (step gates come from DRUM_SEED — hat 0–13, open hat rings into the downbeat) // all lanes stay on:false — the user's first ▶ starts the beat } function closeBeat() { @@ -2601,8 +2598,10 @@ 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); + const parts = String(ans).trim().split(/[\s,]+/).filter(x => x !== "").map(Number); // drop leading empties from " 4" / ",4" + if (!parts.length || !Number.isFinite(parts[0])) return; // junk / empty confirm → no-op; an explicit "0" still clears + const pulses = clamp(Math.round(parts[0]), 0, 16), + rot = clamp(Math.round(Number.isFinite(parts[1]) ? parts[1] : 0), 0, 15); euclidFill(sq.steps, pulses, 16, rot, false); }; const lanes = []; @@ -3050,6 +3049,12 @@ if (zeroUI.nodes && Object.keys(zeroUI.nodes).length) return true; for (const k in tweaks) { const t = tweaks[k]; if (t && ((t.offset && Math.abs(t.offset) > 0.0015) || (t.gain != null && Math.abs(t.gain - 1) > 0.01) || t.mute >= 0.5 || t.freeze >= 0.5)) return true; } + for (const v of DRUM_VOICES) { // a beat is a build too (R11): any lane playing, or its gates edited off the seed + const sq = seqs["drum." + v]; if (!sq) continue; + if (sq.on) return true; + const seed = DRUM_SEED[v]; + for (let i = 0; i < 16; i++) if ((sq.steps[i] ? 1 : 0) !== (seed.indexOf(i) >= 0 ? 1 : 0)) return true; + } return false; } function finishZeroExit(mode, name) { // mode: keep | old | weave (C1) @@ -8175,6 +8180,9 @@ }; const KIT = {}; for (const _v in DRUM_DEFS) { KIT[_v] = {}; DRUM_DEFS[_v].params.forEach((p, i) => { KIT[_v][p] = DRUM_DEFS[_v].def[i]; }); } + // caller-supplied names (from patch/vibe JSON — cross-user data) must be OWN keys, never reached through + // the prototype chain — a "__proto__" key would otherwise write onto Object.prototype and corrupt the runtime. + const own = (o, k) => Object.prototype.hasOwnProperty.call(o, k); const mtof = (m) => 440 * Math.pow(2, (m - 69) / 12); const c01 = (x) => x < 0 ? 0 : x > 1 ? 1 : x; @@ -8375,14 +8383,19 @@ // stopped once their tail has rung out. ---- function dNoise() { const s = ctx.createBufferSource(); s.buffer = N.noiseBuf; s.loop = true; return s; } function drumSync(name, param) { // level/pan → live nodes on a short glide - if (!N || !N.drums || !N.drums[name]) return; + if (!N || !N.drums || !own(N.drums, name)) return; const t = ctx.currentTime, dv = N.drums[name]; if (param === "level") dv.g.gain.setTargetAtTime(KIT[name].level * 1.2, t, 0.02); else if (param === "pan") dv.pan.pan.setTargetAtTime(KIT[name].pan * 2 - 1, t, 0.02); } + // cancel pending automation but HOLD the value the timeline actually has at t — 0 for a same-task + // hit whose envelope hasn't rendered (reading .value there returns the 1.0 default → a pop), the + // decaying level mid-ring. cancelAndHoldAtTime is the right primitive; read-value is the legacy fallback. + const cancelHold = (p, t) => { if (p.cancelAndHoldAtTime) p.cancelAndHoldAtTime(t); + else { p.cancelScheduledValues(t); p.setValueAtTime(p.value, t); } }; let ohatEnv = null; // the currently-ringing open hat, for the TR choke - function choke(t) { if (ohatEnv) { ohatEnv.gain.cancelScheduledValues(t); ohatEnv.gain.setTargetAtTime(0, t, 0.012); ohatEnv = null; } } + function choke(t) { if (ohatEnv) { cancelHold(ohatEnv.gain, t); ohatEnv.gain.setTargetAtTime(0, t, 0.012); ohatEnv = null; } } function fireKick(vel) { const t = ctx.currentTime, K = KIT.kick, dst = N.drums.kick.g; @@ -8401,7 +8414,7 @@ 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); + const dg = N.duckG.gain; cancelHold(dg, t); // same cancel-and-hold idiom as choke() (robust when two kicks land close) dg.linearRampToValueAtTime(1 - K.duck * 0.85, t + 0.012); dg.setTargetAtTime(1, t + 0.05, 0.15); } } @@ -8444,7 +8457,7 @@ // fire a drum — mirrors pluck()'s wake-up, so a drum can be the first sound the page makes function drum(name, vel) { - if (!DRUM_DEFS[name]) return; // unknown voice → silent, no side effects + if (!own(DRUM_DEFS, name)) return; // unknown / non-own voice → silent, no side effects if (!AC) return; if (!ctx) { ctx = new AC(); N = build(); loadWorklets(); } ctx.resume(); @@ -8457,17 +8470,18 @@ else if (name === "ohat") fireHat(v, true); } function drumParam(name, param, value) { - const kv = KIT[name]; if (!kv || !(param in kv)) return undefined; + if (!own(KIT, name) || !own(KIT[name], param)) return undefined; // own-key only — no proto-chain reach + const kv = KIT[name]; if (value === undefined) return kv[param]; // get if (Number.isFinite(value)) { kv[param] = c01(value); drumSync(name, param); } // set (finite, clamped 0..1) → live nodes; ignore junk return kv[param]; } - function drumParamList(name) { const def = DRUM_DEFS[name]; return def ? def.params.map((p) => ({ key: p, label: p })) : []; } - function drumKit() { const o = {}; for (const v in KIT) { o[v] = {}; for (const p in KIT[v]) o[v][p] = KIT[v][p]; } return o; } // deep, JSON-safe copy - function drumKitLoad(obj) { // merge a snapshot; missing/foreign keys keep defaults + function drumParamList(name) { const def = own(DRUM_DEFS, name) ? DRUM_DEFS[name] : null; return def ? def.params.map((p) => ({ key: p, label: p })) : []; } + function drumKit() { const o = {}; for (const v in KIT) if (own(KIT, v)) { o[v] = {}; for (const p in KIT[v]) if (own(KIT[v], p)) o[v][p] = KIT[v][p]; } return o; } // deep, JSON-safe, own-keys only + function drumKitLoad(obj) { // merge a snapshot; missing/foreign/proto keys keep defaults if (!obj) return; - for (const v in obj) if (KIT[v] && obj[v]) for (const p in obj[v]) - if ((p in KIT[v]) && Number.isFinite(obj[v][p])) { KIT[v][p] = c01(obj[v][p]); drumSync(v, p); } + for (const v in obj) if (own(obj, v) && own(KIT, v) && obj[v]) for (const p in obj[v]) + if (own(obj[v], p) && own(KIT[v], p) && Number.isFinite(obj[v][p])) { KIT[v][p] = c01(obj[v][p]); drumSync(v, p); } } function params() {