// Minimal iterative radix-2 complex FFT, in-place on separate re/im arrays. export function fft(re: Float64Array, im: Float64Array): void { const n = re.length; if ((n & (n - 1)) !== 0) throw new Error('FFT length must be a power of 2'); // bit-reversal permutation 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) { const tr = re[i]; re[i] = re[j]; re[j] = tr; const ti = im[i]; im[i] = im[j]; im[j] = ti; } } for (let len = 2; len <= n; len <<= 1) { const ang = (-2 * Math.PI) / len; const wr = Math.cos(ang); const wi = Math.sin(ang); for (let i = 0; i < n; i += len) { let cwr = 1, cwi = 0; for (let k = 0; k < len / 2; k++) { const ur = re[i + k], ui = im[i + k]; const vr = re[i + k + len / 2] * cwr - im[i + k + len / 2] * cwi; const vi = re[i + k + len / 2] * cwi + im[i + k + len / 2] * cwr; re[i + k] = ur + vr; im[i + k] = ui + vi; re[i + k + len / 2] = ur - vr; im[i + k + len / 2] = ui - vi; const nwr = cwr * wr - cwi * wi; cwi = cwr * wi + cwi * wr; cwr = nwr; } } } } // Magnitude spectrum of a real signal with a Hann window, zero-padded to the // next power of two (at least minSize). Returns bins 0..N/2. export function magnitudeSpectrum(x: Float32Array | Float64Array, minSize = 16384): { mag: Float64Array; size: number; } { let size = minSize; while (size < x.length) size <<= 1; const re = new Float64Array(size); const im = new Float64Array(size); const n = x.length; for (let i = 0; i < n; i++) { const w = 0.5 * (1 - Math.cos((2 * Math.PI * i) / (n - 1))); re[i] = x[i] * w; } fft(re, im); const half = size / 2; const mag = new Float64Array(half + 1); for (let i = 0; i <= half; i++) mag[i] = Math.hypot(re[i], im[i]); return { mag, size }; } // Simple STFT magnitude in dB for the spectrogram, frames x bins. export function stft( x: Float32Array, win = 512, hop = 128, ): { frames: number; bins: number; db: Float32Array; floorDb: number } { const nFrames = Math.max(1, Math.floor((x.length - win) / hop) + 1); const bins = win / 2 + 1; const db = new Float32Array(nFrames * bins); const re = new Float64Array(win); const im = new Float64Array(win); const hann = new Float64Array(win); for (let i = 0; i < win; i++) hann[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / (win - 1))); let maxDb = -Infinity; for (let f = 0; f < nFrames; f++) { const off = f * hop; for (let i = 0; i < win; i++) { re[i] = (off + i < x.length ? x[off + i] : 0) * hann[i]; im[i] = 0; } fft(re, im); for (let b = 0; b < bins; b++) { const m = Math.hypot(re[b], im[b]); const v = 20 * Math.log10(m + 1e-12); db[f * bins + b] = v; if (v > maxDb) maxDb = v; } } // normalize so 0 dB is the frame-bin maximum for (let i = 0; i < db.length; i++) db[i] -= maxDb; return { frames: nFrames, bins, db, floorDb: -90 }; }