diff --git a/viz/index.html b/viz/index.html
index 3325a12..240a88c 100644
--- a/viz/index.html
+++ b/viz/index.html
@@ -6793,11 +6793,18 @@
lfo = ctx.createOscillator(); lfo.frequency.value = 0.2;
const lfoAmt = ctx.createGain(); lfoAmt.gain.value = 260; lfo.connect(lfoAmt);
- const leadOsc = ctx.createOscillator(); leadOsc.type = "sawtooth";
+ // real wavetable lead: crossfade a mellow wave ↔ a bright (saw-rich) wave by wavetable.morph
const leadFilt = ctx.createBiquadFilter(); leadFilt.type = "lowpass"; leadFilt.frequency.value = 1200;
const leadAmp = ctx.createGain(); leadAmp.gain.value = 0;
- leadOsc.connect(leadFilt); leadFilt.connect(leadAmp); leadAmp.connect(preSat);
- lfoAmt.connect(leadFilt.frequency); leadOsc.start();
+ const _nH = 20, _rA = new Float32Array(_nH), _iA = new Float32Array(_nH), _rB = new Float32Array(_nH), _iB = new Float32Array(_nH);
+ for (let n = 1; n < _nH; n++) { _iA[n] = n <= 4 ? 0.9 / n : 0.03 / n; _iB[n] = 1 / n; } // A mellow · B bright
+ const leadA = ctx.createOscillator(); leadA.setPeriodicWave(ctx.createPeriodicWave(_rA, _iA));
+ const leadB = ctx.createOscillator(); leadB.setPeriodicWave(ctx.createPeriodicWave(_rB, _iB));
+ const morphA = ctx.createGain(); morphA.gain.value = 1;
+ const morphB = ctx.createGain(); morphB.gain.value = 0;
+ leadA.connect(morphA); leadB.connect(morphB); morphA.connect(leadFilt); morphB.connect(leadFilt);
+ leadFilt.connect(leadAmp); leadAmp.connect(preSat);
+ lfoAmt.connect(leadFilt.frequency); leadA.start(); leadB.start();
const bassOsc = ctx.createOscillator(); bassOsc.type = "square";
const bassFilt = ctx.createBiquadFilter(); bassFilt.type = "lowpass"; bassFilt.frequency.value = 500;
@@ -6846,7 +6853,7 @@
schuDrone.connect(schuDroneG); schuDroneG.connect(droneFilt); schuDrone.start();
lfo.start();
- return { out, preSat, glitch, revSend, fb, satDry, satWet, leadOsc, leadFilt, leadAmp,
+ return { out, preSat, glitch, revSend, fb, satDry, satWet, leadA, leadB, morphA, morphB, leadFilt, leadAmp,
bassOsc, bassAmp, padOscs, padFilt, padAmp, droneOscs, droneAmp,
echoDelay, echoFilt, echoFb, echoWet, echoFlutAmt, hissGain, chorusWet };
}
@@ -6858,19 +6865,20 @@
g.linearRampToValueAtTime(peak, t + 0.008);
g.setTargetAtTime(peak * 0.28, t + 0.01, dec);
}
- function noiseHit(freq, dur, vol) {
+ function noiseHit(freq, dur, vol, at) {
const n = Math.floor(ctx.sampleRate * dur), b = ctx.createBuffer(1, n, ctx.sampleRate), ch = b.getChannelData(0);
for (let i = 0; i < n; i++) ch[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / n, 3);
const s = ctx.createBufferSource(); s.buffer = b;
const hp = ctx.createBiquadFilter(); hp.type = "highpass"; hp.frequency.value = freq;
const g = ctx.createGain(); g.gain.value = vol;
- s.connect(hp); hp.connect(g); g.connect(N.preSat); s.start();
+ s.connect(hp); hp.connect(g); g.connect(N.preSat); s.start(at != null ? at : ctx.currentTime);
}
function params() {
const t = ctx.currentTime, S = (p, v, tc) => p.setTargetAtTime(v, t, tc || 0.1);
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);
+ const _wm = clamp(d("wavetable.morph", 0), 0, 1); // real wavetable: crossfade mellow ↔ bright
+ S(N.morphA.gain, 1 - _wm, 0.08); S(N.morphB.gain, _wm, 0.08);
S(N.padFilt.frequency, 220 + d("pad.brightness", 0) * 4200);
S(N.padAmp.gain, d("pad.brightness", 0) * 0.07, 0.3); // ×9 detuned saws → trimmed to hold the level
if (crusher) { // world-driven 12-bit-DAC crunch (default clean)
@@ -6899,7 +6907,7 @@
const minGap = 30 / Math.max(40, godtime.bpm); // one 8th note
const ln = Math.round(d("lead.note", 0));
if (ln >= 24 && ln !== leadNote) {
- leadNote = ln; N.leadOsc.frequency.setTargetAtTime(mtof(ln), t, 0.02);
+ leadNote = ln; N.leadA.frequency.setTargetAtTime(mtof(ln), t, 0.02); N.leadB.frequency.setTargetAtTime(mtof(ln), t, 0.02);
if (t - lastLeadT > minGap) { hit(N.leadAmp, 0.13, 0.35); lastLeadT = t; }
}
const bn = Math.round(d("bass.note", 0));
@@ -6919,7 +6927,9 @@
while (nextAt < now + 0.1) {
const t = Math.max(nextAt, now), dens = d("perc.density", 0);
if (Math.random() < dens * 0.9) noiseHit(7000, 0.05, 0.06 + dens * 0.07);
- if (step % 4 === 0 && Math.random() < 0.3 + d("granular.density", 0) * 0.4) noiseHit(2800, 0.03, 0.04);
+ const _gd = d("granular.density", 0); // real granular: a cloud of short scattered grains
+ for (let _g = 0, _ng = Math.floor(_gd * _gd * 6); _g < _ng; _g++)
+ noiseHit(1400 + Math.random() * 5000, 0.012 + Math.random() * 0.028, 0.02 + _gd * 0.03, t + Math.random() * six);
if (Math.random() < d("glitch", 0) * 0.18) {
const g = N.glitch.gain; g.setValueAtTime(1, t); g.setValueAtTime(0, t + 0.02); g.setValueAtTime(1, t + 0.055);
}