From c804808870cf3775362c02e40ea7d3d082ed0d91 Mon Sep 17 00:00:00 2001 From: skal Date: Thu, 19 Feb 2026 00:32:54 +0100 Subject: fix(mq_editor): jitter + central spectrum invalidation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mq_synth.js: - jitter was only used as a static initial phase offset (inaudible); now drives per-sample LCG frequency perturbation (±jitter fraction of instantaneous freq) in both sinusoidal (integratePhase path) and resonator modes (separate jitterSeed, independent from noise excitation) - disableJitter option now correctly gates jitter to 0 in both modes (was never read before) viewer.js / app.js: - remove invalidatePartialSpectrum() and onResonatorParamChange callback; replace with viewer.onGetSynthOpts callback, called inside _computePartialSpectrum to pull fresh synthOpts at compute time - all UI changes (resonator r/gain, forceResonator, globalR/gain, forceRGain, sinusoidal params) now use viewer.render() as the single invalidation path — no more split between render() and invalidatePartialSpectrum() handoff(Gemini): jitter active on both synth modes; spectrum always sees fresh synthOpts via onGetSynthOpts; viewer.render() is the only invalidation path needed. Co-Authored-By: Claude Sonnet 4.6 --- tools/mq_editor/mq_synth.js | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) (limited to 'tools/mq_editor/mq_synth.js') diff --git a/tools/mq_editor/mq_synth.js b/tools/mq_editor/mq_synth.js index eeb3b00..1029626 100644 --- a/tools/mq_editor/mq_synth.js +++ b/tools/mq_editor/mq_synth.js @@ -32,6 +32,7 @@ function buildHarmonics(harmonics) { // false = 2π*f*t (simpler, only correct for constant freq) // options.k1: LP coefficient in (0,1] — omit to bypass // options.k2: HP coefficient in (0,1] — omit to bypass +// options.disableJitter: true = suppress per-sample frequency jitter function synthesizeMQ(partials, sampleRate, duration, integratePhase = true, options = {}) { const numSamples = Math.floor(sampleRate * duration); const pcm = new Float32Array(numSamples); @@ -61,34 +62,38 @@ function synthesizeMQ(partials, sampleRate, duration, integratePhase = true, opt : (res.gainComp != null ? res.gainComp : 1.0); const gainNorm = Math.sqrt(Math.max(0, 1.0 - r * r)); - // Build harmonic list (jitter/spread not applied to resonator) + // Build harmonic list (spread not applied to resonator; jitter modulates center freq) const harm = partial.harmonics || defaultHarmonics; const harmonicList = buildHarmonics(harm); + const jitter = options.disableJitter ? 0.0 : (harm.jitter ?? 0.0); configs.push({ mode: 'resonator', fc, r, gainComp, gainNorm, harmonicList, + jitter, y1: new Float64Array(harmonicList.length), y2: new Float64Array(harmonicList.length), - noiseSeed: ((p * 1664525 + 1013904223) & 0xFFFFFFFF) >>> 0 + noiseSeed: ((p * 1664525 + 1013904223) & 0xFFFFFFFF) >>> 0, + jitterSeed: ((p * 6364136223 + 1442695040) & 0xFFFFFFFF) >>> 0 }); } else { // --- Sinusoidal (harmonic) mode --- const harm = partial.harmonics || defaultHarmonics; const spread = harm.spread ?? 0.0; - const jitter = harm.jitter ?? 0.0; + const jitter = options.disableJitter ? 0.0 : (harm.jitter ?? 0.0); const harmonicList = buildHarmonics(harm); const replicaData = []; for (let h = 0; h < harmonicList.length; ++h) { - const hc = harmonicList[h]; - const spreadVal = randFloat(p * 67890 + h * 999, -spread, spread); - const initPhase = randFloat(p * 67890 + h, 0.0, 1.0) * jitter * 2.0 * Math.PI; - replicaData.push({ ratio: hc.ratio, ampMult: hc.ampMult, spread: spreadVal, phase: initPhase }); + const hc = harmonicList[h]; + const spreadVal = randFloat(p * 67890 + h * 999, -spread, spread); + const initPhase = randFloat(p * 67890 + h, 0.0, 1.0) * 2.0 * Math.PI; + const jitterSeed = ((p * 12345 + h * 67890 + 999) & 0xFFFFFFFF) >>> 0; + replicaData.push({ ratio: hc.ratio, ampMult: hc.ampMult, spread: spreadVal, phase: initPhase, jitterSeed }); } - configs.push({ mode: 'sinusoid', fc, replicaData }); + configs.push({ mode: 'sinusoid', fc, replicaData, jitter }); } } @@ -112,9 +117,14 @@ function synthesizeMQ(partials, sampleRate, duration, integratePhase = true, opt cfg.noiseSeed = (Math.imul(1664525, cfg.noiseSeed) + 1013904223) >>> 0; const noise = cfg.noiseSeed / 0x100000000 * 2.0 - 1.0; + // Per-sample frequency jitter on resonator center freq + cfg.jitterSeed = (Math.imul(1664525, cfg.jitterSeed) + 1013904223) >>> 0; + const jNoise = cfg.jitterSeed / 0x100000000 * 2.0 - 1.0; + const f0j = f0 * (1.0 + jNoise * cfg.jitter); + for (let h = 0; h < cfg.harmonicList.length; ++h) { const hc = cfg.harmonicList[h]; - const fh = f0 * hc.ratio; + const fh = f0j * hc.ratio; const omega = 2.0 * Math.PI * fh / sampleRate; const b1 = 2.0 * cfg.r * Math.cos(omega); @@ -138,7 +148,10 @@ function synthesizeMQ(partials, sampleRate, duration, integratePhase = true, opt let phase; if (integratePhase) { - rep.phase += 2.0 * Math.PI * f / sampleRate; + // Per-sample frequency jitter: ±jitter fraction of instantaneous freq + rep.jitterSeed = (Math.imul(1664525, rep.jitterSeed) + 1013904223) >>> 0; + const jNoise = rep.jitterSeed / 0x100000000 * 2.0 - 1.0; + rep.phase += 2.0 * Math.PI * f / sampleRate * (1.0 + jNoise * cfg.jitter); phase = rep.phase; } else { phase = 2.0 * Math.PI * f * t + rep.phase; -- cgit v1.2.3