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.js66
1 files changed, 66 insertions, 0 deletions
diff --git a/tools/mq_editor/mq_extract.js b/tools/mq_editor/mq_extract.js
index 8a0ea0e..d29cfbc 100644
--- a/tools/mq_editor/mq_extract.js
+++ b/tools/mq_editor/mq_extract.js
@@ -182,6 +182,72 @@ function expandPartialsLeft(partials, frames) {
}
}
+// Autodetect spread_above / spread_below from the spectrogram.
+// For each (subsampled) STFT frame within the partial, measures the
+// half-power (-3dB) width of the spectral peak above and below the center.
+// spread = half_bandwidth / f0 (fractional).
+function autodetectSpread(partial, stftCache, fftSize, sampleRate) {
+ const curve = partial.freqCurve;
+ if (!curve || !stftCache) return {spread_above: 0.02, spread_below: 0.02};
+
+ const numFrames = stftCache.getNumFrames();
+ const binHz = sampleRate / fftSize;
+ const halfBins = fftSize / 2;
+
+ let sumAbove = 0, sumBelow = 0, count = 0;
+
+ const STEP = 4;
+ for (let fi = 0; fi < numFrames; fi += STEP) {
+ const frame = stftCache.getFrameAtIndex(fi);
+ if (!frame) continue;
+ const t = frame.time;
+ if (t < curve.t0 || t > curve.t3) continue;
+
+ const f0 = evalBezier(curve, t);
+ if (f0 <= 0) continue;
+
+ const sq = frame.squaredAmplitude;
+ if (!sq) continue;
+
+ // Find peak bin in ±10% window
+ const binCenter = f0 / binHz;
+ const searchBins = Math.max(3, Math.round(f0 * 0.10 / binHz));
+ const binLo = Math.max(1, Math.floor(binCenter - searchBins));
+ const binHi = Math.min(halfBins - 2, Math.ceil(binCenter + searchBins));
+
+ let peakBin = binLo, peakVal = sq[binLo];
+ for (let b = binLo + 1; b <= binHi; ++b) {
+ if (sq[b] > peakVal) { peakVal = sq[b]; peakBin = b; }
+ }
+
+ const halfPower = peakVal * 0.5; // -3dB in power
+
+ // Walk above peak until half-power, interpolate crossing
+ let aboveBin = peakBin;
+ while (aboveBin < halfBins - 1 && sq[aboveBin] > halfPower) ++aboveBin;
+ const tA = aboveBin > peakBin && sq[aboveBin - 1] !== sq[aboveBin]
+ ? (halfPower - sq[aboveBin - 1]) / (sq[aboveBin] - sq[aboveBin - 1])
+ : 0;
+ const widthAbove = (aboveBin - 1 + tA - peakBin) * binHz;
+
+ // Walk below peak until half-power, interpolate crossing
+ let belowBin = peakBin;
+ while (belowBin > 1 && sq[belowBin] > halfPower) --belowBin;
+ const tB = belowBin < peakBin && sq[belowBin + 1] !== sq[belowBin]
+ ? (halfPower - sq[belowBin + 1]) / (sq[belowBin] - sq[belowBin + 1])
+ : 0;
+ const widthBelow = (peakBin - belowBin - 1 + tB) * binHz;
+
+ sumAbove += (widthAbove / f0) * (widthAbove / f0);
+ sumBelow += (widthBelow / f0) * (widthBelow / f0);
+ ++count;
+ }
+
+ const spread_above = count > 0 ? Math.sqrt(sumAbove / count) : 0.01;
+ const spread_below = count > 0 ? Math.sqrt(sumBelow / count) : 0.01;
+ return {spread_above, spread_below};
+}
+
// Fit cubic bezier to trajectory using samples at ~1/3 and ~2/3 as control points
function fitBezier(times, values) {
const n = times.length - 1;