From 7a054e8ee8566eea9d06ff1ff9c1ce48c39fe659 Mon Sep 17 00:00:00 2001 From: skal Date: Wed, 18 Feb 2026 15:46:59 +0100 Subject: feat(mq_editor): expose tracking params in UI with grouped param panel Reorganize extraction parameters into four labeled groups (STFT / Peak Detection / Tracking / Filter). Expose Birth, Death, Phase Wt and Min Len as live controls wired to runExtraction(). All labels carry tooltip descriptions. mq_extract.js now reads these from params instead of hardcoded constants (same defaults preserved). handoff(Claude): tracking params exposed, panel grouped, README updated. Co-Authored-By: Claude Sonnet 4.6 --- tools/mq_editor/README.md | 33 +++++++++++++---- tools/mq_editor/index.html | 83 ++++++++++++++++++++++++++++++++++--------- tools/mq_editor/mq_extract.js | 15 ++++---- 3 files changed, 101 insertions(+), 30 deletions(-) diff --git a/tools/mq_editor/README.md b/tools/mq_editor/README.md index c1f2732..83cd28d 100644 --- a/tools/mq_editor/README.md +++ b/tools/mq_editor/README.md @@ -25,11 +25,32 @@ open tools/mq_editor/index.html ## Parameters -- **Hop Size:** 64–1024 samples (default 256) -- **Threshold:** dB floor for peak detection (default −60 dB) -- **Prominence:** Min dB height of a peak above its surrounding "valley floor" (default 1.0 dB). Filters out insignificant local maxima. -- **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 +Parameters are grouped into four sections in the toolbar. + +### STFT +| Param | Default | Description | +|-------|---------|-------------| +| **Hop** | 256 | STFT hop size in samples. Smaller = finer time resolution, more frames, slower. | + +### Peak Detection +| Param | Default | Description | +|-------|---------|-------------| +| **Threshold (dB)** | −20 | Minimum spectral peak amplitude. Peaks below this are ignored. | +| **Prominence (dB)** | 1.0 | How much a peak must rise above its surrounding valley. Suppresses weak shoulders. | +| **f·Power** | off | Weight spectrum by `f × Power(f)` before detection. Boosts high-frequency peaks relative to low-frequency ones. | + +### Tracking +| Param | Default | Description | +|-------|---------|-------------| +| **Birth** | 3 | Frames a candidate must persist before becoming a partial. Higher = fewer spurious bursts. | +| **Death** | 5 | Frames a partial can go unmatched before termination. Higher = bridges short gaps. | +| **Phase Wt** | 2.0 | Weight of phase prediction error in the peak-matching cost function. Higher = stricter phase coherence. | +| **Min Len** | 10 | Minimum frame count for a partial to survive after tracking. Discards very short partials. | + +### Filter +| Param | Default | Description | +|-------|---------|-------------| +| **Keep %** | 100% | Retain only the strongest N% of extracted partials by peak amplitude. | ## Keyboard Shortcuts @@ -123,7 +144,7 @@ For a partial at 440 Hz with `spread = 0.02`: `BW ≈ 8.8 Hz`, `r ≈ exp(−π 1. **STFT:** Overlapping Hann windows, radix-2 FFT 2. **Peak Detection:** Local maxima above threshold + parabolic interpolation. Includes **Prominence Filtering** (rejects peaks not significantly higher than surroundings). Optional `f·Power(f)` weighting. -3. **Forward Tracking:** Birth/death/continuation with frequency-dependent tolerance. Includes **Predictive Kinematic Tracking** (uses velocity to track rapidly moving partials). +3. **Forward Tracking:** Phase-coherent birth/death/continuation. Configurable `Birth`, `Death`, `Phase Wt`, and `Min Len`. Uses velocity prediction and phase advance to resolve ambiguous peak matches. 4. **Backward Expansion:** Second pass extends each partial leftward to recover onset frames 5. **Bezier Fitting:** Cubic curves optimized via **Least-Squares** (minimizes error across all points). diff --git a/tools/mq_editor/index.html b/tools/mq_editor/index.html index a2daff5..5f6af24 100644 --- a/tools/mq_editor/index.html +++ b/tools/mq_editor/index.html @@ -43,11 +43,31 @@ #extractBtn { background: #666; color: #fff; font-weight: bold; border-color: #888; } input[type="file"] { display: none; } .params { - display: inline-block; - margin-left: 20px; + display: flex; + margin-top: 8px; + background: #222; + border: 1px solid #444; + border-radius: 3px; + } + .param-group { + display: flex; + align-items: center; + gap: 6px; + padding: 5px 12px; + border-right: 1px solid #444; + flex-wrap: wrap; + } + .param-group:last-child { border-right: none; } + .group-label { + font-size: 9px; + color: #666; + text-transform: uppercase; + letter-spacing: 1px; + white-space: nowrap; + margin-right: 2px; } label { - margin-right: 8px; + margin-right: 4px; } input[type="number"], select { width: 80px; @@ -266,22 +286,41 @@
- - - - - +
+ STFT + + +
- - +
+ Peak Detection + + + + + +
- +
+ Tracking + + + + + + + + +
- - - 100% +
+ Filter + + + 100% +
@@ -451,6 +490,10 @@ const threshold = document.getElementById('threshold'); const prominence = document.getElementById('prominence'); const freqWeightCb = document.getElementById('freqWeight'); + const birthPersistenceEl = document.getElementById('birthPersistence'); + const deathAgeEl = document.getElementById('deathAge'); + const phaseErrorWeightEl = document.getElementById('phaseErrorWeight'); + const minLengthEl = document.getElementById('minLength'); const keepPct = document.getElementById('keepPct'); const keepPctLabel = document.getElementById('keepPctLabel'); const fftSize = 1024; // Fixed @@ -570,6 +613,10 @@ threshold: parseFloat(threshold.value), prominence: parseFloat(prominence.value), freqWeight: freqWeightCb.checked, + birthPersistence: parseInt(birthPersistenceEl.value), + deathAge: parseInt(deathAgeEl.value), + phaseErrorWeight: parseFloat(phaseErrorWeightEl.value), + minLength: parseInt(minLengthEl.value), sampleRate: audioBuffer.sampleRate }; @@ -629,6 +676,10 @@ if (stftCache) runExtraction(); }); + for (const el of [birthPersistenceEl, deathAgeEl, phaseErrorWeightEl, minLengthEl]) { + el.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 c084b43..3f7490d 100644 --- a/tools/mq_editor/mq_extract.js +++ b/tools/mq_editor/mq_extract.js @@ -104,20 +104,19 @@ function normalizeAngle(angle) { // Track partials across frames using phase coherence for robust matching. function trackPartials(frames, params) { - const { sampleRate, hopSize } = params; + const { + sampleRate, hopSize, + birthPersistence = 3, + deathAge = 5, + minLength = 10, + phaseErrorWeight = 2.0 + } = params; const partials = []; const activePartials = []; const candidates = []; // pre-birth const trackingRatio = 0.05; // 5% frequency tolerance const minTrackingHz = 20; - const birthPersistence = 3; // frames before partial is born - const deathAge = 5; // frames without match before death - const minLength = 10; // frames required to keep partial - - // Weight phase error heavily in cost function, scaled by frequency. - // This makes phase deviation more significant for high-frequency partials. - const phaseErrorWeight = 2.0; for (const frame of frames) { const matched = new Set(); -- cgit v1.2.3