Skip to content

Commit 2fe72f8

Browse files
committed
Change signature
1 parent bb40511 commit 2fe72f8

5 files changed

Lines changed: 36 additions & 29 deletions

File tree

benchmark/run.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ const { rfft: kissRfft } = await import('kissfft-wasm')
6262
const kissInput = new Float64Array(input)
6363
const kissRun = () => kissRfft(kissInput)
6464

65+
// als-fft
66+
const AlsFFT = require('als-fft')
67+
const alsInput = Array.from(input)
68+
const alsRun = () => AlsFFT.fft(alsInput)
69+
6570
// fft-js (naive Cooley-Tukey, educational)
6671
const fftjs2 = require('fft-js')
6772
const fftjs2Input = Array.from(input)
@@ -77,6 +82,7 @@ const libs = [
7782
['ndarray-fft', ndfftRun],
7883
['ooura', oouraRun],
7984
['kissfft-wasm', kissRun],
85+
['als-fft', alsRun],
8086
['fft-js', fftjs2Run],
8187
]
8288

changelog.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
## Added
1313

14-
- `fft()` named export — returns complex DFT as `{ re, im }`, N/2+1 bins, unnormalized.
14+
- `fft()` named export — returns complex DFT as `[re, im]`, N/2+1 bins, unnormalized.
1515
- Optional output buffer parameter for both `rfft()` and `fft()`.
1616

1717
## Performance

