diff options
Diffstat (limited to 'tools/mq_editor')
| -rw-r--r-- | tools/mq_editor/app.js | 80 | ||||
| -rw-r--r-- | tools/mq_editor/editor.js | 6 | ||||
| -rw-r--r-- | tools/mq_editor/index.html | 3 | ||||
| -rw-r--r-- | tools/mq_editor/viewer.js | 2 |
4 files changed, 91 insertions, 0 deletions
diff --git a/tools/mq_editor/app.js b/tools/mq_editor/app.js index 90f8f7e..862ec6c 100644 --- a/tools/mq_editor/app.js +++ b/tools/mq_editor/app.js @@ -46,6 +46,45 @@ let currentSource = null; let extractedPartials = null; let stftCache = null; +// Undo/redo +const undoStack = []; +const redoStack = []; + +function _updateUndoRedoBtns() { + document.getElementById('undoBtn').disabled = undoStack.length === 0; + document.getElementById('redoBtn').disabled = redoStack.length === 0; +} + +function pushUndo() { + undoStack.push(JSON.parse(JSON.stringify(extractedPartials || []))); + redoStack.length = 0; + if (undoStack.length > 50) undoStack.shift(); + _updateUndoRedoBtns(); +} + +function _applySnapshot(snap) { + extractedPartials = snap; + editor.setPartials(snap); + if (viewer) { + viewer.setPartials(snap); + viewer.setKeepCount(snap.length > 0 ? getKeepCount() : 0); + viewer.selectPartial(-1); + } + _updateUndoRedoBtns(); +} + +function undo() { + if (!undoStack.length) return; + redoStack.push(JSON.parse(JSON.stringify(extractedPartials || []))); + _applySnapshot(undoStack.pop()); +} + +function redo() { + if (!redoStack.length) return; + undoStack.push(JSON.parse(JSON.stringify(extractedPartials || []))); + _applySnapshot(redoStack.pop()); +} + const wavFile = document.getElementById('wavFile'); const chooseFileBtn = document.getElementById('chooseFileBtn'); const extractBtn = document.getElementById('extractBtn'); @@ -83,6 +122,7 @@ editor.onPartialDeleted = () => { if (viewer && extractedPartials) viewer.setKeepCount(extractedPartials.length > 0 ? getKeepCount() : 0); }; +editor.onBeforeChange = pushUndo; // Initialize audio context function initAudioContext() { @@ -111,6 +151,7 @@ function loadAudioBuffer(buffer, label) { editor.setViewer(viewer); viewer.onPartialSelect = (i) => editor.onPartialSelect(i); viewer.onRender = () => editor.onRender(); + viewer.onBeforeChange = pushUndo; if (label.startsWith('Test WAV')) validateTestWAVPeaks(stftCache); }, 10); } @@ -213,6 +254,8 @@ function runExtraction() { } extractBtn.disabled = false; autoSpreadAllBtn.disabled = false; + document.getElementById('newPartialBtn').disabled = false; + undoStack.length = 0; redoStack.length = 0; _updateUndoRedoBtns(); }, 50); } @@ -221,6 +264,36 @@ extractBtn.addEventListener('click', () => { runExtraction(); }); +function createNewPartial() { + if (!audioBuffer || !extractedPartials) return; + pushUndo(); + const dur = audioBuffer.duration; + const newPartial = { + times: [0, dur], + freqs: [440, 440], + amps: [1.0, 1.0], + phases: [0, 0], + muted: false, + freqCurve: { + t0: 0, t1: dur / 3, t2: dur * 2 / 3, t3: dur, + v0: 440, v1: 440, v2: 440, v3: 440, + a0: 1.0, a1: 1.0, a2: 1.0, a3: 1.0, + }, + replicas: { decay_alpha: 0.1, jitter: 0.05, spread_above: 0.02, spread_below: 0.02 }, + }; + extractedPartials.unshift(newPartial); + editor.setPartials(extractedPartials); + if (viewer) { + viewer.setPartials(extractedPartials); + viewer.setKeepCount(getKeepCount()); + viewer.selectPartial(0); + } +} + +document.getElementById('newPartialBtn').addEventListener('click', createNewPartial); +document.getElementById('undoBtn').addEventListener('click', undo); +document.getElementById('redoBtn').addEventListener('click', redo); + autoSpreadAllBtn.addEventListener('click', () => { if (!extractedPartials || !stftCache) return; const fs = stftCache.fftSize; @@ -353,6 +426,13 @@ function playSynthesized() { // Keyboard shortcuts document.addEventListener('keydown', (e) => { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; + if (e.ctrlKey && e.code === 'KeyZ' && !e.shiftKey) { + e.preventDefault(); undo(); return; + } else if (e.ctrlKey && (e.code === 'KeyY' || (e.code === 'KeyZ' && e.shiftKey))) { + e.preventDefault(); redo(); return; + } else if (e.code === 'KeyN' && !e.ctrlKey && !e.metaKey) { + e.preventDefault(); createNewPartial(); return; + } if (e.code === 'Digit1') { e.preventDefault(); playSynthesized(); diff --git a/tools/mq_editor/editor.js b/tools/mq_editor/editor.js index 3c07877..0854ec0 100644 --- a/tools/mq_editor/editor.js +++ b/tools/mq_editor/editor.js @@ -20,6 +20,8 @@ class PartialEditor { // Callback: called after a partial is deleted so the host can update keepCount this.onPartialDeleted = null; + // Callback: called before any mutation (for undo/redo) + this.onBeforeChange = null; // Private state this._selectedIndex = -1; @@ -355,6 +357,7 @@ class PartialEditor { if (!this.partials) return; const val = parseFloat(e.target.value); if (isNaN(val)) return; + if (this.onBeforeChange) this.onBeforeChange(); this.partials[partialIndex][curveKey][field + pointIndex] = val; if (this.viewer) this.viewer.render(); }; @@ -363,6 +366,7 @@ class PartialEditor { _setupButtons() { document.getElementById('mutePartialBtn').addEventListener('click', () => { if (this._selectedIndex < 0 || !this.partials) return; + if (this.onBeforeChange) this.onBeforeChange(); const p = this.partials[this._selectedIndex]; p.muted = !p.muted; if (this.viewer) this.viewer.render(); @@ -371,6 +375,7 @@ class PartialEditor { document.getElementById('deletePartialBtn').addEventListener('click', () => { if (this._selectedIndex < 0 || !this.partials || !this.viewer) return; + if (this.onBeforeChange) this.onBeforeChange(); this.partials.splice(this._selectedIndex, 1); this.viewer.selectPartial(-1); if (this.onPartialDeleted) this.onPartialDeleted(); @@ -490,6 +495,7 @@ class PartialEditor { const curve = partial.freqCurve; for (let i = 0; i < 4; ++i) { if (Math.hypot(this._tToX(curve['t' + i]) - x, this._ampToY(curve['a' + i]) - y) <= 8) { + if (this.onBeforeChange) this.onBeforeChange(); this._dragPointIndex = i; this._dragCompanionOff = null; if (i === 0) diff --git a/tools/mq_editor/index.html b/tools/mq_editor/index.html index 4737d67..4292a5f 100644 --- a/tools/mq_editor/index.html +++ b/tools/mq_editor/index.html @@ -284,6 +284,9 @@ <button id="autoSpreadAllBtn" disabled>Auto Spread All</button> <button id="playBtn" disabled>▶ Play</button> <button id="stopBtn" disabled>■ Stop</button> + <button id="newPartialBtn" disabled>+ Partial</button> + <button id="undoBtn" disabled>↩ Undo</button> + <button id="redoBtn" disabled>↪ Redo</button> <div class="params"> <div class="param-group"> diff --git a/tools/mq_editor/viewer.js b/tools/mq_editor/viewer.js index 723a585..46876c1 100644 --- a/tools/mq_editor/viewer.js +++ b/tools/mq_editor/viewer.js @@ -53,6 +53,7 @@ class SpectrogramViewer { this.dragState = null; // {pointIndex: 0-3} this.onPartialSelect = null; // callback(index) this.onRender = null; // callback() called after each render (for synced panels) + this.onBeforeChange = null; // callback() called before any mutation (for undo/redo) // Setup event handlers this.setupMouseHandlers(); @@ -569,6 +570,7 @@ class SpectrogramViewer { companionOff = { dt: curve.t1 - curve.t0, dv: curve.v1 - curve.v0 }; else if (ptIdx === 3) companionOff = { dt: curve.t2 - curve.t3, dv: curve.v2 - curve.v3 }; + if (this.onBeforeChange) this.onBeforeChange(); this.dragState = { pointIndex: ptIdx, companionOff }; canvas.style.cursor = 'grabbing'; e.preventDefault(); |
