summaryrefslogtreecommitdiff
path: root/tools/mq_editor/editor.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/mq_editor/editor.js')
-rw-r--r--tools/mq_editor/editor.js178
1 files changed, 160 insertions, 18 deletions
diff --git a/tools/mq_editor/editor.js b/tools/mq_editor/editor.js
index 157186a..f57e177 100644
--- a/tools/mq_editor/editor.js
+++ b/tools/mq_editor/editor.js
@@ -62,7 +62,10 @@ class PartialEditor {
const partial = this.partials[index];
const color = this.viewer ? this.viewer.partialColor(index) : '#888';
- document.getElementById('propTitle').textContent = 'Partial #' + index;
+ const titleEl = document.getElementById('propTitle');
+ const badge = partial.resonator && partial.resonator.enabled
+ ? ' <span class="res-badge">RES</span>' : '';
+ titleEl.innerHTML = 'Partial #' + index + badge;
document.getElementById('propSwatch').style.background = color;
let peakAmp = 0, peakIdx = 0;
@@ -118,17 +121,49 @@ 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 repDefaults = { decay_alpha: 0.1, jitter: 0.05, spread_above: 0.02, spread_below: 0.02 };
+ const resDefaults = { r: 0.995, gainComp: 1.0 };
+
+ const isResonator = !!(partial.resonator && partial.resonator.enabled);
+
+ // --- Mode toggle ---
+ const modeLbl = document.createElement('span');
+ modeLbl.textContent = 'mode';
+
+ const modeWrap = document.createElement('div');
+ modeWrap.style.cssText = 'display:flex;gap:3px;';
+
+ const btnSin = document.createElement('button');
+ btnSin.textContent = 'Sinusoid';
+ btnSin.className = 'tab-btn' + (isResonator ? '' : ' active');
+ btnSin.style.cssText = 'flex:1;padding:2px 4px;font-size:11px;margin:0;';
+
+ const btnRes = document.createElement('button');
+ btnRes.textContent = 'Resonator';
+ btnRes.className = 'tab-btn' + (isResonator ? ' active' : '');
+ btnRes.style.cssText = 'flex:1;padding:2px 4px;font-size:11px;margin:0;';
+
+ modeWrap.appendChild(btnSin);
+ modeWrap.appendChild(btnRes);
+ grid.appendChild(modeLbl);
+ grid.appendChild(modeWrap);
+
+ // --- Sinusoid section ---
+ const sinSection = document.createElement('div');
+ sinSection.style.cssText = 'display:contents;';
+ sinSection.dataset.section = 'sinusoid';
+
const rep = partial.replicas || {};
- const params = [
+ const sinParams = [
{ 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' },
];
- const inputs = {};
- for (const p of params) {
- const val = rep[p.key] != null ? rep[p.key] : defaults[p.key];
+ const sinInputs = {};
+ for (const p of sinParams) {
+ const val = rep[p.key] != null ? rep[p.key] : repDefaults[p.key];
const lbl = document.createElement('span');
lbl.textContent = p.label;
const inp = document.createElement('input');
@@ -140,21 +175,19 @@ class PartialEditor {
if (!this.partials) return;
const v = parseFloat(e.target.value);
if (isNaN(v)) return;
- if (!this.partials[index].replicas)
- this.partials[index].replicas = { ...defaults };
+ if (!this.partials[index].replicas) this.partials[index].replicas = { ...repDefaults };
this.partials[index].replicas[p.key] = v;
if (this.viewer) this.viewer.render();
});
- inputs[p.key] = inp;
+ sinInputs[p.key] = inp;
- const jog = this._makeJogSlider(inp, partial, index, p, defaults);
+ const jog = this._makeJogSlider(inp, partial, index, p, repDefaults);
const wrap = document.createElement('div');
wrap.className = 'synth-field-wrap';
wrap.appendChild(inp);
wrap.appendChild(jog);
-
- grid.appendChild(lbl);
- grid.appendChild(wrap);
+ sinSection.appendChild(lbl);
+ sinSection.appendChild(wrap);
}
// Auto-detect spread button
@@ -170,14 +203,123 @@ class PartialEditor {
const sr = this.viewer ? this.viewer.audioBuffer.sampleRate : 44100;
const fs = sc ? sc.fftSize : 2048;
const {spread_above, spread_below} = autodetectSpread(p, sc, fs, sr);
- if (!p.replicas) p.replicas = { ...defaults };
+ if (!p.replicas) p.replicas = { ...repDefaults };
p.replicas.spread_above = spread_above;
p.replicas.spread_below = spread_below;
- inputs['spread_above'].value = spread_above.toFixed(4);
- inputs['spread_below'].value = spread_below.toFixed(4);
+ sinInputs['spread_above'].value = spread_above.toFixed(4);
+ sinInputs['spread_below'].value = spread_below.toFixed(4);
+ });
+ sinSection.appendChild(autoLbl);
+ sinSection.appendChild(autoBtn);
+
+ // --- Resonator section ---
+ const resSection = document.createElement('div');
+ resSection.style.cssText = 'display:contents;';
+ resSection.dataset.section = 'resonator';
+
+ const resObj = partial.resonator || {};
+ const resParams = [
+ { key: 'r', label: 'r (pole)', step: '0.001', min: '0', max: '0.9999',
+ title: 'Pole radius. r→1 = narrow bandwidth / long ring. r→0 = wide / fast decay.' },
+ { key: 'gainComp', label: 'gain', step: '0.01', min: '0', max: '100',
+ title: 'Output gain multiplier (gainNorm=√(1-r²) normalises power; use this to trim level).' },
+ ];
+ for (const p of resParams) {
+ const val = resObj[p.key] != null ? resObj[p.key] : resDefaults[p.key];
+ const lbl = document.createElement('span');
+ lbl.textContent = p.label;
+ lbl.title = p.title || '';
+ const inp = document.createElement('input');
+ inp.type = 'number';
+ inp.value = val.toFixed(4);
+ inp.step = p.step;
+ inp.min = p.min || '0';
+ inp.max = p.max || '';
+ inp.title = p.title || '';
+ inp.addEventListener('change', (e) => {
+ if (!this.partials) return;
+ const v = parseFloat(e.target.value);
+ if (isNaN(v)) return;
+ if (!this.partials[index].resonator) this.partials[index].resonator = { ...resDefaults };
+ this.partials[index].resonator[p.key] = v;
+ if (this.viewer) this.viewer.render();
+ });
+
+ // Inline jog slider for resonator params
+ const step = parseFloat(p.step);
+ const sensitivity = step * 5;
+ const jog = document.createElement('div');
+ jog.className = 'jog-slider';
+ const thumb = document.createElement('div');
+ thumb.className = 'jog-thumb';
+ jog.appendChild(thumb);
+ let dragging = false, startX = 0, startVal = 0;
+ const onMove = (ev) => {
+ if (!dragging) return;
+ const dx = ev.clientX - startX;
+ const half = jog.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(parseFloat(inp.min) || 0,
+ Math.min(parseFloat(inp.max) || 1e9, startVal + dx * sensitivity));
+ inp.value = newVal.toFixed(4);
+ if (!this.partials || !this.partials[index]) return;
+ if (!this.partials[index].resonator) this.partials[index].resonator = { ...resDefaults };
+ this.partials[index].resonator[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);
+ };
+ jog.addEventListener('mousedown', (ev) => {
+ dragging = true;
+ startX = ev.clientX;
+ startVal = parseFloat(inp.value) || 0;
+ document.addEventListener('mousemove', onMove);
+ document.addEventListener('mouseup', onUp);
+ ev.preventDefault();
+ });
+
+ const wrap = document.createElement('div');
+ wrap.className = 'synth-field-wrap';
+ wrap.appendChild(inp);
+ wrap.appendChild(jog);
+ resSection.appendChild(lbl);
+ resSection.appendChild(wrap);
+ }
+
+ // Show/hide helper
+ const applyMode = (resonator) => {
+ for (const el of sinSection.children) el.style.display = resonator ? 'none' : '';
+ for (const el of resSection.children) el.style.display = resonator ? '' : 'none';
+ btnSin.classList.toggle('active', !resonator);
+ btnRes.classList.toggle('active', resonator);
+ };
+
+ // Initial state
+ grid.appendChild(sinSection);
+ grid.appendChild(resSection);
+ applyMode(isResonator);
+
+ // Toggle handlers
+ btnSin.addEventListener('click', () => {
+ if (!this.partials) return;
+ if (!this.partials[index].resonator) this.partials[index].resonator = { ...resDefaults };
+ this.partials[index].resonator.enabled = false;
+ applyMode(false);
+ });
+ btnRes.addEventListener('click', () => {
+ if (!this.partials) return;
+ if (!this.partials[index].resonator) this.partials[index].resonator = { ...resDefaults };
+ this.partials[index].resonator.enabled = true;
+ applyMode(true);
});
- grid.appendChild(autoLbl);
- grid.appendChild(autoBtn);
}
_makeJogSlider(inp, partial, index, p, defaults) {