diff options
| author | skal <pascal.massimino@gmail.com> | 2026-02-18 11:21:12 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-02-18 11:21:12 +0100 |
| commit | 48d8a9fe8af83fd1c8ef029a3c5fb8d87421a46e (patch) | |
| tree | 3306d249040f5f086c86a826d193a4a59894ca56 /tools/mq_editor | |
| parent | 082959062671e0e1a1482fac8dc5f77e05060bee (diff) | |
feat(mq_editor): f·Power checkbox, deselect on extract, panel refresh after auto-spread
- Add 'f·Power' checkbox: weights spectrum by f before peak detection (f·FFT_Power(f))
to accentuate high-frequency peaks; re-runs extraction on toggle
- Deselect partial after Extract Partials run
- Fix right panel not refreshing after Auto Spread All: re-call editor.onPartialSelect
handoff(Claude): mq_editor UX polish
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'tools/mq_editor')
| -rw-r--r-- | tools/mq_editor/README.md | 3 | ||||
| -rw-r--r-- | tools/mq_editor/index.html | 16 | ||||
| -rw-r--r-- | tools/mq_editor/mq_extract.js | 11 |
3 files changed, 22 insertions, 8 deletions
diff --git a/tools/mq_editor/README.md b/tools/mq_editor/README.md index 141963a..0ef2e72 100644 --- a/tools/mq_editor/README.md +++ b/tools/mq_editor/README.md @@ -27,6 +27,7 @@ open tools/mq_editor/index.html - **Hop Size:** 64–1024 samples (default 256) - **Threshold:** dB floor for peak detection (default −60 dB) +- **f·Power:** checkbox — weight spectrum by frequency (`f·FFT_Power(f)`) before peak detection, accentuating high-frequency peaks - **Keep %:** slider to limit how many partials are shown/synthesized ## Keyboard Shortcuts @@ -67,7 +68,7 @@ open tools/mq_editor/index.html ## Algorithm 1. **STFT:** Overlapping Hann windows, radix-2 FFT -2. **Peak Detection:** Local maxima above threshold + parabolic interpolation +2. **Peak Detection:** Local maxima above threshold + parabolic interpolation; optional `f·Power(f)` frequency weighting to accentuate high-frequency peaks 3. **Forward Tracking:** Birth/death/continuation with frequency-dependent tolerance, candidate persistence 4. **Backward Expansion:** Second pass extends each partial leftward to recover onset frames 5. **Bezier Fitting:** Cubic curves with control points at t/3 and 2t/3 diff --git a/tools/mq_editor/index.html b/tools/mq_editor/index.html index f6d052b..aab603b 100644 --- a/tools/mq_editor/index.html +++ b/tools/mq_editor/index.html @@ -261,6 +261,10 @@ <label>Threshold (dB):</label> <input type="number" id="threshold" value="-20" step="any"> + <label style="margin-left:16px;" title="Weight spectrum by frequency before peak detection (f * FFT_Power(f)), accentuates high-frequency peaks"> + <input type="checkbox" id="freqWeight"> f·Power + </label> + <label style="margin-left:16px;">Keep:</label> <input type="range" id="keepPct" min="1" max="100" value="100" style="width:100px; vertical-align:middle;"> <span id="keepPctLabel" style="margin-left:4px;">100%</span> @@ -366,6 +370,7 @@ const hopSize = document.getElementById('hopSize'); const threshold = document.getElementById('threshold'); + const freqWeightCb = document.getElementById('freqWeight'); const keepPct = document.getElementById('keepPct'); const keepPctLabel = document.getElementById('keepPctLabel'); const fftSize = 1024; // Fixed @@ -483,6 +488,7 @@ fftSize: fftSize, hopSize: parseInt(hopSize.value), threshold: parseFloat(threshold.value), + freqWeight: freqWeightCb.checked, sampleRate: audioBuffer.sampleRate }; @@ -501,9 +507,7 @@ setStatus(`Extracted ${result.partials.length} partials`, 'info'); viewer.setPartials(result.partials); viewer.setKeepCount(getKeepCount()); - // Refresh panels: re-select if index still valid, else clear - const prevSel = viewer.selectedPartial; - viewer.selectPartial(prevSel >= 0 && prevSel < result.partials.length ? prevSel : -1); + viewer.selectPartial(-1); } catch (err) { setStatus('Extraction error: ' + err.message, 'error'); @@ -531,6 +535,8 @@ p.replicas.spread_below = spread_below; } if (viewer) viewer.render(); + const sel = viewer ? viewer.selectedPartial : -1; + if (sel >= 0) editor.onPartialSelect(sel); setStatus(`Auto-spread applied to ${extractedPartials.length} partials`, 'info'); }); @@ -538,6 +544,10 @@ if (stftCache) runExtraction(); }); + freqWeightCb.addEventListener('change', () => { + if (stftCache) runExtraction(); + }); + function playAudioBuffer(buffer, statusMsg) { const startTime = audioContext.currentTime; currentSource = audioContext.createBufferSource(); diff --git a/tools/mq_editor/mq_extract.js b/tools/mq_editor/mq_extract.js index d29cfbc..c03e869 100644 --- a/tools/mq_editor/mq_extract.js +++ b/tools/mq_editor/mq_extract.js @@ -3,14 +3,14 @@ // Extract partials from audio buffer function extractPartials(params, stftCache) { - const {fftSize, threshold, sampleRate} = params; + const {fftSize, threshold, sampleRate, freqWeight} = params; const numFrames = stftCache.getNumFrames(); const frames = []; for (let i = 0; i < numFrames; ++i) { const cachedFrame = stftCache.getFrameAtIndex(i); const squaredAmp = stftCache.getSquaredAmplitude(cachedFrame.time); - const peaks = detectPeaks(squaredAmp, fftSize, sampleRate, threshold); + const peaks = detectPeaks(squaredAmp, fftSize, sampleRate, threshold, freqWeight); frames.push({time: cachedFrame.time, peaks}); } @@ -29,10 +29,13 @@ function extractPartials(params, stftCache) { // Detect spectral peaks via local maxima + parabolic interpolation // squaredAmp: pre-computed re*re+im*im per bin -function detectPeaks(squaredAmp, fftSize, sampleRate, thresholdDB) { +// freqWeight: if true, weight by f before peak detection (f * Power(f)) +function detectPeaks(squaredAmp, fftSize, sampleRate, thresholdDB, freqWeight) { const mag = new Float32Array(fftSize / 2); + const binHz = sampleRate / fftSize; for (let i = 0; i < fftSize / 2; ++i) { - mag[i] = 10 * Math.log10(Math.max(squaredAmp[i], 1e-20)); + const w = freqWeight ? (i * binHz) : 1.0; + mag[i] = 10 * Math.log10(Math.max(squaredAmp[i] * w, 1e-20)); } const peaks = []; |
