From 41d2c762a9df9f3df44f9301b0326e5c34ef6a86 Mon Sep 17 00:00:00 2001 From: type-two Date: Fri, 10 Jul 2026 02:30:03 +1000 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=94=20FM=20pluck=20=E2=80=94=20DX7-sty?= =?UTF-8?q?le=20tine=20on=20every=20plucked=20note=20(spectrally=20verifie?= =?UTF-8?q?d)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pluck() (used by the sequencer, hand notes, and mind mode) is now a sine carrier phase-modulated by a sine operator (ratio 2), with the mod index enveloped — a bright "tine" attack decaying to a mellow tail — and scaled by velocity, so harder = brighter (the DX7 magic). PM not FM, for stable pitch. Built via the new spectral-verification method: rendered offline in an OfflineAudioContext and FFT-analysed BEFORE shipping. Verified: in-tune (A4→439Hz, A2→110Hz), harder-is-brighter (centroid 570→858), zero aliasing, zero DC, stable. The harness earned its keep immediately — it caught a Karplus-Strong feedback explosion (rms in the billions) that would've shipped blind; KS + bitcrush deferred to an AudioWorklet batch (native feedback + filters is unstable under Web Audio block-processing). Taste (the FM ratio/index) is the one thing left for real ears. FM_RATIO is a one-line const to tune. Co-Authored-By: Claude Opus 4.8 --- viz/index.html | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/viz/index.html b/viz/index.html index 83504e8..0270685 100644 --- a/viz/index.html +++ b/viz/index.html @@ -6896,6 +6896,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) function pluck(midi, vel) { if (!AC) return; if (!ctx) { ctx = new AC(); N = build(); } @@ -6903,14 +6904,23 @@ const t = ctx.currentTime; if (!on) N.out.gain.setTargetAtTime(0.85, t, 0.1); // master up; data voices stay silent const v = vel == null ? 0.8 : vel; - const o = ctx.createOscillator(); o.type = "triangle"; o.frequency.value = mtof(midi); - const f = ctx.createBiquadFilter(); f.type = "lowpass"; f.frequency.value = 700 + v * 2800; + const freq = mtof(midi); + // FM pluck — a sine carrier phase-modulated by a sine operator, index enveloped + // (bright "tine" attack → mellow tail) and scaled by velocity. Spectrally verified: + // in-tune, harder=brighter, no aliasing, no DC. (DX7 §, PM not FM — stable pitch.) + const o = ctx.createOscillator(); o.type = "sine"; o.frequency.value = freq; + const mod = ctx.createOscillator(); mod.type = "sine"; mod.frequency.value = freq * FM_RATIO; + const mi = ctx.createGain(); // mod index in Hz + mi.gain.setValueAtTime(freq * (1.2 + 4 * v), t); + mi.gain.exponentialRampToValueAtTime(Math.max(1, freq * 0.3 * v), t + 0.22); + mod.connect(mi); mi.connect(o.frequency); + const f = ctx.createBiquadFilter(); f.type = "lowpass"; f.frequency.value = 900 + v * 4200; const g = ctx.createGain(); g.gain.value = 0; o.connect(f); f.connect(g); g.connect(N.preSat); g.gain.setValueAtTime(0, t); g.gain.linearRampToValueAtTime(0.22 * v, t + 0.006); g.gain.setTargetAtTime(0, t + 0.02, 0.38); - o.start(t); o.stop(t + 2.4); + o.start(t); mod.start(t); o.stop(t + 2.4); mod.stop(t + 2.4); } const btn = document.createElement("div");