summaryrefslogtreecommitdiff
path: root/tools/mq_editor
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-19 00:32:54 +0100
committerskal <pascal.massimino@gmail.com>2026-02-19 00:32:54 +0100
commitc804808870cf3775362c02e40ea7d3d082ed0d91 (patch)
tree51b12188515b71cd369f0d682eb50c8ef01d599d /tools/mq_editor
parentdb5c023acd237d7015933bd21a5a6dbe5755841d (diff)
fix(mq_editor): jitter + central spectrum invalidationHEADmain
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>
Diffstat (limited to 'tools/mq_editor')
-rw-r--r--tools/mq_editor/app.js18
-rw-r--r--tools/mq_editor/mq_synth.js33
-rw-r--r--tools/mq_editor/viewer.js2
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;