|
1 | 1 | /** |
2 | | - * Real-valued split-radix FFT. |
| 2 | + * FFT for real and complex signals. |
3 | 3 | * |
4 | 4 | * @module fourier-transform |
5 | 5 | */ |
6 | 6 |
|
7 | | -const { sqrt, sin, cos, abs, SQRT1_2 } = Math |
| 7 | +const { sqrt, sin, cos, abs, SQRT1_2, SQRT2 } = Math |
8 | 8 | const TWO_PI = 6.283185307179586 |
9 | 9 |
|
10 | 10 | // Per-size cached buffers and precomputed twiddle factors |
@@ -218,6 +218,100 @@ export default function rfft(input, output) { |
218 | 218 | return out |
219 | 219 | } |
220 | 220 |
|
| 221 | +// --- Complex radix-2 Cooley-Tukey FFT (in-place) --- |
| 222 | + |
| 223 | +const cCache = new Map() |
| 224 | +let cLastN = 0, cLastEntry = null |
| 225 | + |
| 226 | +function cInit(N) { |
| 227 | + const bits = 31 - Math.clz32(N) |
| 228 | + const perm = new Uint32Array(N) |
| 229 | + for (let i = 0; i < N; i++) { |
| 230 | + let rev = 0, v = i |
| 231 | + for (let j = 0; j < bits; j++) { rev = (rev << 1) | (v & 1); v >>= 1 } |
| 232 | + perm[i] = rev |
| 233 | + } |
| 234 | + // Separate forward/inverse twiddle tables (no per-butterfly dir multiply) |
| 235 | + const twRe = new Float64Array(N) |
| 236 | + const twFwd = new Float64Array(N) |
| 237 | + const twInv = new Float64Array(N) |
| 238 | + let ti = 0 |
| 239 | + for (let len = 2; len <= N; len <<= 1) { |
| 240 | + const half = len >> 1, angle = TWO_PI / len |
| 241 | + for (let j = 0; j < half; j++) { |
| 242 | + const s = sin(j * angle) |
| 243 | + twRe[ti] = cos(j * angle) |
| 244 | + twFwd[ti] = -s |
| 245 | + twInv[ti] = s |
| 246 | + ti++ |
| 247 | + } |
| 248 | + } |
| 249 | + const entry = { perm, twRe, twFwd, twInv } |
| 250 | + cCache.set(N, entry) |
| 251 | + return entry |
| 252 | +} |
| 253 | + |
| 254 | +function cGetEntry(N) { |
| 255 | + if (N === cLastN) return cLastEntry |
| 256 | + const entry = cCache.get(N) || cInit(N) |
| 257 | + cLastN = N |
| 258 | + cLastEntry = entry |
| 259 | + return entry |
| 260 | +} |
| 261 | + |
| 262 | +function cTransform(re, im, inverse) { |
| 263 | + const N = re.length |
| 264 | + if (N < 2 || (N & (N - 1))) throw Error('Length must be a power of 2 (>= 2).') |
| 265 | + const entry = cGetEntry(N) |
| 266 | + const { perm, twRe } = entry |
| 267 | + const twIm = inverse ? entry.twInv : entry.twFwd |
| 268 | + |
| 269 | + // Bit-reversal permutation (in-place swap) |
| 270 | + for (let i = 0; i < N; i++) { |
| 271 | + const j = perm[i] |
| 272 | + if (i < j) { |
| 273 | + let t = re[i]; re[i] = re[j]; re[j] = t |
| 274 | + t = im[i]; im[i] = im[j]; im[j] = t |
| 275 | + } |
| 276 | + } |
| 277 | + |
| 278 | + // Butterfly stages |
| 279 | + let ti = 0 |
| 280 | + for (let len = 2; len <= N; len <<= 1) { |
| 281 | + const half = len >> 1 |
| 282 | + for (let i = 0; i < N; i += len) { |
| 283 | + for (let j = 0; j < half; j++) { |
| 284 | + const wRe = twRe[ti + j], wIm = twIm[ti + j] |
| 285 | + const a = i + j, b = a + half |
| 286 | + const tRe = wRe * re[b] - wIm * im[b] |
| 287 | + const tIm = wRe * im[b] + wIm * re[b] |
| 288 | + re[b] = re[a] - tRe; im[b] = im[a] - tIm |
| 289 | + re[a] += tRe; im[a] += tIm |
| 290 | + } |
| 291 | + } |
| 292 | + ti += half |
| 293 | + } |
| 294 | + |
| 295 | + if (inverse) { |
| 296 | + const inv = 1 / N |
| 297 | + for (let i = 0; i < N; i++) { re[i] *= inv; im[i] *= inv } |
| 298 | + } |
| 299 | +} |
| 300 | + |
| 301 | +/** |
| 302 | + * In-place complex forward FFT (unnormalized). |
| 303 | + * @param {Float64Array} re - Real parts (length must be power of 2, >= 2). |
| 304 | + * @param {Float64Array} im - Imaginary parts (same length). |
| 305 | + */ |
| 306 | +export function cfft(re, im) { cTransform(re, im, false) } |
| 307 | + |
| 308 | +/** |
| 309 | + * In-place complex inverse FFT (1/N normalized). |
| 310 | + * @param {Float64Array} re - Real parts (length must be power of 2, >= 2). |
| 311 | + * @param {Float64Array} im - Imaginary parts (same length). |
| 312 | + */ |
| 313 | +export function cifft(re, im) { cTransform(re, im, true) } |
| 314 | + |
221 | 315 | /** |
222 | 316 | * Compute complex spectrum of real-valued input (unnormalized DFT). |
223 | 317 | * @param {ArrayLike<number>} input - length must be power of 2 (>= 2). |
@@ -245,3 +339,152 @@ export function fft(input, output) { |
245 | 339 |
|
246 | 340 | return complex |
247 | 341 | } |
| 342 | + |
| 343 | +// Inverse split-radix DIF core — mirror of forward DIT transform() |
| 344 | +function inverseTransform(N, entry) { |
| 345 | + const { x, tw, stages, perm } = entry |
| 346 | + const numStages = stages.length |
| 347 | + |
| 348 | + // DIF: butterflies from large n2 to small (reverse of forward) |
| 349 | + let n2 = N << 1 |
| 350 | + for (let s = 0; s < numStages; s++) { |
| 351 | + n2 >>= 1 |
| 352 | + const n4 = n2 >>> 2 |
| 353 | + const n8 = n4 >>> 1 |
| 354 | + const si = numStages - 1 - s |
| 355 | + |
| 356 | + // Zero-angle butterflies |
| 357 | + let ix = 0, id = n2 << 1 |
| 358 | + do { |
| 359 | + for (let i0 = ix; i0 < N; i0 += id) { |
| 360 | + const i1 = i0, i2 = i1 + n4, i3 = i2 + n4, i4 = i3 + n4 |
| 361 | + |
| 362 | + let t1 = x[i1] - x[i3] |
| 363 | + x[i1] += x[i3] |
| 364 | + x[i2] += x[i2] |
| 365 | + x[i4] += x[i4] |
| 366 | + x[i3] = t1 - x[i4] |
| 367 | + x[i4] += t1 |
| 368 | + } |
| 369 | + ix = (id << 1) - n2 |
| 370 | + id <<= 2 |
| 371 | + } while (ix < N) |
| 372 | + |
| 373 | + // SQRT2 section (only when n8 >= 1, i.e. n4 !== 1) |
| 374 | + if (n8 >= 1) { |
| 375 | + ix = 0; id = n2 << 1 |
| 376 | + do { |
| 377 | + for (let i0 = ix; i0 < N; i0 += id) { |
| 378 | + const j1 = i0 + n8, j2 = j1 + n4, j3 = j2 + n4, j4 = j3 + n4 |
| 379 | + |
| 380 | + let t1 = x[j1] - x[j2] |
| 381 | + x[j1] += x[j2] |
| 382 | + let t2 = x[j4] + x[j3] |
| 383 | + x[j2] = x[j4] - x[j3] |
| 384 | + t2 = -t2 * SQRT2 |
| 385 | + t1 *= SQRT2 |
| 386 | + x[j3] = t2 + t1 |
| 387 | + x[j4] = t2 - t1 |
| 388 | + } |
| 389 | + ix = (id << 1) - n2 |
| 390 | + id <<= 2 |
| 391 | + } while (ix < N) |
| 392 | + } |
| 393 | + |
| 394 | + // Twiddle factor butterflies (same twiddles, inverse operations) |
| 395 | + const { offset, count } = stages[si] |
| 396 | + for (let j = 0; j < count; j++) { |
| 397 | + const ti = (offset + j) << 2 |
| 398 | + const cc1 = tw[ti], ss1 = tw[ti + 1], cc3 = tw[ti + 2], ss3 = tw[ti + 3] |
| 399 | + |
| 400 | + ix = 0; id = n2 << 1 |
| 401 | + do { |
| 402 | + for (let i0 = ix; i0 < N; i0 += id) { |
| 403 | + const i1 = i0 + j + 1 |
| 404 | + const i2 = i1 + n4 |
| 405 | + const i3 = i2 + n4 |
| 406 | + const i4 = i3 + n4 |
| 407 | + const i5 = i0 + n4 - j - 1 |
| 408 | + const i6 = i5 + n4 |
| 409 | + const i7 = i6 + n4 |
| 410 | + const i8 = i7 + n4 |
| 411 | + |
| 412 | + let t1 = x[i1] - x[i6] |
| 413 | + x[i1] += x[i6] |
| 414 | + let t2 = x[i5] - x[i2] |
| 415 | + x[i5] += x[i2] |
| 416 | + let t3 = x[i8] + x[i3] |
| 417 | + x[i6] = x[i8] - x[i3] |
| 418 | + let t4 = x[i4] + x[i7] |
| 419 | + x[i2] = x[i4] - x[i7] |
| 420 | + |
| 421 | + const t5 = t1 - t4 |
| 422 | + t1 += t4 |
| 423 | + t4 = t2 - t3 |
| 424 | + t2 += t3 |
| 425 | + |
| 426 | + x[i7] = t5 * ss1 - t4 * cc1 |
| 427 | + x[i3] = t5 * cc1 + t4 * ss1 |
| 428 | + x[i4] = t1 * cc3 - t2 * ss3 |
| 429 | + x[i8] = t1 * ss3 + t2 * cc3 |
| 430 | + } |
| 431 | + ix = (id << 1) - n2 |
| 432 | + id <<= 2 |
| 433 | + } while (ix < N) |
| 434 | + } |
| 435 | + } |
| 436 | + |
| 437 | + // Length-2 butterflies (self-inverse up to scaling) |
| 438 | + for (let ix = 0, id = 4; ix < N; id *= 4) { |
| 439 | + for (let i0 = ix; i0 < N; i0 += id) { |
| 440 | + const t = x[i0] - x[i0 + 1] |
| 441 | + x[i0] += x[i0 + 1] |
| 442 | + x[i0 + 1] = t |
| 443 | + } |
| 444 | + ix = 2 * (id - 1) |
| 445 | + } |
| 446 | + |
| 447 | + // Bit-reversal permutation (in-place swap) |
| 448 | + for (let i = 0; i < N; i++) { |
| 449 | + const j = perm[i] |
| 450 | + if (i < j) { const t = x[i]; x[i] = x[j]; x[j] = t } |
| 451 | + } |
| 452 | + |
| 453 | + // Scale by 1/N |
| 454 | + const inv = 1 / N |
| 455 | + for (let i = 0; i < N; i++) x[i] *= inv |
| 456 | +} |
| 457 | + |
| 458 | +/** |
| 459 | + * Inverse real FFT — recover time-domain signal from complex spectrum. |
| 460 | + * Uses native split-radix DIF algorithm (no complex FFT overhead). |
| 461 | + * @param {Float64Array} re - Real parts (length N/2+1). |
| 462 | + * @param {Float64Array} im - Imaginary parts (length N/2+1). |
| 463 | + * @param {Float64Array} [output] - Optional buffer (length N). If omitted, returns internal view (overwritten on next call with same N). |
| 464 | + * @returns {Float64Array} Real time-domain signal, length N. |
| 465 | + */ |
| 466 | +export function irfft(re, im, output) { |
| 467 | + const bins = re.length |
| 468 | + const N = (bins - 1) << 1 |
| 469 | + if (N < 2 || (N & (N - 1))) throw Error('Input must have N/2+1 bins where N is power of 2 (>= 2).') |
| 470 | + |
| 471 | + const entry = getEntry(N) |
| 472 | + const { x } = entry |
| 473 | + const half = N >>> 1 |
| 474 | + |
| 475 | + // Pack into half-complex format: x[k]=Re(X[k]), x[N-k]=Im(X[k]) |
| 476 | + x[0] = re[0] |
| 477 | + x[half] = re[half] |
| 478 | + for (let k = 1; k < half; k++) { |
| 479 | + x[k] = re[k] |
| 480 | + x[N - k] = im[k] |
| 481 | + } |
| 482 | + |
| 483 | + inverseTransform(N, entry) |
| 484 | + |
| 485 | + if (output) { |
| 486 | + for (let i = 0; i < N; i++) output[i] = x[i] |
| 487 | + return output |
| 488 | + } |
| 489 | + return x |
| 490 | +} |
0 commit comments