🐛 fix 8 adversarial-review findings (zero/vibes/OMNI)
A 30-agent adversarial review (5 dimensions × find→refute×2) confirmed 8 defects; all fixed and live-verified: BLOCKERS - #1 unwired OMNI trim couldn't be silenced: the offset-floor only seeded a dest while offset>0.0015, so reset/mute/lower stopped emitting the key and the last floored value stuck in the synth (Synth.update merges). Now ANY tweaked cableless non-note dest is seeded 0 so the processing loop writes its real value, incl. 0. (verified: crush norm 0.5 on drag → 0 on reset) - #2 leaving zero with an OMNI/offset-only build (no cables) was silently discarded — exitZeroToMood gated on routesArr.length===0. New zeroHasBuild() also counts picked orbs + any non-neutral tweak, so the keep/old/weave card always shows. (verified: offset-only build → 'you shaped this instrument by hand. keep it?') - #3 vibe scale whitelist used truthiness (SCALES[q.scale]) → 'constructor'/'__proto__' passed → NaN into a Web Audio AudioParam → threw ~40×/s (remotely triggerable via public #vibe= links). Now Array.isArray(SCALES[...]) + quantize only on note dests; mxQuantize fallback hardened too. (verified: fx quantize dropped, note scale→minor_pent) SHOULD-FIX - #4 finishZeroExit keep/weave clobbered the kept mix (applyMood resets MOOD_KEYS): re-lay the build's tweaks after applyMood; weave merges build tweaks over prezero. - #5 crush knob was fully dead (never in dests): OMNI.render ensureDest()s every voice/FX key → arc lives, floor works, right-click opens the menu. (verified) - #6 OMNI drags in zero weren't persisted: knob pointerup/dblclick call zeroUI.persist() when zeroMode → survives reload. (verified: offset 0.467 in gs_zerobuild) - #7 reloaded vibes showed OFF: persist activeVibes + per-route _vibes/_base to gs_zerovibes, restoreVibeState() on enterZero(true). (verified: vibe restores ◉) - #8 GODSONIQ chip lit ~3s late: update() re-lights engine/♪ chips each frame. Console clean, syntax clean, 21 knobs, field healthy. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
fc1bb307bc
commit
fa948823a7
@ -1450,7 +1450,7 @@
|
||||
return x;
|
||||
}
|
||||
function mxQuantize(x01, scale, root, octaves) {
|
||||
const steps = SCALES[scale] || SCALES.chromatic;
|
||||
const steps = Array.isArray(SCALES[scale]) ? SCALES[scale] : SCALES.chromatic; // never an inherited prototype name → no NaN
|
||||
const n = steps.length * (octaves || 3);
|
||||
const idx = Math.floor(clamp(x01, 0, 1) * (n - 1) + 0.5);
|
||||
const oct = Math.floor(idx / steps.length), deg = idx % steps.length;
|
||||
@ -2490,10 +2490,9 @@
|
||||
}
|
||||
for (const k in destVals) destVals[k] = clamp(destVals[k], 0, 1);
|
||||
for (const k in notes) destVals[k] = notes[k];
|
||||
for (const [k, n] of dests) { // a hand-set offset floor speaks even with no cable —
|
||||
if (n.isNote || (k in destVals)) continue; // so an OMNI/trim knob gives a voice a level from nothing
|
||||
const tk = tweaks[k];
|
||||
if (tk && !(tk.mute >= 0.5) && (tk.offset || 0) > 0.0015) destVals[k] = 0;
|
||||
for (const [k, n] of dests) { // any hand-tweaked cableless voice: emit its ACTUAL value —
|
||||
if (n.isNote || (k in destVals)) continue; // an offset floor gives it a level from nothing, and a
|
||||
if (tweaks[k]) destVals[k] = 0; // reset/mute/lower writes 0 too (else the last value sticks in the synth)
|
||||
}
|
||||
for (const k in destVals) {
|
||||
const tk = tweaks[k];
|
||||
@ -2647,10 +2646,11 @@
|
||||
if (restore) { // reload — bring the zero build's cables back (C2)
|
||||
let build = null; try { build = JSON.parse(localStorage.getItem("gs_zerobuild") || "null"); } catch (e) {}
|
||||
loadPatch(build && build.routes ? build : { routes: [], tweaks: {}, seqs: {} });
|
||||
zeroUI.restoreVibeState(); // re-tag vibe cables so they show ON and stay removable (#7)
|
||||
} else { // fresh — empty the field and forget any old build
|
||||
loadPatch({ routes: [], tweaks: {}, seqs: {} });
|
||||
zeroUI.nodes = {};
|
||||
try { localStorage.removeItem("gs_zeronodes"); localStorage.removeItem("gs_zerobuild"); } catch (e) {}
|
||||
zeroUI.nodes = {}; activeVibes.clear();
|
||||
try { localStorage.removeItem("gs_zeronodes"); localStorage.removeItem("gs_zerobuild"); localStorage.removeItem("gs_zerovibes"); } catch (e) {}
|
||||
}
|
||||
zeroMode = true;
|
||||
try { localStorage.setItem("gs_mood", "0 zero"); } catch (e) {}
|
||||
@ -2660,18 +2660,28 @@
|
||||
}
|
||||
function exitZeroToMood(name) { // leave zero back into the full world, then a mood
|
||||
if (!zeroMode) { applyMood(name); return; }
|
||||
if (routesArr.length === 0) { finishZeroExit("old", name); return; } // nothing built — quiet restore (C1)
|
||||
if (!zeroHasBuild()) { finishZeroExit("old", name); return; } // truly nothing built — quiet restore (C1)
|
||||
zeroExitChoice(name); // you built something — ask what to keep with it
|
||||
}
|
||||
function zeroHasBuild() { // a zero build can be cables, picked orbs, OR OMNI/trim shapes (no cables)
|
||||
if (routesArr.length) return true;
|
||||
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; }
|
||||
return false;
|
||||
}
|
||||
function finishZeroExit(mode, name) { // mode: keep | old | weave (C1)
|
||||
const build = serializePatch(); // the zero-built patch, captured before we touch routes
|
||||
const build = serializePatch(); // the zero-built patch (routes + tweaks + seqs), captured before we touch anything
|
||||
zeroMode = false; zeroUI.close();
|
||||
try { localStorage.removeItem("gs_zeronodes"); localStorage.removeItem("gs_zerobuild"); } catch (e) {}
|
||||
try { localStorage.removeItem("gs_zeronodes"); localStorage.removeItem("gs_zerobuild"); localStorage.removeItem("gs_zerovibes"); } catch (e) {}
|
||||
if (mode === "old" || mode === "weave") {
|
||||
try { const pz = localStorage.getItem("gs_prezero"); if (pz) loadPatch(JSON.parse(pz)); } catch (e) {}
|
||||
}
|
||||
if (mode === "weave") for (const r of (build.routes || [])) registerRoute(r); // graft build atop the old world
|
||||
applyMood(name); // keep: build stays as-is and the mood just rides it
|
||||
if (mode === "weave") for (const r of (build.routes || [])) registerRoute(r); // graft build's cables atop the old world
|
||||
applyMood(name); // the mood rides on top: its groove + tempo (+ its own mutes/gains)…
|
||||
if (mode === "keep" || mode === "weave") { // …but the kept build's own mix wins — applyMood resets MOOD_KEYS, so re-lay it
|
||||
for (const k in (build.tweaks || {})) tweaks[k] = Object.assign({ mute: 0, gain: 1, offset: 0 }, build.tweaks[k]);
|
||||
}
|
||||
layoutDirty = true;
|
||||
}
|
||||
function zeroExitChoice(name) { // compact three-way card (same language as the door card)
|
||||
@ -2686,7 +2696,8 @@
|
||||
const h = document.createElement("div"); h.textContent = "leaving zero";
|
||||
Object.assign(h.style, { fontWeight: "800", fontSize: "clamp(20px,3.5vw,28px)", letterSpacing: "2px", marginBottom: "6px" });
|
||||
const sub = document.createElement("div");
|
||||
sub.textContent = "you built " + n + " cable" + (n === 1 ? "" : "s") + " here. keep it?";
|
||||
sub.textContent = n ? ("you built " + n + " cable" + (n === 1 ? "" : "s") + " here. keep it?")
|
||||
: "you shaped this instrument by hand. keep it?"; // OMNI/trim-only builds have no cables
|
||||
Object.assign(sub.style, { color: "rgba(165,185,218,0.75)", fontStyle: "italic", marginBottom: "24px", fontSize: "14px" });
|
||||
card.appendChild(h); card.appendChild(sub);
|
||||
const row = document.createElement("div");
|
||||
@ -2903,11 +2914,21 @@
|
||||
},
|
||||
open() { this.build(); this.restoreNodes(); this.root.classList.add("on"); this.renderStage(); this.refreshSockets(); },
|
||||
close() { if (this.root) { this.root.classList.remove("on"); this.closeDrawer(); this.disarm(); } },
|
||||
persist() { // zero build survives a reload: picks + cables (C2)
|
||||
persist() { // zero build survives a reload: picks + cables + vibe ownership (C2)
|
||||
try {
|
||||
const nd = {}; for (const gk in this.nodes) nd[gk] = [...this.nodes[gk].members];
|
||||
localStorage.setItem("gs_zeronodes", JSON.stringify(nd));
|
||||
localStorage.setItem("gs_zerobuild", JSON.stringify(serializePatch()));
|
||||
const own = {}; for (const r of routesArr) if (r._vibes && r._vibes.size) own[r.source + "|" + r.dest] = { v: [...r._vibes], b: !!r._base };
|
||||
localStorage.setItem("gs_zerovibes", JSON.stringify({ active: [...activeVibes], own })); // so reloaded vibes stay live + removable (Z3/#7)
|
||||
} catch (e) {}
|
||||
},
|
||||
restoreVibeState() { // re-tag routes so a reloaded vibe shows ON and its off-toggle works
|
||||
try {
|
||||
const zv = JSON.parse(localStorage.getItem("gs_zerovibes") || "null");
|
||||
if (!zv) return;
|
||||
activeVibes.clear(); for (const nm of (zv.active || [])) activeVibes.add(nm);
|
||||
for (const id in (zv.own || {})) { const rt = routeIndex.get(id); if (rt) { rt._vibes = new Set(zv.own[id].v); rt._base = !!zv.own[id].b; } }
|
||||
} catch (e) {}
|
||||
},
|
||||
restoreNodes() { // rebuild the picked orbs from storage (reload path)
|
||||
@ -3166,10 +3187,13 @@
|
||||
const route = { source: r.source, dest: r.dest,
|
||||
amount: (typeof r.amount === "number" && isFinite(r.amount)) ? clamp(r.amount, -4, 4) : 0.5,
|
||||
curve: VIBE_CURVES.has(r.curve) ? r.curve : "lin" };
|
||||
if (r.quantize && typeof r.quantize === "object") {
|
||||
// quantize only ever belongs on a NOTE dest, and the scale must be an own
|
||||
// array entry of SCALES — never an inherited name ("constructor" etc.), which
|
||||
// would smuggle a NaN into a Web Audio param and throw every audio tick.
|
||||
if (r.quantize && typeof r.quantize === "object" && /note/.test(r.dest.split(".").pop())) {
|
||||
const q = r.quantize;
|
||||
route.quantize = {
|
||||
scale: (typeof q.scale === "string" && SCALES[q.scale]) ? q.scale : "minor_pent",
|
||||
scale: (typeof q.scale === "string" && Array.isArray(SCALES[q.scale])) ? q.scale : "minor_pent",
|
||||
root: (typeof q.root === "number" && isFinite(q.root)) ? clamp(Math.round(q.root), 0, 127) : 60,
|
||||
octaves: (typeof q.octaves === "number" && isFinite(q.octaves)) ? clamp(Math.round(q.octaves), 1, 4) : 2,
|
||||
};
|
||||
@ -3419,14 +3443,16 @@
|
||||
["🎛 GODSONIQ", () => Synth.sampling(), () => { const ok = Synth.godsoniq(3);
|
||||
eventTicker.unshift({ text: ok ? "🎛 GODSONIQ — sampling 3s of the world… every pluck now replays it" : "🎛 press ♪ first, then GODSONIQ", tAdded: now(), src: "sys" }); }],
|
||||
];
|
||||
this._chips = []; // remembered so update() can re-light them (GODSONIQ lands ~3s async) (#8)
|
||||
for (const [label, isOn, act] of engines) {
|
||||
const c = document.createElement("div"); c.className = "ochip" + (isOn() ? " on" : ""); c.textContent = label;
|
||||
c.onclick = () => { try { act(); } catch (e) {} setTimeout(() => this.renderHead(), 40); };
|
||||
h.appendChild(c);
|
||||
h.appendChild(c); this._chips.push([c, isOn]);
|
||||
}
|
||||
const sp = document.createElement("div"); sp.className = "ospacer"; h.appendChild(sp);
|
||||
const snd = document.createElement("div"); snd.className = "ochip" + (Synth.playing && Synth.playing() ? " on" : ""); snd.textContent = "♪";
|
||||
snd.title = "start / stop the synth"; snd.onclick = () => { Synth.toggle(); setTimeout(() => this.renderHead(), 60); }; h.appendChild(snd);
|
||||
this._chips.push([snd, () => Synth.playing && Synth.playing()]);
|
||||
const bpm = document.createElement("div"); bpm.className = "ochip"; bpm.id = "omnibpm"; bpm.style.cursor = "default"; bpm.textContent = Math.round(godtime.bpm) + " bpm"; h.appendChild(bpm);
|
||||
const lock = document.createElement("div"); lock.className = "ochip" + (godtime.locked ? " on" : ""); lock.textContent = godtime.locked ? "🔒" : "🔓";
|
||||
lock.title = "lock the godtime tempo"; lock.onclick = () => { godtime.locked = !godtime.locked; this.renderHead(); }; h.appendChild(lock);
|
||||
@ -3434,7 +3460,10 @@
|
||||
},
|
||||
render() {
|
||||
const b = this.body; b.innerHTML = ""; this.knobs = [];
|
||||
for (const mod of this.MODULES) b.appendChild(this.moduleEl(mod, false));
|
||||
for (const mod of this.MODULES) {
|
||||
for (const key of mod.keys) if (!/note/.test(key.split(".").pop())) ensureDest(key); // register voices/FX (e.g. crush) so their arcs live + floors work
|
||||
b.appendChild(this.moduleEl(mod, false));
|
||||
}
|
||||
const em = this.moduleEl(this.ECHO, true); // earth echo — knobs hidden until it's on
|
||||
em.classList.add("oecho"); if (!earthEchoOn) em.classList.add("off");
|
||||
b.appendChild(em);
|
||||
@ -3473,9 +3502,9 @@
|
||||
let dragging = false, sy = 0, so = 0;
|
||||
wrap.addEventListener("pointerdown", (e) => { if (e.button !== 0) return; dragging = true; sy = e.clientY; so = (tw(key).offset || 0); try { wrap.setPointerCapture(e.pointerId); } catch (x) {} e.preventDefault(); });
|
||||
wrap.addEventListener("pointermove", (e) => { if (!dragging) return; setParamLocal(key, "offset", clamp(so + (sy - e.clientY) / 150, -1, 1)); });
|
||||
const end = (e) => { dragging = false; try { wrap.releasePointerCapture(e.pointerId); } catch (x) {} };
|
||||
const end = (e) => { dragging = false; try { wrap.releasePointerCapture(e.pointerId); } catch (x) {} if (zeroMode) zeroUI.persist(); }; // a knob shape is part of the zero build
|
||||
wrap.addEventListener("pointerup", end); wrap.addEventListener("pointercancel", end);
|
||||
wrap.ondblclick = () => setParamLocal(key, "offset", 0);
|
||||
wrap.ondblclick = () => { setParamLocal(key, "offset", 0); if (zeroMode) zeroUI.persist(); };
|
||||
}
|
||||
wrap.oncontextmenu = (e) => { e.preventDefault(); const n = dests.get(key); if (n) openCtxMenu(n, e.clientX, e.clientY); };
|
||||
this.knobs.push({ key, val, dot, isNote });
|
||||
@ -3484,6 +3513,7 @@
|
||||
update() {
|
||||
if (!this._open) return;
|
||||
const bpmEl = document.getElementById("omnibpm"); if (bpmEl) bpmEl.textContent = Math.round(godtime.bpm) + " bpm";
|
||||
if (this._chips) for (const [c, isOn] of this._chips) c.classList.toggle("on", !!isOn()); // GODSONIQ/♪ light when their async state lands (#8)
|
||||
for (const k of this.knobs) {
|
||||
const n = dests.get(k.key);
|
||||
const frac = n ? clamp(n.normDisp != null ? n.normDisp : (n.norm || 0), 0, 1) : 0;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user