diff options
| author | skal <pascal.massimino@gmail.com> | 2026-02-19 00:32:54 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-02-19 00:32:54 +0100 |
| commit | c804808870cf3775362c02e40ea7d3d082ed0d91 (patch) | |
| tree | 51b12188515b71cd369f0d682eb50c8ef01d599d | |
| parent | db5c023acd237d7015933bd21a5a6dbe5755841d (diff) | |
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 <noreply@anthropic.com>
| -rw-r--r-- | tools/mq_editor/app.js | 18 | ||||
| -rw-r--r-- | tools/mq_editor/mq_synth.js | 33 | ||||
| -rw-r--r-- | tools/mq_editor/viewer.js | 2 |
3 files changed, 31 insertions, 22 deletions
diff --git a/tools/mq_editor/app.js b/tools/mq_editor/app.js index 380bb12..1c6d548 100644 --- a/tools/mq_editor/app.js +++ b/tools/mq_editor/app.js @@ -46,27 +46,20 @@ document.getElementById('hpK2').addEventListener('input', function() { document.getElementById('hpK2Val').textContent = k >= 1.0 ? 'bypass' : fmtHz(f); }); -function invalidatePartialSpectrum() { - if (!viewer) return; - viewer.synthOpts = getSynthParams().opts; - viewer._partialSpecCache = null; - viewer.renderPartialSpectrum(viewer.spectrumTime, true); -} - // Show/hide global resonator params when forceResonator toggled document.getElementById('forceResonator').addEventListener('change', function() { document.getElementById('globalResParams').style.display = this.checked ? '' : 'none'; - invalidatePartialSpectrum(); + if (viewer) viewer.render(); }); document.getElementById('globalR').addEventListener('input', function() { document.getElementById('globalRVal').textContent = parseFloat(this.value).toFixed(4); - invalidatePartialSpectrum(); + if (viewer) viewer.render(); }); document.getElementById('globalGain').addEventListener('input', function() { document.getElementById('globalGainVal').textContent = parseFloat(this.value).toFixed(2); - invalidatePartialSpectrum(); + if (viewer) viewer.render(); }); -document.getElementById('forceRGain').addEventListener('change', invalidatePartialSpectrum); +document.getElementById('forceRGain').addEventListener('change', () => { if (viewer) viewer.render(); }); let audioBuffer = null; let viewer = null; @@ -207,7 +200,8 @@ function loadAudioBuffer(buffer, label) { document.getElementById('exploreBtn').disabled = false; document.getElementById('contourBtn').disabled = false; editor.setViewer(viewer); - viewer.onPartialSelect = (i) => editor.onPartialSelect(i); + viewer.onGetSynthOpts = () => getSynthParams().opts; + viewer.onPartialSelect = (i) => editor.onPartialSelect(i); viewer.onRender = () => editor.onRender(); viewer.onBeforeChange = pushUndo; viewer.onExploreMove = (time, freq) => { 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; diff --git a/tools/mq_editor/viewer.js b/tools/mq_editor/viewer.js index c841acb..4744b96 100644 --- a/tools/mq_editor/viewer.js +++ b/tools/mq_editor/viewer.js @@ -55,6 +55,7 @@ class SpectrogramViewer { this.partialSpectrumCtx = this.partialSpectrumCanvas ? this.partialSpectrumCanvas.getContext('2d') : null; this._partialSpecCache = null; // {partialIndex, time, specData?} — see renderPartialSpectrum this.synthOpts = {}; // synth options forwarded to synthesizeMQ (forceResonator, etc.) + this.onGetSynthOpts = null; // callback() → opts; called before each spectrum compute // Selection and editing this.selectedPartial = -1; @@ -727,6 +728,7 @@ class SpectrogramViewer { // return {squaredAmp, maxDB, sampleRate, fftSize}. Uses this.synthOpts (forceResonator etc). // freqCurve times are shifted so synthesizeMQ's t=0 aligns with tStart = time − window/2. _computePartialSpectrum(partial, time) { + if (this.onGetSynthOpts) this.synthOpts = this.onGetSynthOpts(); const sampleRate = this.audioBuffer.sampleRate; const FFT_SIZE = 2048; const windowDuration = FFT_SIZE / sampleRate; |
