summaryrefslogtreecommitdiff
path: root/tools/mq_editor/viewer.js
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-18 23:55:02 +0100
committerskal <pascal.massimino@gmail.com>2026-02-18 23:55:02 +0100
commit618bd1dc9af4beb98584ed817772951007017f79 (patch)
treedaa3c71762dee6f53fcf232669873f1531b14777 /tools/mq_editor/viewer.js
parente63f885c7caaf7496d01e37f8ed2769190f8a51e (diff)
fix(mq_editor): partial mini-spectrum — correct FFT, time selection, resonator sync
- Fix: fftRadix2 called without bitReversePermute → noisy spectrum (use fftForward) - specTime = mouse pos if inside partial [t0,t3], else center of partial interval - Cache check moved before canvas clear to keep spectrum visible outside [t0,t3] - viewer.synthOpts forwarded to synthesizeMQ so forceResonator/globalR/gain apply - invalidatePartialSpectrum() wired to forceResonator/forceRGain/globalR/globalGain handoff(Gemini): mini-spectrum now correct; fft bug fixed, resonator mode synced
Diffstat (limited to 'tools/mq_editor/viewer.js')
-rw-r--r--tools/mq_editor/viewer.js61
1 files changed, 32 insertions, 29 deletions
diff --git a/tools/mq_editor/viewer.js b/tools/mq_editor/viewer.js
index 82e9c24..1ed609c 100644
--- a/tools/mq_editor/viewer.js
+++ b/tools/mq_editor/viewer.js
@@ -54,6 +54,7 @@ class SpectrogramViewer {
this.partialSpectrumCanvas = document.getElementById('partialSpectrumCanvas');
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.)
// Selection and editing
this.selectedPartial = -1;
@@ -646,10 +647,10 @@ class SpectrogramViewer {
ctx.fillText(useSynth ? 'SYNTH [a]' : 'ORIG [a]', 4, 10);
}
- // Draw synthesized power spectrum of the selected partial at `time` into partialSpectrumCanvas.
+ // Draw synthesized power spectrum of the selected partial into partialSpectrumCanvas.
// X-axis: log frequency (same scale as main view). Y-axis: dB (normalised to peak).
- // force=true bypasses cache — used by render() when params change.
- // Otherwise cached on {partialIndex, time} for mouse-move performance.
+ // specTime = mouse time if inside partial's [t0,t3], else center of partial's interval.
+ // Cached on {partialIndex, specTime}; force=true bypasses cache (param changes, synth toggle).
renderPartialSpectrum(time, force = false) {
const ctx = this.partialSpectrumCtx;
if (!ctx) return;
@@ -659,34 +660,36 @@ class SpectrogramViewer {
const height = canvas.height;
const p = this.selectedPartial;
- // Cache check — skip if same partial+time unless forced by param change
+ const showMsg = (msg) => {
+ ctx.fillStyle = '#1e1e1e'; ctx.fillRect(0, 0, width, height);
+ ctx.font = '9px monospace'; ctx.fillStyle = '#333';
+ ctx.fillText(msg, 4, height / 2 + 4);
+ this._partialSpecCache = null;
+ };
+
+ if (p < 0 || !this.partials || p >= this.partials.length) { showMsg('no partial'); return; }
+
+ const partial = this.partials[p];
+ const curve = partial.freqCurve;
+ if (!curve) { showMsg('no curve'); return; }
+
+ // Use mouse time if inside partial's window, else center of partial
+ const specTime = (time >= curve.t0 && time <= curve.t3)
+ ? time
+ : (curve.t0 + curve.t3) / 2;
+
+ // Cache check — must happen before clearing the canvas
if (!force && this._partialSpecCache &&
this._partialSpecCache.partialIndex === p &&
- this._partialSpecCache.time === time) return;
+ this._partialSpecCache.time === specTime) return;
ctx.fillStyle = '#1e1e1e';
ctx.fillRect(0, 0, width, height);
ctx.font = '9px monospace';
- if (p < 0 || !this.partials || p >= this.partials.length) {
- ctx.fillStyle = '#333';
- ctx.fillText('no partial', 4, height / 2 + 4);
- this._partialSpecCache = {partialIndex: p, time};
- return;
- }
-
- const partial = this.partials[p];
- const curve = partial.freqCurve;
- if (!curve || time < curve.t0 || time > curve.t3) {
- ctx.fillStyle = '#333';
- ctx.fillText('out of range', 4, height / 2 + 4);
- this._partialSpecCache = {partialIndex: p, time};
- return;
- }
-
// Synthesize window → FFT → power spectrum
- const specData = this._computePartialSpectrum(partial, time);
- this._partialSpecCache = {partialIndex: p, time, specData};
+ const specData = this._computePartialSpectrum(partial, specTime);
+ this._partialSpecCache = {partialIndex: p, time: specTime, specData};
const {squaredAmp, maxDB, sampleRate, fftSize} = specData;
const numBins = fftSize / 2;
@@ -718,12 +721,12 @@ class SpectrogramViewer {
}
ctx.fillStyle = color;
- ctx.fillText('P#' + p + ' @' + time.toFixed(3) + 's', 4, 10);
+ ctx.fillText('P#' + p + ' @' + specTime.toFixed(3) + 's', 4, 10);
}
- // Synthesise a 2048-sample Hann-windowed frame of `partial` centred on `time`,
- // run FFT, and return {squaredAmp, maxDB, sampleRate, fftSize}.
- // freqCurve times are shifted so synthesizeMQ's t=0 aligns with tStart = time - window/2.
+ // Synthesise a 2048-sample Hann-windowed frame of `partial` centred on `time`, run FFT,
+ // 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) {
const sampleRate = this.audioBuffer.sampleRate;
const FFT_SIZE = 2048;
@@ -742,7 +745,7 @@ class SpectrogramViewer {
},
};
- const pcm = synthesizeMQ([shiftedPartial], sampleRate, windowDuration, true, {});
+ const pcm = synthesizeMQ([shiftedPartial], sampleRate, windowDuration, true, this.synthOpts);
// Hann window
for (let i = 0; i < FFT_SIZE; ++i) {
@@ -753,7 +756,7 @@ class SpectrogramViewer {
const real = new Float32Array(FFT_SIZE);
const imag = new Float32Array(FFT_SIZE);
for (let i = 0; i < FFT_SIZE; ++i) real[i] = pcm[i];
- fftRadix2(real, imag, FFT_SIZE, 1);
+ fftForward(real, imag, FFT_SIZE);
// Power spectrum
const squaredAmp = new Float32Array(FFT_SIZE / 2);