Skip to content

Commit c2f8ad1

Browse files
committed
Harden
1 parent f77e2a5 commit c2f8ad1

9 files changed

Lines changed: 1158 additions & 120 deletions

File tree

.eslintrc.json

Lines changed: 0 additions & 44 deletions
This file was deleted.

.travis.yml

Lines changed: 0 additions & 13 deletions
This file was deleted.

benchmark/run.js

Lines changed: 102 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,110 @@
1-
import rfft from '../index.js'
1+
import { createRequire } from 'node:module'
2+
import { fft } from '../index.js'
23

3-
const sizes = [256, 1024, 4096, 16384]
4-
const warmup = 1000
5-
const iterations = 50000
4+
const require = createRequire(import.meta.url)
65

7-
for (const N of sizes) {
8-
const input = new Float32Array(N)
9-
for (let i = 0; i < N; i++) input[i] = Math.random() * 2 - 1
6+
const N = 4096
7+
const WARMUP = 500
8+
const ITERATIONS = 20000
109

11-
// warmup (also primes twiddle cache)
12-
for (let i = 0; i < warmup; i++) rfft(input)
10+
// Shared input
11+
const input = new Float32Array(N)
12+
for (let i = 0; i < N; i++) input[i] = Math.random() * 2 - 1
13+
14+
// --- Setup each library outside the timed loop ---
15+
16+
// fourier-transform (this package)
17+
const ftRun = () => fft(input)
18+
19+
// fft.js (indutny, radix-4) — widely considered fastest pure-JS FFT
20+
const FFTJS = require('fft.js')
21+
const fftjs = new FFTJS(N)
22+
const fftjsOut = fftjs.createComplexArray()
23+
const fftjsRun = () => fftjs.realTransform(fftjsOut, input)
24+
25+
// ml-fft (in-place, pre-allocated arrays)
26+
const { FFT: MLFFT } = require('ml-fft')
27+
MLFFT.init(N)
28+
const mlRe = new Array(N), mlIm = new Array(N)
29+
const mlfftRun = () => {
30+
for (let i = 0; i < N; i++) { mlRe[i] = input[i]; mlIm[i] = 0 }
31+
MLFFT.fft(mlRe, mlIm)
32+
}
33+
34+
// dsp.js (our ancestor, the original split-radix)
35+
const dsp = require('dsp.js')
36+
const dspfft = new dsp.FFT(N, 44100)
37+
const dspRun = () => dspfft.forward(input)
38+
39+
// ndarray-fft
40+
const ndfft = require('ndarray-fft')
41+
const ndarray = require('ndarray')
42+
const ndRe = new Float64Array(N), ndIm = new Float64Array(N)
43+
const ndx = ndarray(ndRe), ndy = ndarray(ndIm)
44+
const ndfftRun = () => {
45+
for (let i = 0; i < N; i++) { ndRe[i] = input[i]; ndIm[i] = 0 }
46+
ndfft(1, ndx, ndy)
47+
}
48+
49+
// ooura (port of Ooura's C FFT, radix-4)
50+
const Ooura = require('ooura')
51+
const oo = new Ooura(N, { type: 'real', radix: 4 })
52+
const ooIn = new Float64Array(N)
53+
const ooRe = oo.scalarArrayFactory()
54+
const ooIm = oo.scalarArrayFactory()
55+
const oouraRun = () => {
56+
for (let i = 0; i < N; i++) ooIn[i] = input[i]
57+
oo.fft(ooIn.buffer, ooRe.buffer, ooIm.buffer)
58+
}
59+
60+
// kissfft-wasm (WASM KissFFT)
61+
const { rfft: kissRfft } = await import('kissfft-wasm')
62+
const kissInput = new Float64Array(input)
63+
const kissRun = () => kissRfft(kissInput)
64+
65+
// fft-js (naive Cooley-Tukey, educational)
66+
const fftjs2 = require('fft-js')
67+
const fftjs2Input = Array.from(input)
68+
const fftjs2Run = () => fftjs2.fft(fftjs2Input)
69+
70+
// --- Benchmark runner ---
71+
72+
const libs = [
73+
['fourier-transform', ftRun],
74+
['fft.js (indutny)', fftjsRun],
75+
['ml-fft', mlfftRun],
76+
['dsp.js', dspRun],
77+
['ndarray-fft', ndfftRun],
78+
['ooura', oouraRun],
79+
['kissfft-wasm', kissRun],
80+
['fft-js', fftjs2Run],
81+
]
82+
83+
console.log(`\nFFT benchmark — N=${N}, ${ITERATIONS} iterations\n`)
84+
85+
const results = []
86+
87+
for (const [name, fn] of libs) {
88+
// warmup
89+
for (let i = 0; i < WARMUP; i++) fn()
1390

1491
const start = performance.now()
15-
for (let i = 0; i < iterations; i++) rfft(input)
92+
for (let i = 0; i < ITERATIONS; i++) fn()
1693
const elapsed = performance.now() - start
1794

18-
const us = (elapsed / iterations * 1000).toFixed(1)
19-
console.log(`N=${String(N).padStart(5)} ${us.padStart(6)}µs/call (${iterations} iters, ${elapsed.toFixed(0)}ms)`)
95+
const us = elapsed / ITERATIONS * 1000
96+
results.push({ name, us, elapsed })
97+
}
98+
99+
// Sort by speed
100+
results.sort((a, b) => a.us - b.us)
101+
102+
const fastest = results[0].us
103+
for (const { name, us, elapsed } of results) {
104+
const ratio = us / fastest
105+
const bar = '█'.repeat(Math.min(40, Math.round(ratio * 4)))
106+
console.log(
107+
`${name.padEnd(22)} ${us.toFixed(1).padStart(7)}µs/call ${ratio === 1 ? ' ' : ('×' + ratio.toFixed(1)).padStart(6)} ${bar}`
108+
)
20109
}
110+
console.log()

index.js

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
/**
22
* Real-valued split-radix FFT.
3-
* Returns magnitude spectrum as Float64Array of length N/2.
43
*
54
* @module fourier-transform
6-
* @param {Float32Array|Float64Array|Array} input - Time-domain signal, length must be power of 2 (>=2).
7-
* @param {Float64Array} [output] - Optional pre-allocated output buffer (length N/2). If omitted, returns internal buffer (overwritten on next call with same N).
8-
* @returns {Float64Array} Magnitude spectrum, length N/2.
95
*/
106

117
const { sqrt, sin, cos, abs, SQRT1_2 } = Math
@@ -16,12 +12,16 @@ const cache = new Map()
1612
let lastN = 0, lastEntry = null
1713

1814
function init(N) {
15+
const half = N >>> 1
1916
const x = new Float64Array(N)
20-
const spectrum = new Float64Array(N >>> 1)
17+
const spectrum = new Float64Array(half)
18+
const re = new Float64Array(half + 1)
19+
const im = new Float64Array(half + 1)
20+
const complex = { re, im }
2121
const bSi = 2 / N
2222

2323
// Count twiddle factors per stage
24-
let total = 0, n2 = 2, nn = N >>> 1
24+
let total = 0, n2 = 2, nn = half
2525
const stages = []
2626
while ((nn = nn >>> 1)) {
2727
n2 = n2 << 1
@@ -33,7 +33,7 @@ function init(N) {
3333

3434
// Interleaved twiddle table: [cc1, ss1, cc3, ss3] per entry
3535
const tw = new Float64Array(total << 2)
36-
n2 = 2; nn = N >>> 1
36+
n2 = 2; nn = half
3737
let si = 0
3838
while ((nn = nn >>> 1)) {
3939
n2 = n2 << 1
@@ -52,17 +52,26 @@ function init(N) {
5252
si++
5353
}
5454

55-
const entry = { x, spectrum, bSi, tw, stages }
55+
const entry = { x, spectrum, complex, bSi, tw, stages }
5656
cache.set(N, entry)
5757
return entry
5858
}
5959

60-
export default function rfft(input, output) {
60+
function getEntry(N) {
61+
if (N === lastN) return lastEntry
62+
const entry = cache.get(N) || init(N)
63+
lastN = N
64+
lastEntry = entry
65+
return entry
66+
}
67+
68+
// Shared butterfly computation
69+
function transform(input) {
6170
const N = input.length
6271
if (N < 2 || (N & (N - 1))) throw Error('Input length must be a power of 2 (>= 2).')
6372

64-
const entry = N === lastN ? lastEntry : (lastEntry = cache.get(N) || init(N), lastN = N, lastEntry)
65-
const { x, spectrum, bSi, tw, stages } = entry
73+
const entry = getEntry(N)
74+
const { x, tw, stages } = entry
6675

6776
reverseBinPermute(N, x, input)
6877

@@ -171,8 +180,21 @@ export default function rfft(input, output) {
171180
si++
172181
}
173182

174-
// Magnitude spectrum: Re(X[k]) = x[k], Im(X[k]) = x[N-k]
183+
return entry
184+
}
185+
186+
/**
187+
* Compute magnitude spectrum of real-valued input.
188+
* @param {ArrayLike<number>} input - length must be power of 2 (>= 2).
189+
* @param {Float64Array} [output] - Optional buffer (length N/2). If omitted, returns internal view (overwritten on next call with same N).
190+
* @returns {Float64Array} Magnitude spectrum, length N/2.
191+
*/
192+
export default function rfft(input, output) {
193+
const entry = transform(input)
194+
const N = input.length
195+
const { x, spectrum, bSi } = entry
175196
const out = output || spectrum
197+
176198
let i = N >>> 1
177199
while (--i) {
178200
const rval = x[i], ival = x[N - i]
@@ -183,6 +205,32 @@ export default function rfft(input, output) {
183205
return out
184206
}
185207

208+
/**
209+
* Compute complex spectrum of real-valued input (unnormalized DFT).
210+
* @param {ArrayLike<number>} input - length must be power of 2 (>= 2).
211+
* @param {{re: Float64Array, im: Float64Array}} [output] - Optional buffers (length N/2+1 each). If omitted, returns internal view.
212+
* @returns {{re: Float64Array, im: Float64Array}} Complex spectrum, N/2+1 bins (DC through Nyquist).
213+
*/
214+
export function fft(input, output) {
215+
const entry = transform(input)
216+
const N = input.length
217+
const half = N >>> 1
218+
const { x, complex } = entry
219+
const re = output ? output.re : complex.re
220+
const im = output ? output.im : complex.im
221+
222+
re[0] = x[0]
223+
im[0] = 0
224+
for (let k = 1; k < half; k++) {
225+
re[k] = x[k]
226+
im[k] = x[N - k]
227+
}
228+
re[half] = x[half]
229+
im[half] = 0
230+
231+
return output || complex
232+
}
233+
186234
function reverseBinPermute(N, dest, source) {
187235
const halfSize = N >>> 1
188236
const nm1 = N - 1

0 commit comments

Comments
 (0)