summaryrefslogtreecommitdiff
path: root/tools/mq_editor/mq_extract.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/mq_editor/mq_extract.js')
-rw-r--r--tools/mq_editor/mq_extract.js94
1 files changed, 94 insertions, 0 deletions
diff --git a/tools/mq_editor/mq_extract.js b/tools/mq_editor/mq_extract.js
index 8f6961f..ff38d63 100644
--- a/tools/mq_editor/mq_extract.js
+++ b/tools/mq_editor/mq_extract.js
@@ -432,6 +432,100 @@ function trackFromSeed(frames, seedTime, seedFreq, params) {
};
}
+// Track an iso-energy contour starting from (seedTime, seedFreq).
+// Instead of following spectral peaks, follows where energy ≈ seedEnergy.
+// Useful for broad/diffuse bass regions with no detectable peaks.
+// Returns a partial with large default spread, or null if seed energy is zero.
+function trackIsoContour(stftCache, seedTime, seedFreq, params) {
+ const { sampleRate, deathAge = 8 } = params;
+ const numFrames = stftCache.getNumFrames();
+ const fftSize = stftCache.fftSize;
+ const binHz = sampleRate / fftSize;
+ const halfBins = fftSize / 2;
+
+ // Find seed frame
+ let seedFrameIdx = 0, bestDt = Infinity;
+ for (let i = 0; i < numFrames; ++i) {
+ const dt = Math.abs(stftCache.getFrameAtIndex(i).time - seedTime);
+ if (dt < bestDt) { bestDt = dt; seedFrameIdx = i; }
+ }
+
+ const seedFrame = stftCache.getFrameAtIndex(seedFrameIdx);
+ const seedBin = Math.max(1, Math.min(halfBins - 2, Math.round(seedFreq / binHz)));
+ const targetSq = seedFrame.squaredAmplitude[seedBin];
+ if (targetSq <= 0) return null;
+ const targetDB = 10 * Math.log10(targetSq);
+
+ const trackingRatio = 0.15; // larger search window than peak tracker
+ const minTrackHz = 30;
+ const maxDbDev = 15; // dB: declare miss if nothing within this range
+
+ // Find bin minimizing |dB(b) - targetDB| near refBin, with mild position bias.
+ function findContourBin(sq, refBin) {
+ const tol = Math.max(refBin * binHz * trackingRatio, minTrackHz);
+ const tolBins = Math.ceil(tol / binHz);
+ const lo = Math.max(1, refBin - tolBins);
+ const hi = Math.min(halfBins - 2, refBin + tolBins);
+ let bestBin = -1, bestCost = Infinity;
+ for (let b = lo; b <= hi; ++b) {
+ const dE = Math.abs(10 * Math.log10(Math.max(sq[b], 1e-20)) - targetDB);
+ if (dE > maxDbDev) continue;
+ const dPos = Math.abs(b - refBin) / Math.max(1, tolBins);
+ const cost = dE + 3 * dPos; // energy match dominates, position breaks ties
+ if (cost < bestCost) { bestCost = cost; bestBin = b; }
+ }
+ return bestBin;
+ }
+
+ const times = [seedFrame.time];
+ const freqs = [seedBin * binHz];
+ const amps = [Math.sqrt(Math.max(0, targetSq))];
+
+ // Forward pass
+ let fwdBin = seedBin, fwdAge = 0;
+ for (let i = seedFrameIdx + 1; i < numFrames; ++i) {
+ const frame = stftCache.getFrameAtIndex(i);
+ const b = findContourBin(frame.squaredAmplitude, fwdBin);
+ if (b >= 0) {
+ times.push(frame.time);
+ freqs.push(b * binHz);
+ amps.push(Math.sqrt(Math.max(0, frame.squaredAmplitude[b])));
+ fwdBin = b; fwdAge = 0;
+ } else { if (++fwdAge > deathAge) break; }
+ }
+
+ // Backward pass
+ const bwdTimes = [], bwdFreqs = [], bwdAmps = [];
+ let bwdBin = seedBin, bwdAge = 0;
+ for (let i = seedFrameIdx - 1; i >= 0; --i) {
+ const frame = stftCache.getFrameAtIndex(i);
+ const b = findContourBin(frame.squaredAmplitude, bwdBin);
+ if (b >= 0) {
+ bwdTimes.unshift(frame.time);
+ bwdFreqs.unshift(b * binHz);
+ bwdAmps.unshift(Math.sqrt(Math.max(0, frame.squaredAmplitude[b])));
+ bwdBin = b; bwdAge = 0;
+ } else { if (++bwdAge > deathAge) break; }
+ }
+
+ const allTimes = [...bwdTimes, ...times];
+ const allFreqs = [...bwdFreqs, ...freqs];
+ const allAmps = [...bwdAmps, ...amps];
+ if (allTimes.length < 2) return null;
+
+ const freqCurve = fitBezier(allTimes, allFreqs);
+ const ac = fitBezier(allTimes, allAmps);
+ freqCurve.a0 = ac.v0; freqCurve.a1 = ac.v1;
+ freqCurve.a2 = ac.v2; freqCurve.a3 = ac.v3;
+
+ return {
+ times: allTimes, freqs: allFreqs, amps: allAmps,
+ phases: new Array(allTimes.length).fill(0),
+ muted: false, freqCurve,
+ replicas: { decay_alpha: 0.1, jitter: 0.05, spread_above: 0.15, spread_below: 0.15 },
+ };
+}
+
// Fit cubic bezier to trajectory using least-squares for inner control points
function fitBezier(times, values) {
const n = times.length - 1;