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(); }