summaryrefslogtreecommitdiff
path: root/tools/mq_editor/app.js
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-18 17:28:38 +0100
committerskal <pascal.massimino@gmail.com>2026-02-18 17:28:38 +0100
commit37d7601ab64e0dd22ca3e579c3b8332d32c41b9a (patch)
treeacd5ac5889af6ba3a644bbfb23d381a3e4f32843 /tools/mq_editor/app.js
parent1eb7f1d06798b3a3455817dfcd1876febd3eca89 (diff)
feat(mq_editor): add new partial, undo/redo
- '+ Partial' button (N key): insert 440Hz/max-amp partial at front - Undo/Redo buttons (Ctrl+Z/Y): JSON snapshot stack, 50 levels - Hooks in delete, mute, curve edits, amp drag, freq drag handoff(Gemini): undo/redo + new-partial added to mq_editor Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'tools/mq_editor/app.js')
-rw-r--r--tools/mq_editor/app.js80
1 files changed, 80 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();