summaryrefslogtreecommitdiff
path: root/tools/mq_editor
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-18 15:46:59 +0100
committerskal <pascal.massimino@gmail.com>2026-02-18 15:46:59 +0100
commit7a054e8ee8566eea9d06ff1ff9c1ce48c39fe659 (patch)
tree30ee1013f398cc565139d67c2613200dbbebca6f /tools/mq_editor
parent02dd7799396ebe3b5c3764796cfa3cbc72b72110 (diff)
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 <noreply@anthropic.com>
Diffstat (limited to 'tools/mq_editor')
-rw-r--r--tools/mq_editor/README.md33
-rw-r--r--tools/mq_editor/index.html83
-rw-r--r--tools/mq_editor/mq_extract.js15
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 @@
<button id="stopBtn" disabled>â–  Stop</button>
<div class="params">
- <label>Hop:</label>
- <input type="number" id="hopSize" value="256" min="64" max="1024" step="64">
-
- <label>Threshold (dB):</label>
- <input type="number" id="threshold" value="-20" step="any">
+ <div class="param-group">
+ <span class="group-label">STFT</span>
+ <label title="STFT hop size in samples. Smaller = finer time resolution, more frames, slower.">Hop</label>
+ <input type="number" id="hopSize" value="256" min="64" max="1024" step="64" style="width:60px;">
+ </div>
- <label>Prominence (dB):</label>
- <input type="number" id="prominence" value="1.0" step="0.1" min="0">
+ <div class="param-group">
+ <span class="group-label">Peak Detection</span>
+ <label title="Minimum spectral peak amplitude in dB. Peaks below this are ignored.">Threshold (dB)</label>
+ <input type="number" id="threshold" value="-20" step="any">
+ <label title="Minimum prominence in dB: how much a peak must stand above its surrounding valley. Suppresses weak shoulders.">Prominence (dB)</label>
+ <input type="number" id="prominence" value="1.0" step="0.1" min="0">
+ <label title="Weight spectrum by frequency before peak detection: f × Power(f). Boosts high-frequency peaks relative to low-frequency ones.">
+ <input type="checkbox" id="freqWeight"> f·Power
+ </label>
+ </div>
- <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>
+ <div class="param-group">
+ <span class="group-label">Tracking</span>
+ <label title="Frames a candidate must persist consecutively before being promoted to a tracked partial. Higher = fewer spurious short bursts.">Birth</label>
+ <input type="number" id="birthPersistence" value="3" min="1" max="10" step="1" style="width:50px;">
+ <label title="Frames a partial can go unmatched before it is terminated. Higher = bridges short gaps; lower = cuts off quickly.">Death</label>
+ <input type="number" id="deathAge" value="5" min="1" max="20" step="1" style="width:50px;">
+ <label title="Weight of phase prediction error in the peak-matching cost function. Higher = stricter phase coherence required to continue a partial.">Phase Wt</label>
+ <input type="number" id="phaseErrorWeight" value="2.0" min="0" max="10" step="0.5" style="width:55px;">
+ <label title="Minimum number of frames a tracked partial must span. Shorter partials are discarded after tracking.">Min Len</label>
+ <input type="number" id="minLength" value="10" min="1" max="50" step="1" style="width:50px;">
+ </div>
- <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>
+ <div class="param-group">
+ <span class="group-label">Filter</span>
+ <label title="Keep only the strongest N% of extracted partials, ranked by peak amplitude.">Keep</label>
+ <input type="range" id="keepPct" min="1" max="100" value="100" style="width:100px; vertical-align:middle;">
+ <span id="keepPctLabel">100%</span>
+ </div>
</div>
</div>
@@ -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();