summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-18 11:05:37 +0100
committerskal <pascal.massimino@gmail.com>2026-02-18 11:05:37 +0100
commit890f4fdf96945832d5da078cb795266127cf122d (patch)
treeecfc93c2ad41df0787aa471d8f0be4991160f683
parentf8f664964594a341884b2e9947f64feea4b925a6 (diff)
feat(mq_editor): jog sliders for synth params, reset partials on WAV load, panel refresh after extract
- Clear extractedPartials and editor state when loading a new WAV - After extract, refresh right panels (re-select if index still valid) - Synth fields (decay, jitter, spread) get jog sliders: drag to nudge, spring-back on release - Spread extension limit dashed line: alpha 0.4→0.75, lineWidth 1→1.5, dash [3,4]→[4,3] handoff(Gemini): mq_editor UX polish — jog sliders, WAV reset, panel refresh, spread line visibility Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--tools/mq_editor/editor.js55
-rw-r--r--tools/mq_editor/index.html49
-rw-r--r--tools/mq_editor/viewer.js6
3 files changed, 103 insertions, 7 deletions
diff --git a/tools/mq_editor/editor.js b/tools/mq_editor/editor.js
index 9cfcdf1..b767534 100644
--- a/tools/mq_editor/editor.js
+++ b/tools/mq_editor/editor.js
@@ -146,8 +146,15 @@ class PartialEditor {
if (this.viewer) this.viewer.render();
});
inputs[p.key] = inp;
+
+ const jog = this._makeJogSlider(inp, partial, index, p, defaults);
+ const wrap = document.createElement('div');
+ wrap.className = 'synth-field-wrap';
+ wrap.appendChild(inp);
+ wrap.appendChild(jog);
+
grid.appendChild(lbl);
- grid.appendChild(inp);
+ grid.appendChild(wrap);
}
// Auto-detect spread button
@@ -173,6 +180,52 @@ class PartialEditor {
grid.appendChild(autoBtn);
}
+ _makeJogSlider(inp, partial, index, p, defaults) {
+ const slider = document.createElement('div');
+ slider.className = 'jog-slider';
+ const thumb = document.createElement('div');
+ thumb.className = 'jog-thumb';
+ slider.appendChild(thumb);
+
+ const sensitivity = parseFloat(p.step) * 5;
+ let startX = 0, startVal = 0, dragging = false;
+
+ const onMove = (e) => {
+ if (!dragging) return;
+ const dx = e.clientX - startX;
+ const half = slider.offsetWidth / 2;
+ const clamped = Math.max(-half, Math.min(half, dx));
+ thumb.style.transition = 'none';
+ thumb.style.left = `calc(50% - 3px + ${clamped}px)`;
+ const newVal = Math.max(0, startVal + dx * sensitivity);
+ inp.value = newVal.toFixed(3);
+ if (!this.partials || !this.partials[index]) return;
+ if (!this.partials[index].replicas) this.partials[index].replicas = { ...defaults };
+ this.partials[index].replicas[p.key] = newVal;
+ if (this.viewer) this.viewer.render();
+ };
+
+ const onUp = () => {
+ if (!dragging) return;
+ dragging = false;
+ thumb.style.transition = '';
+ thumb.style.left = 'calc(50% - 3px)';
+ document.removeEventListener('mousemove', onMove);
+ document.removeEventListener('mouseup', onUp);
+ };
+
+ slider.addEventListener('mousedown', (e) => {
+ dragging = true;
+ startX = e.clientX;
+ startVal = Math.max(0, parseFloat(inp.value) || 0);
+ document.addEventListener('mousemove', onMove);
+ document.addEventListener('mouseup', onUp);
+ e.preventDefault();
+ });
+
+ return slider;
+ }
+
_makeCurveUpdater(partialIndex, curveKey, field, pointIndex) {
return (e) => {
if (!this.partials) return;
diff --git a/tools/mq_editor/index.html b/tools/mq_editor/index.html
index 5e1e5c4..fa7543c 100644
--- a/tools/mq_editor/index.html
+++ b/tools/mq_editor/index.html
@@ -172,8 +172,14 @@
align-items: center;
}
.synth-grid span { color: #888; font-size: 12px; }
- .synth-grid input[type="number"] {
- width: 100%;
+ .synth-field-wrap {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ }
+ .synth-field-wrap input[type="number"] {
+ flex: 1;
+ min-width: 0;
background: #333;
color: #ccc;
border: 1px solid #444;
@@ -183,6 +189,37 @@
font-family: monospace;
box-sizing: border-box;
}
+ .synth-field-wrap input[type="number"]:focus { border-color: #666; outline: none; }
+ .jog-slider {
+ width: 44px;
+ height: 16px;
+ background: #1e1e1e;
+ border: 1px solid #444;
+ border-radius: 3px;
+ cursor: ew-resize;
+ position: relative;
+ flex-shrink: 0;
+ user-select: none;
+ overflow: hidden;
+ }
+ .jog-slider::before {
+ content: '';
+ position: absolute;
+ left: 50%; top: 3px; bottom: 3px;
+ width: 1px;
+ background: #484848;
+ transform: translateX(-50%);
+ }
+ .jog-thumb {
+ position: absolute;
+ top: 3px; bottom: 3px;
+ width: 6px;
+ background: #888;
+ border-radius: 2px;
+ left: calc(50% - 3px);
+ transition: left 0.12s ease;
+ }
+ .jog-slider:hover .jog-thumb { background: #aaa; }
.synth-grid input[type="number"]:focus { border-color: #666; outline: none; }
.synth-section {
border-top: 1px solid #444;
@@ -362,6 +399,10 @@
playBtn.disabled = false;
setStatus('Computing STFT cache...', 'info');
+ // Reset partials from previous file
+ extractedPartials = null;
+ editor.setPartials(null);
+
setTimeout(() => {
const signal = audioBuffer.getChannelData(0);
stftCache = new STFTCache(signal, audioBuffer.sampleRate, fftSize, Math.max(64, parseInt(hopSize.value) || 64));
@@ -458,7 +499,9 @@
setStatus(`Extracted ${result.partials.length} partials`, 'info');
viewer.setPartials(result.partials);
viewer.setKeepCount(getKeepCount());
- viewer.selectPartial(-1);
+ // Refresh panels: re-select if index still valid, else clear
+ const prevSel = viewer.selectedPartial;
+ viewer.selectPartial(prevSel >= 0 && prevSel < result.partials.length ? prevSel : -1);
} catch (err) {
setStatus('Extraction error: ' + err.message, 'error');
diff --git a/tools/mq_editor/viewer.js b/tools/mq_editor/viewer.js
index 3b2e1b2..76c57e2 100644
--- a/tools/mq_editor/viewer.js
+++ b/tools/mq_editor/viewer.js
@@ -312,10 +312,10 @@ class SpectrogramViewer {
ctx.fill();
// Dashed boundary lines
- ctx.globalAlpha = 0.4;
+ ctx.globalAlpha = 0.75;
ctx.strokeStyle = color;
- ctx.lineWidth = 1;
- ctx.setLineDash([3, 4]);
+ ctx.lineWidth = 1.5;
+ ctx.setLineDash([4, 3]);
ctx.beginPath();
ctx.moveTo(upper[0][0], upper[0][1]);
for (let i = 1; i < upper.length; ++i) ctx.lineTo(upper[i][0], upper[i][1]);