diff options
| author | skal <pascal.massimino@gmail.com> | 2026-02-18 10:17:07 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-02-18 10:17:07 +0100 |
| commit | 78faa77b208d20c01c242a942a1ddf9278f4ef17 (patch) | |
| tree | c2f74b31c24dbe7ee79e4a042c854f702a26e106 /tools | |
| parent | 6d2c3a9fa7ea3e7dc272d5622722f60d889612ce (diff) | |
feat(mq_editor): tabbed freq/amp/synth panel, spread band viz, UI polish
- Freq/Amp curve params now in tabs to save vertical space
- Add Synth tab: per-partial decay, jitter, spread_above/below controls
- Visualize spread band on selected partial (filled band + dashed bounds)
- Larger fonts and right panel (14px body, 260px panel width)
- Non-kept partials at 0.12 alpha (was 0.5)
- Default threshold -20dB (was -60dB)
handoff(Claude): mq_editor UI/UX improvements
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/mq_editor/editor.js | 35 | ||||
| -rw-r--r-- | tools/mq_editor/index.html | 106 | ||||
| -rw-r--r-- | tools/mq_editor/viewer.js | 55 |
3 files changed, 168 insertions, 28 deletions
diff --git a/tools/mq_editor/editor.js b/tools/mq_editor/editor.js index e1e70c6..441f787 100644 --- a/tools/mq_editor/editor.js +++ b/tools/mq_editor/editor.js @@ -11,6 +11,7 @@ class PartialEditor { this._ampTitle = document.getElementById('ampEditTitle'); this._freqGrid = document.getElementById('freqCurveGrid'); this._ampGrid = document.getElementById('ampCurveGrid'); + this._synthGrid = document.getElementById('synthGrid'); this._ampCtx = this._ampCanvas ? this._ampCanvas.getContext('2d') : null; // References set by host @@ -82,6 +83,7 @@ class PartialEditor { this._buildCurveGrid(this._freqGrid, partial, 'freqCurve', 'f', index); this._buildCurveGrid(this._ampGrid, partial, 'ampCurve', 'a', index); + this._buildSynthGrid(partial, index); } _buildCurveGrid(grid, partial, curveKey, valueLabel, partialIndex) { @@ -113,6 +115,39 @@ class PartialEditor { } } + _buildSynthGrid(partial, index) { + const grid = this._synthGrid; + grid.innerHTML = ''; + const defaults = { decay_alpha: 0.1, jitter: 0.05, spread_above: 0.02, spread_below: 0.02 }; + const rep = partial.replicas || {}; + const params = [ + { key: 'decay_alpha', label: 'decay', step: '0.001' }, + { key: 'jitter', label: 'jitter', step: '0.001' }, + { key: 'spread_above', label: 'spread ↑', step: '0.001' }, + { key: 'spread_below', label: 'spread ↓', step: '0.001' }, + ]; + for (const p of params) { + const val = rep[p.key] != null ? rep[p.key] : defaults[p.key]; + const lbl = document.createElement('span'); + lbl.textContent = p.label; + const inp = document.createElement('input'); + inp.type = 'number'; + inp.value = val.toFixed(3); + inp.step = p.step; + inp.min = '0'; + inp.addEventListener('change', (e) => { + if (!this.partials) return; + const v = parseFloat(e.target.value); + if (isNaN(v)) return; + if (!this.partials[index].replicas) + this.partials[index].replicas = { ...defaults }; + this.partials[index].replicas[p.key] = v; + }); + grid.appendChild(lbl); + grid.appendChild(inp); + } + } + _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 c5902a7..8f36314 100644 --- a/tools/mq_editor/index.html +++ b/tools/mq_editor/index.html @@ -6,6 +6,7 @@ <style> body { font-family: monospace; + font-size: 14px; margin: 20px; background: #1a1a1a; color: #ddd; @@ -73,9 +74,9 @@ border: 1px solid #555; border-radius: 4px; padding: 12px; - min-width: 220px; - max-width: 220px; - max-height: 600px; + min-width: 260px; + max-width: 260px; + max-height: 700px; overflow-y: auto; display: flex; flex-direction: column; @@ -83,7 +84,7 @@ box-sizing: border-box; } .panel-title { - font-size: 11px; + font-size: 12px; color: #888; text-transform: uppercase; letter-spacing: 1px; @@ -100,40 +101,52 @@ gap: 6px; margin: 0; cursor: pointer; - font-size: 13px; + font-size: 14px; } /* Partial properties */ .prop-row { display: flex; justify-content: space-between; align-items: baseline; + font-size: 13px; + padding: 2px 0; + } + .prop-label { color: #777; font-size: 12px; } + .curve-tabs { + display: flex; + gap: 2px; + margin-top: 8px; + margin-bottom: 4px; + } + .tab-btn { + flex: 1; + padding: 4px 0; font-size: 12px; - padding: 1px 0; + margin: 0; + background: #222; + border-color: #444; + color: #888; } - .prop-label { color: #777; font-size: 11px; } - .prop-section { - font-size: 10px; - color: #666; - text-transform: uppercase; - letter-spacing: 0.5px; - margin-top: 6px; - margin-bottom: 2px; + .tab-btn.active { + background: #3a3a3a; + border-color: #666; + color: #ddd; } .curve-grid { display: grid; grid-template-columns: 18px 1fr 1fr; - gap: 2px 3px; + gap: 3px 4px; align-items: center; } - .curve-grid span { color: #666; font-size: 10px; } + .curve-grid span { color: #666; font-size: 11px; } .curve-grid input[type="number"] { width: 100%; background: #333; color: #ccc; border: 1px solid #444; - padding: 1px 3px; + padding: 2px 4px; border-radius: 2px; - font-size: 10px; + font-size: 11px; font-family: monospace; box-sizing: border-box; } @@ -144,14 +157,33 @@ .partial-actions { display: flex; gap: 4px; - margin-top: 6px; + margin-top: 8px; } .partial-actions button { flex: 1; - padding: 3px 6px; - font-size: 11px; + padding: 4px 6px; + font-size: 12px; margin: 0; } + .synth-grid { + display: grid; + grid-template-columns: auto 1fr; + gap: 4px 8px; + align-items: center; + } + .synth-grid span { color: #888; font-size: 12px; } + .synth-grid input[type="number"] { + width: 100%; + background: #333; + color: #ccc; + border: 1px solid #444; + padding: 2px 4px; + border-radius: 2px; + font-size: 11px; + font-family: monospace; + box-sizing: border-box; + } + .synth-grid input[type="number"]:focus { border-color: #666; outline: none; } .synth-section { border-top: 1px solid #444; padding-top: 8px; @@ -188,7 +220,7 @@ <input type="number" id="hopSize" value="256" min="64" max="1024" step="64"> <label>Threshold (dB):</label> - <input type="number" id="threshold" value="-60" step="any"> + <input type="number" id="threshold" value="-20" step="any"> <label style="margin-left:16px;">Keep:</label> <input type="range" id="keepPct" min="1" max="100" value="100" style="width:100px; vertical-align:middle;"> @@ -234,17 +266,27 @@ <span class="prop-label">Time</span> <span id="propTime">—</span> </div> - <div class="prop-section">Freq Curve</div> - <div class="curve-grid" id="freqCurveGrid"></div> - <div class="prop-section">Amp Curve</div> - <div class="curve-grid" id="ampCurveGrid"></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</button> </div> </div> - <div id="noSelMsg" style="color:#555;font-size:11px;padding:2px 0;">Click a partial to select</div> + <div id="noSelMsg" style="color:#555;font-size:13px;padding:2px 0;">Click a partial to select</div> <!-- Synthesis options (always at bottom) --> <div class="synth-section"> @@ -543,6 +585,16 @@ } }); + // Curve tab switching + document.querySelectorAll('.tab-btn').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + document.querySelectorAll('.tab-pane').forEach(p => p.style.display = 'none'); + document.getElementById('tab' + btn.dataset.tab).style.display = ''; + }); + }); + // --- Test WAV peak validation --- function validateTestWAVPeaks(cache) { const SR = cache.sampleRate; diff --git a/tools/mq_editor/viewer.js b/tools/mq_editor/viewer.js index c158536..db23c72 100644 --- a/tools/mq_editor/viewer.js +++ b/tools/mq_editor/viewer.js @@ -227,7 +227,7 @@ class SpectrogramViewer { _renderPartial(p, partial, isSelected) { const {ctx} = this; const color = this.partialColor(p); - let alpha = isSelected ? 1.0 : (p < this.keepCount ? 1.0 : 0.5); + let alpha = isSelected ? 1.0 : (p < this.keepCount ? 1.0 : 0.12); if (partial.muted && !isSelected) alpha = 0.15; ctx.globalAlpha = alpha; @@ -247,6 +247,11 @@ class SpectrogramViewer { } if (started) ctx.stroke(); + // Spread band (selected only) + if (isSelected && partial.freqCurve) { + this._renderSpreadBand(partial, color); + } + // Bezier curve if (partial.freqCurve) { const curve = partial.freqCurve; @@ -276,6 +281,54 @@ class SpectrogramViewer { } } + _renderSpreadBand(partial, color) { + const {ctx} = this; + const curve = partial.freqCurve; + const rep = partial.replicas || {}; + const sa = rep.spread_above != null ? rep.spread_above : 0.02; + const sb = rep.spread_below != null ? rep.spread_below : 0.02; + + const STEPS = 60; + const upper = [], lower = []; + for (let i = 0; i <= STEPS; ++i) { + const t = curve.t0 + (curve.t3 - curve.t0) * i / STEPS; + if (t < this.t_view_min - 0.01 || t > this.t_view_max + 0.01) continue; + const f = evalBezier(curve, t); + upper.push([this.timeToX(t), this.freqToY(f * (1 + sa))]); + lower.push([this.timeToX(t), this.freqToY(f * (1 - sb))]); + } + if (upper.length < 2) return; + + const savedAlpha = ctx.globalAlpha; + + // Outer soft fill + 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]); + for (let i = lower.length - 1; i >= 0; --i) ctx.lineTo(lower[i][0], lower[i][1]); + ctx.closePath(); + ctx.fillStyle = color; + ctx.globalAlpha = 0.13; + ctx.fill(); + + // Dashed boundary lines + ctx.globalAlpha = 0.4; + ctx.strokeStyle = color; + ctx.lineWidth = 1; + ctx.setLineDash([3, 4]); + 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]); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(lower[0][0], lower[0][1]); + for (let i = 1; i < lower.length; ++i) ctx.lineTo(lower[i][0], lower[i][1]); + ctx.stroke(); + ctx.setLineDash([]); + + ctx.globalAlpha = savedAlpha; + } + renderPeaks() { const {ctx, frames} = this; if (!frames || frames.length === 0) return; |
