summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--tools/mq_editor/index.html12
-rw-r--r--tools/mq_editor/mq_extract.js56
-rw-r--r--tools/mq_editor/mq_synth.js4
-rw-r--r--tools/mq_editor/viewer.js29
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