summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-19 06:46:57 +0100
committerskal <pascal.massimino@gmail.com>2026-02-19 06:46:57 +0100
commit83eec3cece795f56f4edc1298a008216cb9511a0 (patch)
treeab55930912d50b063838f92a2314598d6be0d291
parentc804808870cf3775362c02e40ea7d3d082ed0d91 (diff)
feat(mq_editor): UI revamp — params panel, layout, partial spectrum
- Move Synthesis controls (integratePhase, jitter, spread, resonator, LP/HP filters) and Auto Spread All into the ⚙ Params dropdown - Group Extract Partials / +Partial / ✕ Clear All in one toolbar group - Add per-partial Sine/Res mode toggle in the property panel (Mode row) - Move partial mini-spectrum below the right panel (right-col layout) - Partial mini-spectrum: dynamic dB range scanned across full duration (8 samples, [peak−60, peak]), cached on partial select - Print bezier amplitude A= in red at top-right of partial spectrum - Status/info messages set to 80% gray (#ccc) handoff(Claude): UI revamp complete, TODO items implemented. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--tools/mq_editor/TODO.md17
-rw-r--r--tools/mq_editor/editor.js32
-rw-r--r--tools/mq_editor/index.html154
-rw-r--r--tools/mq_editor/style.css24
-rw-r--r--tools/mq_editor/viewer.js36
5 files changed, 178 insertions, 85 deletions
diff --git a/tools/mq_editor/TODO.md b/tools/mq_editor/TODO.md
new file mode 100644
index 0000000..d85aa84
--- /dev/null
+++ b/tools/mq_editor/TODO.md
@@ -0,0 +1,17 @@
+# UI revamp
+
+- the Synthesis panel (Integrate Phase, etc, LP, MP) contains global parameters -> Should go under the top "Params" button
+
+- 'Auto Spread All' should go under the 'Params' button too. It's a global action.
+
+- group 'Extract Partials' button with '+Partial' and 'x Clear All' ones.
+
+- add a sine / resonator toggle per partial to toggle between sine-based synth and resonator. 'Resonator (all)' takes precedence.
+
+- the partial's mini-spectrum should go under the right panel, since it's related
+
+- the partial's mini-spectrum should have a fixed dB range ([-40dB,10dB]?) instead of a dynamic one, to see the impact of amplitude modulation per partial
+
+- the messages' color is too dark, make them 80% gray when appropriate
+
+- streamline the CSS and styling, if possible
diff --git a/tools/mq_editor/editor.js b/tools/mq_editor/editor.js
index 98c92e5..07312d5 100644
--- a/tools/mq_editor/editor.js
+++ b/tools/mq_editor/editor.js
@@ -86,6 +86,38 @@ class PartialEditor {
muteBtn.textContent = partial.muted ? 'Unmute' : 'Mute';
muteBtn.style.color = partial.muted ? '#fa4' : '';
+ // Mode toggle (quick sine/resonator switch, no tab required)
+ const modeToggle = document.getElementById('synthModeToggle');
+ if (modeToggle) {
+ modeToggle.innerHTML = '';
+ const isRes = !!(partial.resonator && partial.resonator.enabled);
+ const resDefaults = { r: 0.995, gainComp: 1.0 };
+ const btnS = document.createElement('button');
+ btnS.textContent = 'Sine';
+ btnS.className = 'tab-btn' + (isRes ? '' : ' active');
+ btnS.style.cssText = 'padding:2px 8px;font-size:11px;margin:0;';
+ const btnR = document.createElement('button');
+ btnR.textContent = 'Res';
+ btnR.className = 'tab-btn' + (isRes ? ' active' : '');
+ btnR.style.cssText = 'padding:2px 8px;font-size:11px;margin:0;';
+ btnS.onclick = () => {
+ if (!this.partials || !this.partials[index]) return;
+ if (!this.partials[index].resonator) this.partials[index].resonator = {...resDefaults};
+ this.partials[index].resonator.enabled = false;
+ this._updatePropPanel(index);
+ if (this.viewer) this.viewer.render();
+ };
+ btnR.onclick = () => {
+ if (!this.partials || !this.partials[index]) return;
+ if (!this.partials[index].resonator) this.partials[index].resonator = {...resDefaults};
+ this.partials[index].resonator.enabled = true;
+ this._updatePropPanel(index);
+ if (this.viewer) this.viewer.render();
+ };
+ modeToggle.appendChild(btnS);
+ modeToggle.appendChild(btnR);
+ }
+
this._buildCurveGrid(this._freqGrid, partial, 'freqCurve', 'f', index);
this._buildCurveGrid(this._ampGrid, partial, 'freqCurve', 'a', index, 'a');
this._buildSynthGrid(partial, index);
diff --git a/tools/mq_editor/index.html b/tools/mq_editor/index.html
index efbd73d..a03ab35 100644
--- a/tools/mq_editor/index.html
+++ b/tools/mq_editor/index.html
@@ -19,7 +19,8 @@
<span class="toolbar-sep"></span>
<span class="toolbar-group">
<button id="extractBtn" disabled>Extract Partials</button>
- <button id="autoSpreadAllBtn" disabled>Auto Spread All</button>
+ <button id="newPartialBtn" disabled>+ Partial</button>
+ <button id="clearAllBtn" disabled>✕ Clear All</button>
</span>
<span class="toolbar-sep"></span>
<span class="toolbar-group">
@@ -28,11 +29,6 @@
</span>
<span class="toolbar-sep"></span>
<span class="toolbar-group">
- <button id="newPartialBtn" disabled>+ Partial</button>
- <button id="clearAllBtn" disabled>✕ Clear All</button>
- </span>
- <span class="toolbar-sep"></span>
- <span class="toolbar-group">
<button id="exploreBtn" disabled>⊕ Explore</button>
<button id="contourBtn" disabled>≋ Contour</button>
</span>
@@ -71,6 +67,39 @@
<label title="Minimum number of frames a tracked partial must span. Shorter partials are discarded after tracking.">Min Len</label>
<input type="number" id="minLength" value="10" min="1" max="50" step="1" style="width:50px;">
</div>
+ <div class="param-group param-synth">
+ <span class="group-label">Synthesis</span>
+ <label><input type="checkbox" id="integratePhase" checked> Integrate phase</label>
+ <label><input type="checkbox" id="disableJitter"> Disable jitter</label>
+ <label><input type="checkbox" id="disableSpread"> Disable spread</label>
+ <label title="Test mode: force resonator synthesis for all partials (ignores per-partial mode setting)"><input type="checkbox" id="forceResonator"> Resonator (all)</label>
+ <div id="globalResParams" style="display:none;">
+ <label title="Global pole radius r in (0,1). Applied to all partials in resonator mode.">
+ r (pole)
+ <input type="range" id="globalR" min="0.75" max="0.9999" step="0.0001" value="0.995">
+ <span id="globalRVal" class="slider-val">0.9950</span>
+ </label>
+ <label title="Global gain compensation applied to all partials in resonator mode.">
+ gain
+ <input type="range" id="globalGain" min="0.0" max="4.0" step="0.01" value="1.0">
+ <span id="globalGainVal" class="slider-val">1.00</span>
+ </label>
+ <label title="Override per-partial r/gain with global values during playback"><input type="checkbox" id="forceRGain"> force r/gain</label>
+ </div>
+ <label title="LP filter coefficient k1 in (0,1]. 1.0 = bypass.">
+ LP k1
+ <input type="range" id="lpK1" min="0.001" max="1.0" step="0.001" value="1.0">
+ <span id="lpK1Val" class="slider-val">bypass</span>
+ </label>
+ <label title="HP filter coefficient k2 in (0,1]. 1.0 = bypass.">
+ HP k2
+ <input type="range" id="hpK2" min="0.001" max="1.0" step="0.001" value="1.0">
+ <span id="hpK2Val" class="slider-val">bypass</span>
+ </label>
+ <div style="padding-top:4px;">
+ <button id="autoSpreadAllBtn" disabled style="width:100%;padding:4px 8px;font-size:12px;margin:0;">Auto Spread All</button>
+ </div>
+ </div>
</div>
</span>
</div>
@@ -82,10 +111,6 @@
<canvas id="cursorCanvas" width="1400" height="600"></canvas>
<canvas id="playheadCanvas" width="1400" height="600"></canvas>
- <!-- Partial spectrum viewer (bottom-right overlay, left of main spectrum) -->
- <div id="partialSpectrumViewer">
- <canvas id="partialSpectrumCanvas" width="200" height="100"></canvas>
- </div>
<!-- Mini spectrum viewer (bottom-right overlay) -->
<div id="spectrumViewer">
<canvas id="spectrumCanvas" width="400" height="100"></canvas>
@@ -109,75 +134,52 @@
</div>
</div>
- <div class="right-panel">
- <!-- Partial properties (visible when a partial is selected) -->
- <div id="partialProps" style="display:none;">
- <div class="panel-title">
- <span id="propTitle">Partial #—</span>
- <span id="propSwatch"></span>
- </div>
- <div class="prop-row">
- <span class="prop-label">Peak</span>
- <span id="propPeak">—</span>
- </div>
- <div class="prop-row">
- <span class="prop-label">Time</span>
- <span id="propTime">—</span>
+ <div class="right-col">
+ <div class="right-panel">
+ <!-- Partial properties (visible when a partial is selected) -->
+ <div id="partialProps" style="display:none;">
+ <div class="panel-title">
+ <span id="propTitle">Partial #—</span>
+ <span id="propSwatch"></span>
+ </div>
+ <div class="prop-row">
+ <span class="prop-label">Peak</span>
+ <span id="propPeak">—</span>
+ </div>
+ <div class="prop-row">
+ <span class="prop-label">Time</span>
+ <span id="propTime">—</span>
+ </div>
+ <div class="prop-row">
+ <span class="prop-label">Mode</span>
+ <span id="synthModeToggle" style="display:flex;gap:3px;"></span>
+ </div>
+ <div class="curve-tabs">
+ <button class="tab-btn active" data-tab="Freq">Freq</button>
+ <button class="tab-btn" data-tab="Amp">Amp</button>
+ <button class="tab-btn" data-tab="Synth">Synth</button>
+ </div>
+ <div class="tab-pane" id="tabFreq">
+ <div class="curve-grid" id="freqCurveGrid"></div>
+ </div>
+ <div class="tab-pane" id="tabAmp" style="display:none;">
+ <div class="curve-grid" id="ampCurveGrid"></div>
+ </div>
+ <div class="tab-pane" id="tabSynth" style="display:none;">
+ <div class="synth-grid" id="synthGrid"></div>
+ </div>
+ <div class="partial-actions">
+ <button id="mutePartialBtn">Mute</button>
+ <button id="deletePartialBtn">Delete <kbd>Del</kbd></button>
+ </div>
</div>
- <div class="curve-tabs">
- <button class="tab-btn active" data-tab="Freq">Freq</button>
- <button class="tab-btn" data-tab="Amp">Amp</button>
- <button class="tab-btn" data-tab="Synth">Synth</button>
- </div>
- <div class="tab-pane" id="tabFreq">
- <div class="curve-grid" id="freqCurveGrid"></div>
- </div>
- <div class="tab-pane" id="tabAmp" style="display:none;">
- <div class="curve-grid" id="ampCurveGrid"></div>
- </div>
- <div class="tab-pane" id="tabSynth" style="display:none;">
- <div class="synth-grid" id="synthGrid"></div>
- </div>
- <div class="partial-actions">
- <button id="mutePartialBtn">Mute</button>
- <button id="deletePartialBtn">Delete <kbd>Del</kbd></button>
- </div>
- </div>
- <div id="noSelMsg">Click a partial to select</div>
+ <div id="noSelMsg">Click a partial to select</div>
+ </div>
- <!-- Synthesis options (always at bottom) -->
- <div class="synth-section">
- <div class="panel-title">Synthesis</div>
- <label><input type="checkbox" id="integratePhase" checked> Integrate phase</label>
- <label><input type="checkbox" id="disableJitter"> Disable jitter</label>
- <label><input type="checkbox" id="disableSpread"> Disable spread</label>
- <label title="Test mode: force resonator synthesis for all partials (ignores per-partial mode setting)"><input type="checkbox" id="forceResonator"> Resonator (all)</label>
- <div id="globalResParams" style="display:none;">
- <label title="Global pole radius r in (0,1). Applied to all partials in resonator mode.">
- r (pole)
- <input type="range" id="globalR" min="0.75" max="0.9999" step="0.0001" value="0.995">
- <span id="globalRVal" class="slider-val">0.9950</span>
- </label>
- <label title="Global gain compensation applied to all partials in resonator mode.">
- gain
- <input type="range" id="globalGain" min="0.0" max="4.0" step="0.01" value="1.0">
- <span id="globalGainVal" class="slider-val">1.00</span>
- </label>
- <label title="Override per-partial r/gain with global values during playback"><input type="checkbox" id="forceRGain"> force r/gain</label>
- </div>
- <div style="margin-top:6px;">
- <label title="LP filter coefficient k1 in (0,1]. 1.0 = bypass.">
- LP k1
- <input type="range" id="lpK1" min="0.001" max="1.0" step="0.001" value="1.0">
- <span id="lpK1Val" class="slider-val">bypass</span>
- </label>
- <label title="HP filter coefficient k2 in (0,1]. 1.0 = bypass.">
- HP k2
- <input type="range" id="hpK2" min="0.001" max="1.0" step="0.001" value="1.0">
- <span id="hpK2Val" class="slider-val">bypass</span>
- </label>
- </div>
+ <!-- Partial spectrum viewer (below right panel, since it's related) -->
+ <div id="partialSpectrumViewer">
+ <canvas id="partialSpectrumCanvas" width="258" height="100"></canvas>
</div>
</div>
</div>
diff --git a/tools/mq_editor/style.css b/tools/mq_editor/style.css
index 07a404a..2a9b5b7 100644
--- a/tools/mq_editor/style.css
+++ b/tools/mq_editor/style.css
@@ -12,6 +12,7 @@ input[type="file"] { display: none; }
.main-area { display: flex; align-items: flex-start; gap: 10px; margin-top: 10px; }
.canvas-col { display: flex; flex-direction: column; gap: 6px; flex-shrink: 0; }
.canvas-wrap { position: relative; }
+.right-col { display: flex; flex-direction: column; gap: 6px; }
/* === Toolbar === */
.toolbar { margin-bottom: 10px; padding: 10px; background: #2a2a2a; border-radius: 4px; display: flex; align-items: center; flex-wrap: wrap; gap: 4px; }
@@ -36,14 +37,25 @@ button.contour-active { background: #145; border-color: #0cc; color: #aff; }
.param-group:last-child { border-bottom: none; }
.group-label { grid-column: 1 / -1; font-size: 9px; color: #666; text-transform: uppercase; letter-spacing: 1px; }
+/* Synthesis param group (column layout) */
+.param-synth { display: block; padding: 8px 14px; border-bottom: 1px solid #333; }
+.param-synth:last-child { border-bottom: none; }
+.param-synth .group-label { display: block; margin-bottom: 4px; }
+.param-synth label { display: flex; align-items: center; gap: 6px; font-size: 12px; margin: 2px 0; cursor: pointer; white-space: nowrap; }
+.param-synth label input[type="range"] { flex: 1; min-width: 80px; max-width: 120px; }
+.param-synth #globalResParams { margin-top: 4px; padding: 4px 0 2px 12px; border-left: 2px solid #555; }
+
/* === Canvas & overlays === */
#canvas { border: 1px solid #555; background: #000; cursor: crosshair; display: block; flex-shrink: 0; }
#cursorCanvas, #playheadCanvas { position: absolute; top: 0; left: 0; pointer-events: none; }
-#partialSpectrumViewer { position: absolute; bottom: 10px; right: 420px; width: 200px; height: 100px; background: rgba(30,30,30,.9); border: 1px solid #555; border-radius: 3px; pointer-events: none; }
#spectrumViewer { position: absolute; bottom: 10px; right: 10px; width: 400px; height: 100px; background: rgba(30,30,30,.9); border: 1px solid #555; border-radius: 3px; pointer-events: none; }
#keepOverlay { position: absolute; bottom: 10px; left: 10px; background: rgba(30,30,30,.88); border: 1px solid #555; border-radius: 3px; padding: 4px 8px; display: flex; align-items: center; gap: 6px; font-size: 12px; color: #aaa; user-select: none; }
#keepOverlay input[type="range"] { width: 90px; }
+/* Partial spectrum (below right panel) */
+#partialSpectrumViewer { width: 260px; box-sizing: border-box; background: rgba(30,30,30,.9); border: 1px solid #555; border-radius: 3px; overflow: hidden; }
+#partialSpectrumCanvas { display: block; }
+
/* === Amp edit panel === */
#ampEditPanel { display: none; }
.amp-edit-header { font-size: 10px; color: #555; padding: 2px 0 3px 1px; display: flex; align-items: center; gap: 10px; text-transform: uppercase; letter-spacing: .5px; }
@@ -59,7 +71,7 @@ button.contour-active { background: #145; border-color: #0cc; color: #aff; }
#noSelMsg { color: #555; font-size: 13px; padding: 2px 0; }
/* === Partial properties === */
-.prop-row { display: flex; justify-content: space-between; align-items: baseline; font-size: 13px; padding: 2px 0; }
+.prop-row { display: flex; justify-content: space-between; align-items: center; font-size: 13px; padding: 2px 0; }
.prop-label { color: #777; font-size: 12px; }
#propSwatch { display: inline-block; width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0; }
.partial-actions { display: flex; gap: 4px; margin-top: 8px; }
@@ -78,12 +90,10 @@ kbd { font-size: 10px; opacity: 0.55; }
.synth-field-wrap input[type="number"]:focus,
.synth-grid input[type="number"]:focus { border-color: #666; outline: none; }
-/* === Synth section === */
-.synth-section { border-top: 1px solid #444; padding-top: 8px; margin-top: auto; }
+/* === Synth grid (per-partial synth tab) === */
.synth-grid { display: grid; grid-template-columns: auto 1fr; gap: 4px 8px; align-items: center; }
.synth-grid span { color: #888; font-size: 12px; }
.synth-field-wrap { display: flex; align-items: center; gap: 4px; }
-#globalResParams { margin-top: 4px; padding: 4px 0 2px 12px; border-left: 2px solid #555; }
.slider-val { width: 44px; text-align: right; }
/* === Jog slider === */
@@ -97,8 +107,8 @@ kbd { font-size: 10px; opacity: 0.55; }
/* === Status & tooltip === */
#fileLabel { font-size: 13px; color: #8af; opacity: .8; }
-#status { margin-top: 10px; padding: 8px; background: #2a2a2a; border-radius: 4px; min-height: 20px; }
-.info { color: #4af; }
+#status { margin-top: 10px; padding: 8px; background: #2a2a2a; border-radius: 4px; min-height: 20px; color: #ccc; }
+.info { color: #ccc; }
.warn { color: #fa4; }
.error { color: #f44; }
#tooltip { position: fixed; display: none; background: #2a2a2a; padding: 4px 8px; border: 1px solid #555; border-radius: 3px; pointer-events: none; font-size: 11px; z-index: 1000; }
diff --git a/tools/mq_editor/viewer.js b/tools/mq_editor/viewer.js
index 4744b96..1ac1afd 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._partialRangeCache = null; // {partialIndex, dbMin, dbMax} — scanned across full partial duration
this.synthOpts = {}; // synth options forwarded to synthesizeMQ (forceResonator, etc.)
this.onGetSynthOpts = null; // callback() → opts; called before each spectrum compute
@@ -179,6 +180,7 @@ class SpectrogramViewer {
selectPartial(index) {
this._partialSpecCache = null;
+ this._partialRangeCache = null;
this.selectedPartial = index;
this.render();
if (this.onPartialSelect) this.onPartialSelect(index);
@@ -691,7 +693,13 @@ class SpectrogramViewer {
const specData = this._computePartialSpectrum(partial, specTime);
this._partialSpecCache = {partialIndex: p, time: specTime, specData};
- const {squaredAmp, maxDB, sampleRate, fftSize} = specData;
+ // dB range: scanned across full partial duration, cached per partial
+ if (!this._partialRangeCache || this._partialRangeCache.partialIndex !== p) {
+ this._partialRangeCache = this._computePartialRange(p, partial);
+ }
+ const {dbMin: DB_MIN, dbMax: DB_MAX} = this._partialRangeCache;
+
+ const {squaredAmp, sampleRate, fftSize} = specData;
const numBins = fftSize / 2;
const binWidth = sampleRate / fftSize;
const color = this.partialColor(p);
@@ -710,7 +718,7 @@ class SpectrogramViewer {
for (let b = bStart; b <= bEnd; ++b) if (squaredAmp[b] > maxSq) maxSq = squaredAmp[b];
const magDB = 10 * Math.log10(Math.max(maxSq, 1e-20));
- const barH = Math.round(this.normalizeDB(magDB, maxDB) * (height - 12));
+ const barH = Math.round(Math.max(0, Math.min(1, (magDB - DB_MIN) / (DB_MAX - DB_MIN))) * (height - 12));
if (barH <= 0) continue;
const grad = ctx.createLinearGradient(0, height - barH, 0, height);
@@ -722,6 +730,12 @@ class SpectrogramViewer {
ctx.fillStyle = color;
ctx.fillText('P#' + p + ' @' + specTime.toFixed(3) + 's', 4, 10);
+
+ const amp = evalBezierAmp(curve, specTime);
+ ctx.fillStyle = '#f44';
+ ctx.textAlign = 'right';
+ ctx.fillText('A=' + amp.toFixed(3), width - 3, 10);
+ ctx.textAlign = 'left';
}
// Synthesise a 2048-sample Hann-windowed frame of `partial` centred on `time`, run FFT,
@@ -773,6 +787,24 @@ class SpectrogramViewer {
return {squaredAmp, maxDB, sampleRate, fftSize: FFT_SIZE};
}
+ // Scan the partial across its full duration to find the peak dB level, then derive
+ // [dbMin, dbMax] as [peak − 60, peak]. Cached per partialIndex; only called once on select.
+ _computePartialRange(partialIndex, partial) {
+ const fc = partial.freqCurve;
+ if (!fc) return {partialIndex, dbMin: -60, dbMax: 0};
+ const N = 8;
+ let globalMaxSq = 1e-20;
+ for (let i = 0; i < N; ++i) {
+ const t = fc.t0 + (fc.t3 - fc.t0) * (i + 0.5) / N;
+ const {squaredAmp} = this._computePartialSpectrum(partial, t);
+ for (let b = 0; b < squaredAmp.length; ++b) {
+ if (squaredAmp[b] > globalMaxSq) globalMaxSq = squaredAmp[b];
+ }
+ }
+ const dbMax = 10 * Math.log10(globalMaxSq);
+ return {partialIndex, dbMin: dbMax - 60, dbMax};
+ }
+
// --- View management ---
updateViewBounds() {