🔔 FM pluck — DX7-style tine on every plucked note (spectrally verified)

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 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-10 02:30:03 +10:00
parent 9e5de4f55f
commit 41d2c762a9

View File

@ -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");