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