diff options
Diffstat (limited to 'tools/mq_editor/editor.js')
| -rw-r--r-- | tools/mq_editor/editor.js | 178 |
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) { |
