From 35ebfac6c860cc7de7db447b57158a7a3a27daaa Mon Sep 17 00:00:00 2001 From: skal Date: Wed, 18 Feb 2026 05:11:12 +0100 Subject: fix(mq_editor): Catmull-Rom bezier fit, NaN guard, synth FFT toggle - evalBezier: guard dt<=0 to avoid NaN on degenerate curves - fitBezier: replace nearest-neighbor control points with Catmull-Rom tangents (Hermite->Bezier), curve now passes through endpoints - key 'a': toggle mini-spectrum between original and synth FFT handoff(Claude): bezier fix + synth FFT comparison Co-Authored-By: Claude Sonnet 4.6 --- tools/mq_editor/mq_extract.js | 56 ++++++++++++++++--------------------------- 1 file changed, 20 insertions(+), 36 deletions(-) (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 237f1ab..878b2bc 100644 --- a/tools/mq_editor/mq_extract.js +++ b/tools/mq_editor/mq_extract.js @@ -182,46 +182,30 @@ function trackPartials(frames, sampleRate) { return partials; } -// Fit cubic bezier curve to trajectory +// Fit cubic bezier curve to trajectory using Catmull-Rom tangents. +// Estimates end tangents via forward/backward differences, then converts +// Hermite form to Bezier: B1 = P0 + m0*dt/3, B2 = P3 - m3*dt/3. +// This guarantees the curve passes through both endpoints. function fitBezier(times, values) { - if (times.length < 4) { - // Not enough points, just use linear segments - return { - t0: times[0], v0: values[0], - t1: times[0], v1: values[0], - t2: times[times.length-1], v2: values[values.length-1], - t3: times[times.length-1], v3: values[values.length-1] - }; - } - - // Fix endpoints - const t0 = times[0]; - const t3 = times[times.length - 1]; - const v0 = values[0]; - const v3 = values[values.length - 1]; + const n = times.length - 1; + const t0 = times[0], v0 = values[0]; + const t3 = times[n], v3 = values[n]; + const dt = t3 - t0; - // Solve for interior control points via least squares - // Simplification: place at 1/3 and 2/3 positions - const t1 = t0 + (t3 - t0) / 3; - const t2 = t0 + 2 * (t3 - t0) / 3; + if (dt <= 0 || n === 0) { + return {t0, v0, t1: t0, v1: v0, t2: t3, v2: v3, t3, v3}; + } - // Find v1, v2 by evaluating at nearest data points - let v1 = v0, v2 = v3; - let minDist1 = Infinity, minDist2 = Infinity; + // Catmull-Rom endpoint tangents (forward diff at start, backward at end) + const dt0 = times[1] - times[0]; + const m0 = dt0 > 0 ? (values[1] - values[0]) / dt0 : 0; - for (let i = 0; i < times.length; ++i) { - const dist1 = Math.abs(times[i] - t1); - const dist2 = Math.abs(times[i] - t2); + const dtn = times[n] - times[n - 1]; + const m3 = dtn > 0 ? (values[n] - values[n - 1]) / dtn : 0; - if (dist1 < minDist1) { - minDist1 = dist1; - v1 = values[i]; - } - if (dist2 < minDist2) { - minDist2 = dist2; - v2 = values[i]; - } - } + // Hermite -> Bezier control points + const v1 = v0 + m0 * dt / 3; + const v2 = v3 - m3 * dt / 3; - return {t0, v0, t1, v1, t2, v2, t3, v3}; + return {t0, v0, t1: t0 + dt / 3, v1, t2: t0 + 2 * dt / 3, v2, t3, v3}; } -- cgit v1.2.3