summaryrefslogtreecommitdiff
path: root/tools/mq_editor/app.js
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-18 18:12:48 +0100
committerskal <pascal.massimino@gmail.com>2026-02-18 18:12:48 +0100
commitc5ee9ed388013133fe1e028c403370242c558e2d (patch)
treee86d07941c74b9953bf6f9c8e7203848b05ade0e /tools/mq_editor/app.js
parentd73414c6d42fbec2666cb24b65b1f597b335c1e9 (diff)
feat(mq_editor): add iso-contour tracking mode for bass/diffuse regions
trackIsoContour() follows constant energy level through STFT frames instead of peaks. Useful for broad bass areas where peak detector finds nothing. Preview in cyan, auto-detects spread on commit (naturally large). Toggle: ≋ Contour button or C key. Mutually exclusive with ⊕ Explore. 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.js51
1 files changed, 33 insertions, 18 deletions
diff --git a/tools/mq_editor/app.js b/tools/mq_editor/app.js
index 62c1bb7..59849da 100644
--- a/tools/mq_editor/app.js
+++ b/tools/mq_editor/app.js
@@ -45,13 +45,13 @@ let audioContext = null;
let currentSource = null;
let extractedPartials = null;
let stftCache = null;
-let exploreModeActive = false;
+let exploreMode = false; // false | 'peak' | 'contour'
-function setExploreMode(enabled) {
- exploreModeActive = enabled;
- const btn = document.getElementById('exploreBtn');
- btn.classList.toggle('explore-active', enabled);
- if (viewer) viewer.setExploreMode(enabled);
+function setExploreMode(mode) { // false | 'peak' | 'contour'
+ exploreMode = mode;
+ document.getElementById('exploreBtn').classList.toggle('explore-active', mode === 'peak');
+ document.getElementById('contourBtn').classList.toggle('contour-active', mode === 'contour');
+ if (viewer) viewer.setExploreMode(mode);
}
// Undo/redo
@@ -171,29 +171,40 @@ function loadAudioBuffer(buffer, label) {
viewer = new SpectrogramViewer(canvas, audioBuffer, stftCache);
viewer.setFrames(peakFrames);
document.getElementById('exploreBtn').disabled = false;
+ document.getElementById('contourBtn').disabled = false;
editor.setViewer(viewer);
viewer.onPartialSelect = (i) => editor.onPartialSelect(i);
viewer.onRender = () => editor.onRender();
viewer.onBeforeChange = pushUndo;
viewer.onExploreMove = (time, freq) => {
- if (!viewer.frames || viewer.frames.length === 0) return;
- const params = {
- hopSize: Math.max(64, parseInt(hopSize.value) || 64),
- sampleRate: audioBuffer.sampleRate,
- deathAge: parseInt(deathAgeEl.value),
- phaseErrorWeight: parseFloat(phaseErrorWeightEl.value),
- };
- viewer.setPreviewPartial(trackFromSeed(viewer.frames, time, freq, params));
+ let partial = null;
+ if (exploreMode === 'peak') {
+ if (!viewer.frames || viewer.frames.length === 0) return;
+ partial = trackFromSeed(viewer.frames, time, freq, {
+ hopSize: Math.max(64, parseInt(hopSize.value) || 64),
+ sampleRate: audioBuffer.sampleRate,
+ deathAge: parseInt(deathAgeEl.value),
+ phaseErrorWeight: parseFloat(phaseErrorWeightEl.value),
+ });
+ } else if (exploreMode === 'contour') {
+ partial = trackIsoContour(stftCache, time, freq, {
+ sampleRate: audioBuffer.sampleRate,
+ deathAge: parseInt(deathAgeEl.value),
+ });
+ }
+ viewer.setPreviewPartial(partial);
};
viewer.onExploreCommit = (partial) => {
if (!extractedPartials) extractedPartials = [];
pushUndo();
+ const {spread_above, spread_below} = autodetectSpread(partial, stftCache, fftSize, audioBuffer.sampleRate);
+ partial.replicas = { ...partial.replicas, spread_above, spread_below };
extractedPartials.unshift(partial);
editor.setPartials(extractedPartials);
viewer.setPartials(extractedPartials);
viewer.setKeepCount(getKeepCount());
viewer.selectPartial(0);
- setStatus(`Explore: added partial (${extractedPartials.length} total)`, 'info');
+ setStatus(`${exploreMode}: added partial (${extractedPartials.length} total)`, 'info');
};
if (label.startsWith('Test WAV')) validateTestWAVPeaks(stftCache);
}, 10);
@@ -348,7 +359,8 @@ function clearAllPartials() {
document.getElementById('newPartialBtn').addEventListener('click', createNewPartial);
document.getElementById('clearAllBtn').addEventListener('click', clearAllPartials);
-document.getElementById('exploreBtn').addEventListener('click', () => setExploreMode(!exploreModeActive));
+document.getElementById('exploreBtn').addEventListener('click', () => setExploreMode(exploreMode === 'peak' ? false : 'peak'));
+document.getElementById('contourBtn').addEventListener('click', () => setExploreMode(exploreMode === 'contour' ? false : 'contour'));
document.getElementById('undoBtn').addEventListener('click', undo);
document.getElementById('redoBtn').addEventListener('click', redo);
@@ -526,9 +538,12 @@ document.addEventListener('keydown', (e) => {
if (!extractBtn.disabled) extractBtn.click();
} else if (e.code === 'KeyX' && !e.ctrlKey && !e.metaKey) {
e.preventDefault();
- if (!document.getElementById('exploreBtn').disabled) setExploreMode(!exploreModeActive);
+ if (!document.getElementById('exploreBtn').disabled) setExploreMode(exploreMode === 'peak' ? false : 'peak');
+ } else if (e.code === 'KeyC' && !e.ctrlKey && !e.metaKey) {
+ e.preventDefault();
+ if (!document.getElementById('contourBtn').disabled) setExploreMode(exploreMode === 'contour' ? false : 'contour');
} else if (e.code === 'Escape') {
- if (exploreModeActive) { setExploreMode(false); return; }
+ if (exploreMode) { setExploreMode(false); return; }
if (viewer) viewer.selectPartial(-1);
}
});