From fb32d1099183ce9634d733720c1b686f49aea95d Mon Sep 17 00:00:00 2001 From: type-two Date: Sat, 11 Jul 2026 01:26:01 +1000 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20seed=20the=20synth=20idle=20(not?= =?UTF-8?q?=200)=20for=20cableless=20tweaked=20voices=20=E2=80=94=20fix-ve?= =?UTF-8?q?rify=20regression?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fix-verify pass flagged one CONFIRMED regression in the #1 floor loop: it pinned any tweaked cableless non-note dest to a hard 0, but two dests have a non-zero synth idle — filter.cutoff (d(...,0.3)) and tempo.nudge (d(...,0.5)). So touching the filter knob in zero (or, before this, merely opening OMNI, which created a neutral tweak) pinned filter.cutoff to 0 → 250 Hz, darkening the lead. - new DEST_IDLE map; the floor loop seeds destVals[k]=DEST_IDLE[k]||0, so a neutral/reset knob returns a voice to its idle (filter 0.3), an offset rides from the idle, and mute still → 0. (verified: reset filter → 0.3 not 0; drag up → 0.767; mute → 0) - OMNI update() + knob pointerdown now read tweaks[k] directly instead of tw(k), so rendering/opening the panel no longer creates phantom neutral tweaks (which also kept the tweaks map — and saved dimensions — clean). (verified: open OMNI → filter has no tweak) A clean instrument (no tweaks) stays byte-identical; the public house patch was already masked (filter.cutoff is cabled to sun.speed). Deploy gate: GO. Console clean, syntax clean, 21 knobs, field healthy. Co-Authored-By: Claude Opus 4.8 --- viz/index.html | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/viz/index.html b/viz/index.html index 371522c..f16ee20 100644 --- a/viz/index.html +++ b/viz/index.html @@ -2448,6 +2448,10 @@ return v; } + // Synth-side idle for the two dests whose "no value" default isn't 0 (see the + // d("filter.cutoff",0.3) / d("tempo.nudge",0.5) fallbacks). The cableless-tweak + // floor seeds THIS, so a neutral/reset knob returns a voice to its idle, not to 0. + const DEST_IDLE = { "filter.cutoff": 0.3, "tempo.nudge": 0.5 }; let clientMatrix = false; // true once the house patch has been cloned in function computeDests() { if (!clientMatrix) return; @@ -2491,8 +2495,8 @@ 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) { // 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) + if (n.isNote || (k in destVals)) continue; // an offset floor gives it a level from its idle, and a + if (tweaks[k]) destVals[k] = DEST_IDLE[k] || 0; // reset/mute/lower re-emit too — seed the synth idle (not a blank 0), so a neutral filter/tempo doesn't pin dark } for (const k in destVals) { const tk = tweaks[k]; @@ -3500,7 +3504,7 @@ const val = wrap.querySelector(".val"), dot = wrap.querySelector(".dot"); if (!isNote) { 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("pointerdown", (e) => { if (e.button !== 0) return; dragging = true; sy = e.clientY; so = ((tweaks[key] && tweaks[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) {} if (zeroMode) zeroUI.persist(); }; // a knob shape is part of the zero build wrap.addEventListener("pointerup", end); wrap.addEventListener("pointercancel", end); @@ -3519,7 +3523,7 @@ const frac = n ? clamp(n.normDisp != null ? n.normDisp : (n.norm || 0), 0, 1) : 0; k.val.style.strokeDashoffset = (1000 * (1 - frac)).toFixed(1); if (!k.isNote) { - const off = tw(k.key).offset || 0; + const off = (tweaks[k.key] && tweaks[k.key].offset) || 0; // read-only — don't create a neutral tweak just by rendering const a = (135 + ((off + 1) / 2) * 270) * Math.PI / 180; k.dot.setAttribute("cx", (50 + 34 * Math.cos(a)).toFixed(1)); k.dot.setAttribute("cy", (50 + 34 * Math.sin(a)).toFixed(1));