From 105c817021a84bfacffa1553d6bcd536808b9f23 Mon Sep 17 00:00:00 2001 From: skal Date: Wed, 18 Feb 2026 06:59:32 +0100 Subject: feat(mq_editor): UI improvements and partial detection enhancements - Right panel with synthesis checkboxes (integrate phase, disable jitter, disable spread) - Style file chooser as button; show filename next to page title - Second backward pass in extractPartials to recover partial onsets - Cursor line drawn on overlay canvas (no full redraw on mousemove) handoff(Claude): UI + algo improvements complete --- tools/mq_editor/mq_extract.js | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) (limited to 'tools/mq_editor/mq_extract.js') diff --git a/tools/mq_editor/mq_extract.js b/tools/mq_editor/mq_extract.js index 2293d52..8a0ea0e 100644 --- a/tools/mq_editor/mq_extract.js +++ b/tools/mq_editor/mq_extract.js @@ -16,6 +16,9 @@ function extractPartials(params, stftCache) { const partials = trackPartials(frames); + // Second pass: extend partials leftward to recover onset frames + expandPartialsLeft(partials, frames); + for (const partial of partials) { partial.freqCurve = fitBezier(partial.times, partial.freqs); partial.ampCurve = fitBezier(partial.times, partial.amps); @@ -144,6 +147,41 @@ function trackPartials(frames) { return partials; } +// Second pass: extend each partial leftward to recover onset frames missed +// by the birthPersistence requirement in the forward pass. +function expandPartialsLeft(partials, frames) { + const trackingRatio = 0.05; + const minTrackingHz = 20; + + // Build time → frame index map + const timeToIdx = new Map(); + for (let i = 0; i < frames.length; ++i) timeToIdx.set(frames[i].time, i); + + for (const partial of partials) { + let startIdx = timeToIdx.get(partial.times[0]); + if (startIdx == null || startIdx === 0) continue; + + for (let i = startIdx - 1; i >= 0; --i) { + const frame = frames[i]; + const refFreq = partial.freqs[0]; + const tol = Math.max(refFreq * trackingRatio, minTrackingHz); + + let bestIdx = -1, bestDist = Infinity; + for (let j = 0; j < frame.peaks.length; ++j) { + const dist = Math.abs(frame.peaks[j].freq - refFreq); + if (dist < tol && dist < bestDist) { bestDist = dist; bestIdx = j; } + } + + if (bestIdx < 0) break; + + const pk = frame.peaks[bestIdx]; + partial.times.unshift(frame.time); + partial.freqs.unshift(pk.freq); + partial.amps.unshift(pk.amp); + } + } +} + // Fit cubic bezier to trajectory using samples at ~1/3 and ~2/3 as control points function fitBezier(times, values) { const n = times.length - 1; -- cgit v1.2.3