Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,141 @@
/**
* Spectral-flux onset detection function via a hand-rolled radix-2 real FFT.
*
* Per-frame magnitude spectra are differenced bin-by-bin; only positive
* differences count toward the ODF. No browser analyser node, no Node bindings — the
* implementation is pure numeric code over `Float32Array`/`Float64Array` so
* the module runs identically in Bun, Node.js, Deno, and browsers.
*/
/**
* In-place Cooley-Tukey radix-2 complex FFT.
*
* `re` and `im` carry the real and imaginary parts; both arrays must have the
* same length and that length must be a power of two. The transform is in
* place — on return `re[k]` and `im[k]` carry the `k`th complex bin.
*/
export function fftRadix2(re: Float64Array, im: Float64Array): void {
const n = re.length;
if (n <= 1) return;
// Bit-reversal permutation
let j = 0;
for (let i = 1; i < n; i++) {
let bit = n >> 1;
for (; (j & bit) !== 0; bit >>= 1) {
j ^= bit;
}
j ^= bit;
if (i < j) {
const tr = re[i]!;
const ti = im[i]!;
re[i] = re[j]!;
im[i] = im[j]!;
re[j] = tr;
im[j] = ti;
}
}
// Butterflies
for (let size = 2; size <= n; size <<= 1) {
const half = size >> 1;
const theta = (-2 * Math.PI) / size;
const wRe = Math.cos(theta);
const wIm = Math.sin(theta);
for (let start = 0; start < n; start += size) {
let curRe = 1;
let curIm = 0;
for (let k = 0; k < half; k++) {
const a = start + k;
const b = a + half;
const tre = re[b]! * curRe - im[b]! * curIm;
const tim = re[b]! * curIm + im[b]! * curRe;
re[b] = re[a]! - tre;
im[b] = im[a]! - tim;
re[a] = re[a]! + tre;
im[a] = im[a]! + tim;
const newRe = curRe * wRe - curIm * wIm;
const newIm = curRe * wIm + curIm * wRe;
curRe = newRe;
curIm = newIm;
}
}
}
}
/**
* Compute the magnitude spectrum of a real-valued frame of length
* `fftSize` (which must be a power of two). Returned array has length
* `fftSize / 2 + 1` (the non-redundant half plus Nyquist). A Hann window is
* applied in-place to a working copy of the frame before transform.
*/
export function magnitudeSpectrum(
frame: Float32Array,
fftSize: number
): Float64Array {
const re = new Float64Array(fftSize);
const im = new Float64Array(fftSize);
const n = Math.min(frame.length, fftSize);
const denom = Math.max(1, fftSize - 1);
for (let i = 0; i < n; i++) {
const w = 0.5 * (1 - Math.cos((2 * Math.PI * i) / denom));
re[i] = frame[i]! * w;
}
fftRadix2(re, im);
const halfPlusOne = (fftSize >> 1) + 1;
const mag = new Float64Array(halfPlusOne);
for (let k = 0; k < halfPlusOne; k++) {
const r = re[k]!;
const i = im[k]!;
mag[k] = Math.sqrt(r * r + i * i);
}
return mag;
}
/**
* Single-frame spectral flux: sum of positive bin-to-bin differences between
* the current magnitude spectrum and the previous one. `prevMag` is mutated
* in place to become the current spectrum so the caller can chain.
*/
export function spectralFlux(
frame: Float32Array,
prevMag: Float64Array,
fftSize: number
): number {
const mag = magnitudeSpectrum(frame, fftSize);
let flux = 0;
for (let k = 0; k < mag.length; k++) {
const m = mag[k]!;
const d = m - prevMag[k]!;
if (d > 0) flux += d;
prevMag[k] = m;
}
return flux;
}
/**
* Build a spectral-flux ODF over a sample stream by splitting it into
* `hop`-sample frames of `fftSize` samples each (zero-padded if necessary).
* Returns a `Float64Array` of length `ceil(length / hop)`.
*/
export function spectralFluxOdf(
mono: Float32Array,
fftSize: number,
hop: number
): Float64Array {
if (mono.length === 0 || fftSize <= 0 || hop <= 0) {
return new Float64Array(0);
}
const halfPlusOne = (fftSize >> 1) + 1;
const prev = new Float64Array(halfPlusOne);
const frames = Math.ceil(mono.length / hop);
const odf = new Float64Array(frames);
const buf = new Float32Array(fftSize);
for (let f = 0; f < frames; f++) {
const start = f * hop;
buf.fill(0);
const take = Math.min(fftSize, mono.length - start);
for (let i = 0; i < take; i++) buf[i] = mono[start + i]!;
odf[f] = spectralFlux(buf, prev, fftSize);
}
odf[0] = 0; // first frame has no predecessor signal
return odf;
}