diff options
| author | skal <pascal.massimino@gmail.com> | 2026-02-18 18:12:48 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-02-18 18:12:48 +0100 |
| commit | c5ee9ed388013133fe1e028c403370242c558e2d (patch) | |
| tree | e86d07941c74b9953bf6f9c8e7203848b05ade0e /tools/mq_editor/app.js | |
| parent | d73414c6d42fbec2666cb24b65b1f597b335c1e9 (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.js | 51 |
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); } }); |