index.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function init(N) {
1717
const spectrum = new Float64Array(half)
1818
const im = new Float64Array(half + 1)
1919
const re = x.subarray(0, half + 1) // zero-copy view into x
20-
const complex = { re, im }
20+
const complex = [re, im]
2121
const bSi = 2 / N
2222

2323
// Precompute bit-reversal permutation table
@@ -221,8 +221,8 @@ export default function rfft(input, output) {
221221
/**
222222
* Compute complex spectrum of real-valued input (unnormalized DFT).
223223
* @param {ArrayLike<number>} input - length must be power of 2 (>= 2).
224-
* @param {{re: Float64Array, im: Float64Array}} [output] - Optional buffers (length N/2+1 each). If omitted, returns internal view.
225-
* @returns {{re: Float64Array, im: Float64Array}} Complex spectrum, N/2+1 bins (DC through Nyquist).
224+
* @param {[Float64Array, Float64Array]} [output] - Optional [re, im] buffers (length N/2+1 each). If omitted, returns internal view.
225+
* @returns {[Float64Array, Float64Array]} Complex spectrum [re, im], N/2+1 bins (DC through Nyquist).
226226
*/
227227
export function fft(input, output) {
228228
const entry = transform(input)
@@ -231,15 +231,15 @@ export function fft(input, output) {
231231
const { x, complex } = entry
232232

233233
if (output) {
234-
const re = output.re, im = output.im
234+
const re = output[0], im = output[1]
235235
for (let k = 0; k <= half; k++) re[k] = x[k]
236236
im[0] = 0; im[half] = 0
237237
for (let k = 1; k < half; k++) im[k] = x[N - k]
238238
return output
239239
}
240240

241241
// re is already a zero-copy view of x[0..half] — no copy needed
242-
const im = complex.im
242+
const im = complex[1]
243243
im[0] = 0; im[half] = 0
244244
for (let k = 1; k < half; k++) im[k] = x[N - k]
245245

readme.md

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const spectrum = rfft(waveform)
1515
import { fft } from 'fourier-transform'
1616

1717
// complex DFT output (N/2+1 bins, unnormalized)
18-
const { re, im } = fft(waveform)
18+
const [re, im] = fft(waveform)
1919
```
2020

2121
## API
@@ -32,9 +32,9 @@ Normalization: a unit-amplitude cosine at frequency bin *k* produces `spectrum[k
3232

3333
### `fft(input, output?)` — named export
3434

35-
Returns complex DFT as `{ re, im }`, each `Float64Array` of length N/2+1 (DC through Nyquist).
35+
Returns complex DFT as `[re, im]`, each `Float64Array` of length N/2+1 (DC through Nyquist).
3636

37-
- `output` — optional `{ re: Float64Array(N/2+1), im: Float64Array(N/2+1) }`.
37+
- `output` — optional `[Float64Array(N/2+1), Float64Array(N/2+1)]`.
3838
- Unnormalized: `X[k] = sum( x[n] * e^(-j*2*pi*k*n/N) )`.
3939
- DC and Nyquist bins always have `im = 0` (real input).
4040

@@ -52,17 +52,18 @@ rfft(signal, out) // safe to keep
5252
N=4096 real-valued FFT, complex output, 20k iterations (lower is better):
5353

5454
```
55-
fft.js (indutny) 16.2µs ×1.0 — radix-4, interleaved output
56-
fourier-transform 17.3µs ×1.1 — split-radix, separate re/im
57-
ooura 23.1µs ×1.4 — Ooura C port
58-
ml-fft 36.0µs ×2.2
59-
dsp.js 47.1µs ×2.9 — our split-radix ancestor
60-
kissfft-wasm 49.4µs ×3.1 — WASM KissFFT
61-
ndarray-fft 62.6µs ×3.9
62-
fft-js 2297.4µs ×142 — naive recursive
55+
fft.js (indutny) 16.5µs ×1.0 — radix-4, interleaved output
56+
fourier-transform 17.8µs ×1.1 — split-radix, separate re/im
57+
ooura 23.6µs ×1.4 — Ooura C port
58+
ml-fft 37.0µs ×2.2
59+
dsp.js 48.1µs ×2.9 — our split-radix ancestor
60+
kissfft-wasm 49.4µs ×3.0 — WASM KissFFT
61+
ndarray-fft 63.1µs ×3.8
62+
als-fft 2311.4µs ×140
63+
fft-js 2329.2µs ×141 — naive recursive
6364
```
6465

65-
Raw transform speed is identical to fft.js. The ×1.1 gap is entirely the cost of returning separate `re`/`im` arrays vs interleaved output.
66+
Raw transform speed is identical to fft.js. The gap is the cost of returning separate `re`/`im` arrays vs interleaved output.
6667

6768
`npm run benchmark` to reproduce.
6869

test.js

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -174,14 +174,14 @@ test('internal buffer overwritten on repeated calls (view semantics)', () => {
174174

175175
test('fft: returns N/2+1 complex bins', () => {
176176
const N = 64
177-
const { re, im } = fft(new Float32Array(N))
177+
const [re, im] = fft(new Float32Array(N))
178178
assert.equal(re.length, N / 2 + 1)
179179
assert.equal(im.length, N / 2 + 1)
180180
})
181181

182182
test('fft: DC signal → real-only X[0]=N', () => {
183183
const N = 64
184-
const { re, im } = fft(new Float32Array(N).fill(1))
184+
const [re, im] = fft(new Float32Array(N).fill(1))
185185
assert(Math.abs(re[0] - N) < EPSILON, `DC re: ${re[0]}`)
186186
assert(Math.abs(im[0]) < EPSILON, `DC im: ${im[0]}`)
187187
for (let k = 1; k <= N / 2; k++) {
@@ -195,7 +195,7 @@ test('fft: cosine → real component X[k] = N/2', () => {
195195
for (const k of [1, 5, 32]) {
196196
const input = new Float32Array(N)
197197
for (let n = 0; n < N; n++) input[n] = Math.cos(2 * Math.PI * k * n / N)
198-
const { re, im } = fft(input)
198+
const [re, im] = fft(input)
199199
assert(Math.abs(re[k] - N / 2) < EPSILON, `cos k=${k}: re=${re[k]}, expected ${N / 2}`)
200200
assert(Math.abs(im[k]) < EPSILON, `cos k=${k}: im=${im[k]}, expected 0`)
201201
}
@@ -206,7 +206,7 @@ test('fft: sine → imaginary component X[k] = -jN/2', () => {
206206
for (const k of [1, 5, 63]) {
207207
const input = new Float32Array(N)
208208
for (let n = 0; n < N; n++) input[n] = Math.sin(2 * Math.PI * k * n / N)
209-
const { re, im } = fft(input)
209+
const [re, im] = fft(input)
210210
assert(Math.abs(re[k]) < EPSILON, `sin k=${k}: re=${re[k]}, expected 0`)
211211
assert(Math.abs(im[k] - (-N / 2)) < EPSILON, `sin k=${k}: im=${im[k]}, expected ${-N / 2}`)
212212
}
@@ -216,7 +216,7 @@ test('fft: DC and Nyquist always have zero imaginary', () => {
216216
for (const N of [4, 64, 512]) {
217217
const input = new Float32Array(N)
218218
for (let i = 0; i < N; i++) input[i] = Math.sin(i * 1.7) + Math.cos(i * 0.3)
219-
const { im } = fft(input)
219+
const [, im] = fft(input)
220220
assert.equal(im[0], 0, `N=${N}: DC im must be 0`)
221221
assert.equal(im[N / 2], 0, `N=${N}: Nyquist im must be 0`)
222222
}
@@ -228,7 +228,7 @@ test('fft: magnitude matches rfft', () => {
228228
for (let i = 0; i < N; i++) input[i] = Math.sin(i * 0.7) + Math.cos(i * 1.3)
229229

230230
const mag = new Float64Array(rfft(input))
231-
const { re, im } = fft(input)
231+
const [re, im] = fft(input)
232232

233233
const bSi = 2 / N
234234
assert(Math.abs(mag[0] - Math.abs(bSi * re[0])) < EPSILON, `DC mismatch`)
@@ -240,10 +240,10 @@ test('fft: magnitude matches rfft', () => {
240240

241241
test('fft: output buffer parameter', () => {
242242
const N = 64
243-
const out = { re: new Float64Array(N / 2 + 1), im: new Float64Array(N / 2 + 1) }
243+
const out = [new Float64Array(N / 2 + 1), new Float64Array(N / 2 + 1)]
244244
const ret = fft(new Float32Array(N).fill(1), out)
245245
assert.equal(ret, out)
246-
assert(Math.abs(out.re[0] - N) < EPSILON)
246+
assert(Math.abs(out[0][0] - N) < EPSILON)
247247
})
248248

249249
test('fft: view overwritten on repeated calls', () => {
@@ -254,10 +254,10 @@ test('fft: view overwritten on repeated calls', () => {
254254
b[i] = Math.sin(2 * Math.PI * 10 * i / N)
255255
}
256256
const ra = fft(a)
257-
assert(Math.abs(ra.re[5] - N / 2) < EPSILON)
257+
assert(Math.abs(ra[0][5] - N / 2) < EPSILON)
258258

259259
fft(b) // overwrites ra since same N
260260

261-
assert(Math.abs(ra.re[5]) < EPSILON, 'ra.re[5] should be overwritten')
262-
assert(Math.abs(ra.im[10] - (-N / 2)) < EPSILON, 'ra now reflects b')
261+
assert(Math.abs(ra[0][5]) < EPSILON, 'ra[0][5] should be overwritten')
262+
assert(Math.abs(ra[1][10] - (-N / 2)) < EPSILON, 'ra now reflects b')
263263
})

0 commit comments

Comments
 (0)