From 00ce97d64b8bf7e1dcbdb5151bdf2033132ffbc3 Mon Sep 17 00:00:00 2001 From: skal Date: Wed, 18 Feb 2026 16:01:13 +0100 Subject: refactor(mq_editor): consolidate duplicates, extract utils.js and app.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - utils.js (new): evalBezier (robust), getCanvasCoords, buildBandPoints - app.js (new): extract ~450-line inline script from index.html - editor.js: generalize _makeJogSlider(inp, options) with onUpdate cb, eliminate 50-line inline resonator jog duplication, use getCanvasCoords - mq_extract.js: extract findBestPeak(), replace two identical loop bodies - viewer.js: remove duplicate evalBezier, use getCanvasCoords/buildBandPoints - mq_synth.js: remove duplicate evalBezier - index.html: inline script removed, load order: utils→fft→extract→synth→viewer→editor→app handoff(Claude): mq_editor refactor complete — no logic changes, browser-ready. Co-Authored-By: Claude Sonnet 4.6 --- doc/COMPLETED.md | 9 + tools/mq_editor/app.js | 447 +++++++++++++++++++++++++++++++++++++++++ tools/mq_editor/editor.js | 85 +++----- tools/mq_editor/index.html | 449 +----------------------------------------- tools/mq_editor/mq_extract.js | 41 ++-- tools/mq_editor/mq_synth.js | 13 -- tools/mq_editor/utils.js | 36 ++++ tools/mq_editor/viewer.js | 37 +--- 8 files changed, 547 insertions(+), 570 deletions(-) create mode 100644 tools/mq_editor/app.js create mode 100644 tools/mq_editor/utils.js diff --git a/doc/COMPLETED.md b/doc/COMPLETED.md index 3e02c40..1fb51f5 100644 --- a/doc/COMPLETED.md +++ b/doc/COMPLETED.md @@ -557,6 +557,15 @@ Use `read @doc/archive/FILENAME.md` to access archived documents. - **Task #39: Visual Debugging System**: Implemented a comprehensive set of wireframe primitives (Sphere, Cone, Cross, Line, Trajectory) in `VisualDebug`. Updated `test_3d_render` to demonstrate usage. - **Task #68: Mesh Wireframe Rendering**: Added `add_mesh_wireframe` to `VisualDebug` to visualize triangle edges for mesh objects. Integrated into `Renderer3D` debug path and `test_mesh` tool. +#### mq_editor Refactoring (February 18, 2026) +- **`utils.js`** (new): consolidated `evalBezier` (robust dt≤0 guard), `getCanvasCoords`, `buildBandPoints` — loaded first. +- **`app.js`** (new): extracted ~450-line inline ` - + diff --git a/tools/mq_editor/mq_extract.js b/tools/mq_editor/mq_extract.js index 3f7490d..107b2ac 100644 --- a/tools/mq_editor/mq_extract.js +++ b/tools/mq_editor/mq_extract.js @@ -102,6 +102,21 @@ function normalizeAngle(angle) { return angle - 2 * Math.PI * Math.floor((angle + Math.PI) / (2 * Math.PI)); } +// Find best matching peak for a predicted freq/phase. Returns {bestIdx, bestCost}. +function findBestPeak(peaks, matched, predictedFreq, predictedPhase, tol, phaseErrorWeight) { + let bestIdx = -1, bestCost = Infinity; + for (let i = 0; i < peaks.length; ++i) { + if (matched.has(i)) continue; + const pk = peaks[i]; + const freqError = Math.abs(pk.freq - predictedFreq); + if (freqError > tol) continue; + const phaseError = Math.abs(normalizeAngle(pk.phase - predictedPhase)); + const cost = freqError + phaseErrorWeight * phaseError * predictedFreq; + if (cost < bestCost) { bestCost = cost; bestIdx = i; } + } + return { bestIdx, bestCost }; +} + // Track partials across frames using phase coherence for robust matching. function trackPartials(frames, params) { const { @@ -134,19 +149,8 @@ function trackPartials(frames, params) { const predictedPhase = lastPhase + phaseAdvance; const tol = Math.max(predictedFreq * trackingRatio, minTrackingHz); - let bestIdx = -1, bestCost = Infinity; - // Find the peak in the new frame with the lowest cost (freq + phase error). - for (let i = 0; i < frame.peaks.length; ++i) { - if (matched.has(i)) continue; - const pk = frame.peaks[i]; - const freqError = Math.abs(pk.freq - predictedFreq); - if (freqError > tol) continue; - - const phaseError = Math.abs(normalizeAngle(pk.phase - predictedPhase)); - const cost = freqError + phaseErrorWeight * phaseError * predictedFreq; - if (cost < bestCost) { bestCost = cost; bestIdx = i; } - } + const { bestIdx } = findBestPeak(frame.peaks, matched, predictedFreq, predictedPhase, tol, phaseErrorWeight); if (bestIdx >= 0) { const pk = frame.peaks[bestIdx]; @@ -175,18 +179,7 @@ function trackPartials(frames, params) { const predictedPhase = lastPhase + phaseAdvance; const tol = Math.max(predictedFreq * trackingRatio, minTrackingHz); - let bestIdx = -1, bestCost = Infinity; - - for (let j = 0; j < frame.peaks.length; ++j) { - if (matched.has(j)) continue; - const pk = frame.peaks[j]; - const freqError = Math.abs(pk.freq - predictedFreq); - if (freqError > tol) continue; - - const phaseError = Math.abs(normalizeAngle(pk.phase - predictedPhase)); - const cost = freqError + phaseErrorWeight * phaseError * predictedFreq; - if (cost < bestCost) { bestCost = cost; bestIdx = j; } - } + const { bestIdx } = findBestPeak(frame.peaks, matched, predictedFreq, predictedPhase, tol, phaseErrorWeight); if (bestIdx >= 0) { const pk = frame.peaks[bestIdx]; diff --git a/tools/mq_editor/mq_synth.js b/tools/mq_editor/mq_synth.js index 2d4cf1b..4c68056 100644 --- a/tools/mq_editor/mq_synth.js +++ b/tools/mq_editor/mq_synth.js @@ -1,19 +1,6 @@ // MQ Synthesizer // Replica oscillator bank for sinusoidal synthesis, plus two-pole resonator mode -// Evaluate cubic bezier curve at time t -function evalBezier(curve, t) { - const dt = curve.t3 - curve.t0; - if (dt <= 0) return curve.v0; - let u = (t - curve.t0) / dt; - u = Math.max(0, Math.min(1, u)); - const u1 = 1.0 - u; - return u1*u1*u1 * curve.v0 + - 3*u1*u1*u * curve.v1 + - 3*u1*u*u * curve.v2 + - u*u*u * curve.v3; -} - // Deterministic LCG PRNG function randFloat(seed, min, max) { seed = (1664525 * seed + 1013904223) % 0x100000000; diff --git a/tools/mq_editor/utils.js b/tools/mq_editor/utils.js new file mode 100644 index 0000000..c38b1f5 --- /dev/null +++ b/tools/mq_editor/utils.js @@ -0,0 +1,36 @@ +// Shared utilities for mq_editor + +// Evaluate cubic bezier curve at time t (robust: handles dt<=0) +function evalBezier(curve, t) { + const dt = curve.t3 - curve.t0; + if (dt <= 0) return curve.v0; + let u = (t - curve.t0) / dt; + u = Math.max(0, Math.min(1, u)); + const u1 = 1.0 - u; + return u1*u1*u1 * curve.v0 + + 3*u1*u1*u * curve.v1 + + 3*u1*u*u * curve.v2 + + u*u*u * curve.v3; +} + +// Get canvas-relative {x, y} from a mouse event +function getCanvasCoords(e, canvas) { + const rect = canvas.getBoundingClientRect(); + return { x: e.clientX - rect.left, y: e.clientY - rect.top }; +} + +// Build upper/lower band point arrays for a frequency curve. +// factorAbove/factorBelow are fractional offsets (e.g. 0.02 = ±2%). +// Returns { upper: [[x,y],...], lower: [[x,y],...] } +function buildBandPoints(viewer, curve, factorAbove, factorBelow) { + const STEPS = 60; + const upper = [], lower = []; + for (let i = 0; i <= STEPS; ++i) { + const t = curve.t0 + (curve.t3 - curve.t0) * i / STEPS; + if (t < viewer.t_view_min - 0.01 || t > viewer.t_view_max + 0.01) continue; + const f = evalBezier(curve, t); + upper.push([viewer.timeToX(t), viewer.freqToY(f * (1 + factorAbove))]); + lower.push([viewer.timeToX(t), viewer.freqToY(f * (1 - factorBelow))]); + } + return { upper, lower }; +} diff --git a/tools/mq_editor/viewer.js b/tools/mq_editor/viewer.js index 76c57e2..2575cac 100644 --- a/tools/mq_editor/viewer.js +++ b/tools/mq_editor/viewer.js @@ -288,15 +288,7 @@ class SpectrogramViewer { const sa = rep.spread_above != null ? rep.spread_above : 0.02; const sb = rep.spread_below != null ? rep.spread_below : 0.02; - const STEPS = 60; - const upper = [], lower = []; - for (let i = 0; i <= STEPS; ++i) { - const t = curve.t0 + (curve.t3 - curve.t0) * i / STEPS; - if (t < this.t_view_min - 0.01 || t > this.t_view_max + 0.01) continue; - const f = evalBezier(curve, t); - upper.push([this.timeToX(t), this.freqToY(f * (1 + sa))]); - lower.push([this.timeToX(t), this.freqToY(f * (1 - sb))]); - } + const {upper, lower} = buildBandPoints(this, curve, sa, sb); if (upper.length < 2) return; const savedAlpha = ctx.globalAlpha; @@ -327,14 +319,7 @@ class SpectrogramViewer { ctx.setLineDash([]); // 50% drop-off reference lines (dotted, dimmer) - const p5upper = [], p5lower = []; - for (let i = 0; i <= STEPS; ++i) { - const t = curve.t0 + (curve.t3 - curve.t0) * i / STEPS; - if (t < this.t_view_min - 0.01 || t > this.t_view_max + 0.01) continue; - const f = evalBezier(curve, t); - p5upper.push([this.timeToX(t), this.freqToY(f * 1.50)]); - p5lower.push([this.timeToX(t), this.freqToY(f * 0.50)]); - } + const {upper: p5upper, lower: p5lower} = buildBandPoints(this, curve, 0.50, 0.50); if (p5upper.length >= 2) { ctx.globalAlpha = 0.55; ctx.strokeStyle = color; @@ -572,9 +557,7 @@ class SpectrogramViewer { const {canvas, tooltip} = this; canvas.addEventListener('mousedown', (e) => { - const rect = canvas.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; + const {x, y} = getCanvasCoords(e, canvas); // Check control point drag on selected partial if (this.selectedPartial >= 0 && this.selectedPartial < this.partials.length) { @@ -593,9 +576,7 @@ class SpectrogramViewer { }); canvas.addEventListener('mousemove', (e) => { - const rect = canvas.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; + const {x, y} = getCanvasCoords(e, canvas); if (this.dragState) { const t = Math.max(0, Math.min(this.t_max, this.canvasToTime(x))); @@ -717,13 +698,3 @@ class SpectrogramViewer { } } -// Bezier evaluation (shared utility) -function evalBezier(curve, t) { - let u = (t - curve.t0) / (curve.t3 - curve.t0); - u = Math.max(0, Math.min(1, u)); - const u1 = 1 - u; - return u1*u1*u1 * curve.v0 + - 3*u1*u1*u * curve.v1 + - 3*u1*u*u * curve.v2 + - u*u*u * curve.v3; -} -- cgit v1.2.3