diff options
| -rw-r--r-- | tools/mq_editor/index.html | 12 | ||||
| -rw-r--r-- | tools/mq_editor/mq_extract.js | 56 | ||||
| -rw-r--r-- | tools/mq_editor/mq_synth.js | 4 | ||||
| -rw-r--r-- | tools/mq_editor/viewer.js | 29 |
4 files changed, 59 insertions, 42 deletions
diff --git a/tools/mq_editor/index.html b/tools/mq_editor/index.html index e8386c2..d7cec6a 100644 --- a/tools/mq_editor/index.html +++ b/tools/mq_editor/index.html @@ -315,6 +315,12 @@ setStatus(`Synthesizing ${keepCount}/${extractedPartials.length} partials (${keepPct.value}%)...`, 'info'); const pcm = synthesizeMQ(partialsToUse, sampleRate, duration); + // Build STFT cache for synth signal (for FFT comparison via key 'a') + if (viewer) { + const synthStft = new STFTCache(pcm, sampleRate, fftSize, parseInt(hopSize.value)); + viewer.setSynthStftCache(synthStft); + } + // Create audio buffer const synthBuffer = audioContext.createBuffer(1, pcm.length, sampleRate); synthBuffer.getChannelData(0).set(pcm); @@ -360,6 +366,12 @@ } else if (e.code === 'KeyP') { e.preventDefault(); if (viewer) viewer.togglePeaks(); + } else if (e.code === 'KeyA') { + e.preventDefault(); + if (viewer) { + viewer.showSynthFFT = !viewer.showSynthFFT; + viewer.renderSpectrum(); + } } }); </script> 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}; } diff --git a/tools/mq_editor/mq_synth.js b/tools/mq_editor/mq_synth.js index f1c7f73..8dcb4bd 100644 --- a/tools/mq_editor/mq_synth.js +++ b/tools/mq_editor/mq_synth.js @@ -4,7 +4,9 @@ // Evaluate cubic bezier curve at time t function evalBezier(curve, t) { // Normalize t to [0, 1] - let u = (t - curve.t0) / (curve.t3 - curve.t0); + 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)); // Cubic interpolation diff --git a/tools/mq_editor/viewer.js b/tools/mq_editor/viewer.js index e8780e7..21c0a0c 100644 --- a/tools/mq_editor/viewer.js +++ b/tools/mq_editor/viewer.js @@ -40,6 +40,8 @@ class SpectrogramViewer { this.spectrumCanvas = document.getElementById('spectrumCanvas'); this.spectrumCtx = this.spectrumCanvas ? this.spectrumCanvas.getContext('2d') : null; this.spectrumTime = 0; // Time to display spectrum for + this.showSynthFFT = false; // Toggle: false=original, true=synth + this.synthStftCache = null; // Setup event handlers this.setupMouseHandlers(); @@ -467,9 +469,16 @@ class SpectrogramViewer { return this.stftCache.getMagnitudeDB(time, freq); } + setSynthStftCache(cache) { + this.synthStftCache = cache; + } + renderSpectrum() { if (!this.spectrumCtx || !this.stftCache) return; + const useSynth = this.showSynthFFT && this.synthStftCache; + const cache = useSynth ? this.synthStftCache : this.stftCache; + const canvas = this.spectrumCanvas; const ctx = this.spectrumCtx; const width = canvas.width; @@ -479,10 +488,10 @@ class SpectrogramViewer { ctx.fillStyle = '#1e1e1e'; ctx.fillRect(0, 0, width, height); - const spectrum = this.stftCache.getFFT(this.spectrumTime); + const spectrum = cache.getFFT(this.spectrumTime); if (!spectrum) return; - const fftSize = this.stftCache.fftSize; + const fftSize = cache.fftSize; // Draw bars const numBars = 100; @@ -503,14 +512,24 @@ class SpectrogramViewer { const barHeight = intensity * height; const x = i * barWidth; - // Gradient from bottom (cyan) to top (yellow) + // Color: cyan/yellow for original, green/lime for synth const gradient = ctx.createLinearGradient(0, height - barHeight, 0, height); - gradient.addColorStop(0, '#4af'); - gradient.addColorStop(1, '#fa4'); + if (useSynth) { + gradient.addColorStop(0, '#4f8'); + gradient.addColorStop(1, '#af4'); + } else { + gradient.addColorStop(0, '#4af'); + gradient.addColorStop(1, '#fa4'); + } ctx.fillStyle = gradient; ctx.fillRect(x, height - barHeight, barWidth - 1, barHeight); } + + // Label + ctx.fillStyle = useSynth ? '#4f8' : '#4af'; + ctx.font = '9px monospace'; + ctx.fillText(useSynth ? 'SYNTH [a]' : 'ORIG [a]', 4, 10); } // Utilities |
