Skip to content

Commit 5cfef75

Browse files
committed
Implement ifft, irfft, cfft
1 parent b4e8781 commit 5cfef75

6 files changed

Lines changed: 514 additions & 8 deletions

File tree

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) Dmitry Ivanov
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

changelog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
## Added
1313

1414
- `fft()` named export — returns complex DFT as `[re, im]`, N/2+1 bins, unnormalized.
15+
- `irfft(re, im)` — inverse of `fft()`, native split-radix DIF inverse (~1.8x faster than complex IFFT approach).
16+
- `cfft(re, im)` / `cifft(re, im)` — in-place complex FFT and inverse FFT (radix-2 Cooley-Tukey).
1517
- Optional output buffer parameter for both `rfft()` and `fft()`.
1618

1719
## Performance

index.js

Lines changed: 245 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
/**
2-
* Real-valued split-radix FFT.
2+
* FFT for real and complex signals.
33
*
44
* @module fourier-transform
55
*/
66

7-
const { sqrt, sin, cos, abs, SQRT1_2 } = Math
7+
const { sqrt, sin, cos, abs, SQRT1_2, SQRT2 } = Math
88
const TWO_PI = 6.283185307179586
99

1010
// Per-size cached buffers and precomputed twiddle factors
@@ -218,6 +218,100 @@ export default function rfft(input, output) {
218218
return out
219219
}
220220

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+
221315
/**
222316
* Compute complex spectrum of real-valued input (unnormalized DFT).
223317
* @param {ArrayLike<number>} input - length must be power of 2 (>= 2).
@@ -245,3 +339,152 @@ export function fft(input, output) {
245339

246340
return complex
247341
}
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+
}

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "fourier-transform",
3-
"version": "2.1.0",
3+
"version": "2.0.0",
44
"description": "Minimalistic and efficient FFT implementation",
55
"type": "module",
66
"exports": "./index.js",
@@ -12,11 +12,15 @@
1212
"type": "git",
1313
"url": "git+https://github.com/scijs/fourier-transform.git"
1414
},
15+
"files": ["index.js"],
1516
"keywords": [
1617
"fourier",
1718
"dsp",
1819
"fft",
1920
"rfft",
21+
"irfft",
22+
"ifft",
23+
"cfft",
2024
"dft"
2125
],
2226
"author": "Dmitry Iv. <dfcreative@gmail.com>",

readme.md

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# fourier-transform [![test](https://github.com/scijs/fourier-transform/actions/workflows/test.yml/badge.svg)](https://github.com/scijs/fourier-transform/actions/workflows/test.yml)
22

3-
Efficient real-valued FFT for 2<sup>n</sup>-size inputs. Split-radix algorithm with precomputed twiddle factors and typed-array buffers. Zero dependencies.
3+
FFT for real and complex signals. Split-radix real FFT + radix-2 complex FFT. Precomputed twiddle factors, typed-array buffers, zero dependencies.
44

55
## Usage
66

@@ -14,10 +14,19 @@ const spectrum = rfft(waveform)
1414
```js
1515
import { fft } from 'fourier-transform'
1616

17-
// complex DFT output (N/2+1 bins, unnormalized)
17+
// complex DFT of real input (N/2+1 bins, unnormalized)
1818
const [re, im] = fft(waveform)
1919
```
2020

21+
```js
22+
import { cfft, cifft } from 'fourier-transform'
23+
24+
// in-place complex FFT / inverse FFT
25+
const re = new Float64Array(N), im = new Float64Array(N)
26+
cfft(re, im) // forward
27+
cifft(re, im) // inverse (1/N normalized)
28+
```
29+
2130
## API
2231

2332
### `rfft(input, output?)` — default export
@@ -38,9 +47,31 @@ Returns complex DFT as `[re, im]`, each `Float64Array` of length N/2+1 (DC throu
3847
- Unnormalized: `X[k] = sum( x[n] * e^(-j*2*pi*k*n/N) )`.
3948
- DC and Nyquist bins always have `im = 0` (real input).
4049

50+
### `irfft(re, im, output?)` — named export
51+
52+
Inverse of `fft()` — recovers time-domain signal from complex spectrum. Returns `Float64Array` of length N.
53+
54+
- `re`, `im``Float64Array` of length N/2+1 (as returned by `fft()`).
55+
- `im[0]` and `im[N/2]` are ignored (half-complex format has no slot for them).
56+
- Native split-radix DIF inverse — no complex FFT overhead.
57+
58+
```js
59+
const [re, im] = fft(signal)
60+
// modify spectrum...
61+
const recovered = irfft(re, im)
62+
```
63+
64+
### `cfft(re, im)` — named export
65+
66+
In-place complex forward FFT (unnormalized). Both `re` and `im` must be `Float64Array` of equal power-of-2 length (>= 2). Modifies arrays in place.
67+
68+
### `cifft(re, im)` — named export
69+
70+
In-place complex inverse FFT (1/N normalized). Same signature as `cfft`.
71+
4172
### View semantics
4273

43-
Both functions return internal cached buffers by default. The next call with the same N overwrites the previous result. Pass an output buffer to keep results across calls:
74+
`rfft`, `fft`, and `irfft` return internal cached buffers by default. The next call with the same N overwrites the previous result. Pass an output buffer to keep results across calls:
4475

4576
```js
4677
const out = new Float64Array(N / 2)
@@ -69,7 +100,7 @@ Raw transform speed is identical to fft.js. The gap is the cost of returning sep
69100

70101
## Acknowledgments
71102

72-
Based on the split-radix real FFT from [dsp.js](https://github.com/corbanbrook/dsp.js) by @corbanbrook, itself derived from [RealFFT](http://www.jjj.de/fxt/).
103+
Forward split-radix real FFT from [dsp.js](https://github.com/corbanbrook/dsp.js) by @corbanbrook, derived from [RealFFT](http://www.jjj.de/fxt/). Inverse split-radix DIF algorithm from [FXT](https://www.jjj.de/fxt/fxtbook.pdf) by Joerg Arndt.
73104

74105
## License
75106

0 commit comments

Comments
 (0)