Files
qtalker---/frontend/src/lib/fft.ts
Indiana 6edbbbbc2a feat: complete Quantumancy web app — full frontend + docs
Frontend (React 18 + TS + Vite):
- Landing: glitching hero, live /api/stats veil ticker, mode cards, featured spirits
- Séance: three.js shader ghost (hue/form per entity, mood + audio-reactive),
  Ouija planchette board spelling utterances, transcript with TTS replay,
  entity dossier, direct contact streaming, passive/active listening
- Modes: Wire Ghost telemetry panel, EVP mic anomaly detection, WebUSB
  RTL-SDR sweep + waterfall (hardware pass pending), Ouija/Direct Contact
- Codex: public registry + entity dossiers, rarity tiers, i18n EN/ES complete
- State: VeilSocket (reconnect/backoff), seance reducer, auth context
- 72 vitest tests green; served by FastAPI at :7777

Docs: README + as-built plans 3-7
2026-07-20 21:11:49 +00:00

96 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Radix-2 Cooley–Tukey FFT, in-place, iterative, bit-reversal ordered.
// Used by the RTL-SDR spectrum sweep. Length must be a power of two.
export type ComplexArray = { re: Float64Array; im: Float64Array }
export function nextPow2(n: number): number {
let p = 1
while (p < n) p <<= 1
return p
}
/**
* In-place iterative radix-2 FFT. `re`/`im` must have identical length
* and that length must be a power of two.
*/
export function fftInPlace(re: Float64Array, im: Float64Array): void {
const n = re.length
if (n !== im.length) throw new Error('re/im length mismatch')
if ((n & (n - 1)) !== 0) throw new Error('FFT length must be a power of two')
if (n <= 1) return
// 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 half = 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
let cwi = 0
for (let j = 0; j < half; j++) {
const uR = re[i + j]
const uI = im[i + j]
const vR = re[i + j + half] * cwr - im[i + j + half] * cwi
const vI = re[i + j + half] * cwi + im[i + j + half] * cwr
re[i + j] = uR + vR
im[i + j] = uI + vI
re[i + j + half] = uR - vR
im[i + j + half] = uI - vI
const nwr = cwr * wr - cwi * wi
cwi = cwr * wi + cwi * wr
cwr = nwr
}
}
}
}
/** Convenience wrapper returning magnitude spectrum (first n/2 bins). */
export function magnitudeSpectrum(re: Float64Array, im: Float64Array): Float64Array {
const r = Float64Array.from(re)
const i = Float64Array.from(im)
fftInPlace(r, i)
const half = r.length >> 1
const mags = new Float64Array(half)
for (let k = 0; k < half; k++) {
mags[k] = Math.hypot(r[k], i[k])
}
return mags
}
/**
* Power spectrum in dBFS-ish units from interleaved I/Q samples.
* `iq` layout: [i0, q0, i1, q1, ...]. Returns n/2 dB values.
*/
export function powerSpectrumDb(iq: Float64Array): Float64Array {
const n = nextPow2(iq.length >> 1)
const re = new Float64Array(n)
const im = new Float64Array(n)
for (let k = 0; k < n && 2 * k + 1 < iq.length; k++) {
re[k] = iq[2 * k]
im[k] = iq[2 * k + 1]
}
fftInPlace(re, im)
const half = n >> 1
const out = new Float64Array(half)
for (let k = 0; k < half; k++) {
const power = (re[k] * re[k] + im[k] * im[k]) / (n * n)
out[k] = 10 * Math.log10(power + 1e-12)
}
return out
}