diff options
Diffstat (limited to 'tools/mq_editor/mq_extract.js')
| -rw-r--r-- | tools/mq_editor/mq_extract.js | 94 |
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; |
