summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--tools/mq_editor/editor.js35
-rw-r--r--tools/mq_editor/index.html106
-rw-r--r--tools/mq_editor/viewer.js55
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;