From d1fa247ad84dbfcdb0110d56159b1b259d106e3f Mon Sep 17 00:00:00 2001 From: type-two Date: Fri, 10 Jul 2026 02:41:15 +1000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=B9=20supersaw=20pad=20+=20brighter=20?= =?UTF-8?q?FM=20tine=20(spectrally=20verified)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Supersaw: the pad's 3 chord tones each become a detuned saw cluster (9 saws, ±14¢ spread) — the lush JP-8000 wall. Verified offline: lush (133 partials), no clip (peak 0.29), no aliasing, no DC; padAmp trimmed 0.10→0.07 for the extra voices. Retuning loop + level updated for the new {o,iv} structure. FM finish: bumped FM_RATIO 2→3 (the classic bright DX7 tine). Verified alias-free across the whole register — even ratio 4 at A6/hard aliases <0.2% — so brightness is pure taste, not a correctness risk. One-line const if you want warmer (2) or glassier (4). Co-Authored-By: Claude Opus 4.8 --- viz/index.html | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/viz/index.html b/viz/index.html index 0270685..22e5d25 100644 --- a/viz/index.html +++ b/viz/index.html @@ -6772,10 +6772,14 @@ 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); - const padOscs = [0, 7, 16].map((iv, i) => { - const o = ctx.createOscillator(); o.type = "sawtooth"; o.detune.value = (i - 1) * 4; - o.frequency.value = mtof(50 + iv); o.connect(padFilt); o.start(); return o; - }); + // 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 = []; + for (const iv of [0, 7, 16]) for (const s of [-1, 0, 1]) { + const o = ctx.createOscillator(); o.type = "sawtooth"; o.detune.value = s * 14; // ±14¢ supersaw spread + o.frequency.value = mtof(50 + iv); o.connect(padFilt); o.start(); + padOscs.push({ o, iv }); + } const droneFilt = ctx.createBiquadFilter(); droneFilt.type = "lowpass"; droneFilt.frequency.value = 380; const droneAmp = ctx.createGain(); droneAmp.gain.value = 0; @@ -6832,7 +6836,7 @@ S(N.leadFilt.frequency, 250 + d("filter.cutoff", 0.3) * 5000, 0.05); N.leadFilt.Q.setTargetAtTime(1 + d("wavetable.morph", 0) * 8, t, 0.1); S(N.padFilt.frequency, 220 + d("pad.brightness", 0) * 4200); - S(N.padAmp.gain, d("pad.brightness", 0) * 0.10, 0.3); + S(N.padAmp.gain, d("pad.brightness", 0) * 0.07, 0.3); // ×9 detuned saws → trimmed to hold the level S(N.droneAmp.gain, d("drone.voices", 0) * 0.08, 0.3); S(N.revSend.gain, (d("reverb.size", 0) * 0.5 + d("master.space", 0) * 0.5) * 0.30, 0.3); S(N.fb.gain, Math.min(0.55, d("delay.feedback", 0) * 0.6), 0.2); @@ -6861,7 +6865,7 @@ if (bn >= 20 && bn !== bassNote && t - lastBassT > minGap * 2) { lastBassT = t; bassNote = bn; N.bassOsc.frequency.setTargetAtTime(mtof(bn), t, 0.03); hit(N.bassAmp, 0.20, 0.6); - N.padOscs.forEach((o, i) => o.frequency.setTargetAtTime(mtof(bn + 12 + [0, 7, 16][i]), t, 0.5)); + N.padOscs.forEach(p => p.o.frequency.setTargetAtTime(mtof(bn + 12 + p.iv), t, 0.5)); N.droneOscs.forEach((o, i) => o.frequency.setTargetAtTime(mtof(bn - 12 + [0, 7][i]), t, 0.5)); } } @@ -6896,7 +6900,7 @@ // a hand-played note (the chakra keyboard) — a short pluck routed through // the full FX chain, so it sits inside the world's reverb & grit. Wakes // the audio graph on first strike if ♪ hasn't been pressed. - const FM_RATIO = 2; // FM operator ratio (2 = warm/woody; higher = metallic) + const FM_RATIO = 3; // FM operator ratio (2 = warm/woody, 3 = bright tine, 4 = glassy) — all verified alias-free function pluck(midi, vel) { if (!AC) return; if (!ctx) { ctx = new AC(); N = build(); }