/** In-place radix-2 complex FFT; lengths must be powers of two. Small and * sufficient for the analysis windows used here (1024 and 4096). */ export function fft(re: Float32Array, im: Float32Array): void { const n = re.length; if ((n & (n - 1)) !== 0) throw new Error(`fft wants a power of two, got ${n}`); for (let i = 1, j = 0; i < n; i++) { let bit = n >> 1; for (; j & bit; bit >>= 1) j ^= bit; j ^= bit; if (i < j) { let t = re[i]; re[i] = re[j]; re[j] = t; t = im[i]; im[i] = im[j]; im[j] = t; } } for (let len = 2; len <= n; len <<= 1) { const half = len >> 1; const ang = (-2 * Math.PI) / len; const wr = Math.cos(ang), wi = Math.sin(ang); for (let i = 0; i < n; i += len) { let cr = 1, ci = 0; for (let k = 0; k < half; k++) { const ar = re[i + k], ai = im[i + k]; const br = re[i + k + half] * cr - im[i + k + half] * ci; const bi = re[i + k + half] * ci + im[i + k + half] * cr; re[i + k] = ar + br; im[i + k] = ai + bi; re[i + k + half] = ar - br; im[i + k + half] = ai - bi; const t = cr * wr - ci * wi; ci = cr * wi + ci * wr; cr = t; } } } } /** Hann-windowed power spectrum of x zero-padded to size n (power of two); * returns n/2 bins. Bin k is frequency k * sr / n. */ export function powerSpectrum(x: Float32Array, n: number): Float32Array { const re = new Float32Array(n); const im = new Float32Array(n); const m = Math.min(x.length, n); for (let i = 0; i < m; i++) { const w = 0.5 - 0.5 * Math.cos((2 * Math.PI * i) / (m - 1 || 1)); re[i] = x[i] * w; } fft(re, im); const p = new Float32Array(n >> 1); for (let k = 0; k < p.length; k++) p[k] = re[k] * re[k] + im[k] * im[k]; return p; }