FFT for real and complex signals. Split-radix real FFT + radix-2 complex FFT. Precomputed twiddle factors, typed-array buffers, zero dependencies.
import rfft from 'fourier-transform'
// magnitude spectrum (N/2 bins)
const spectrum = rfft(waveform)import { fft } from 'fourier-transform'
// complex DFT of real input (N/2+1 bins, unnormalized)
const [re, im] = fft(waveform)import { cfft, cifft } from 'fourier-transform'
// in-place complex FFT / inverse FFT
const re = new Float64Array(N), im = new Float64Array(N)
cfft(re, im) // forward
cifft(re, im) // inverse (1/N normalized)Returns magnitude spectrum as Float64Array of length N/2.
input—Float32Array,Float64Array, or plainArray. Length must be power of 2 (>= 2).output— optionalFloat64Array(N/2)to write into.- Returns internal buffer if no output provided (overwritten on next call with same N).
Normalization: a unit-amplitude cosine at frequency bin k produces spectrum[k] = 1.0.
Returns complex DFT as [re, im], each Float64Array of length N/2+1 (DC through Nyquist).
output— optional[Float64Array(N/2+1), Float64Array(N/2+1)].- Unnormalized:
X[k] = sum( x[n] * e^(-j*2*pi*k*n/N) ). - DC and Nyquist bins always have
im = 0(real input).
Inverse of fft() — recovers time-domain signal from complex spectrum. Returns Float64Array of length N.
re,im—Float64Arrayof length N/2+1 (as returned byfft()).im[0]andim[N/2]are ignored (half-complex format has no slot for them).- Native split-radix DIF inverse — no complex FFT overhead.
const [re, im] = fft(signal)
// modify spectrum...
const recovered = ifft(re, im)In-place complex forward FFT (unnormalized). Both re and im must be Float64Array of equal power-of-2 length (>= 2). Modifies arrays in place.
In-place complex inverse FFT (1/N normalized). Same signature as cfft.
import { stft, istft, stftBatch, stftStream, stftAnalysisStream } from 'fourier-transform/stft'Returns an array of frames, each with { re, im, mag, phase, time }.
signal—Float32Array,Float64Array, or plainArray.opts.frameSize— FFT size, power of 2. Default:2048.opts.hopSize— hop between frames. Default:frameSize / 4.timeis the sample index of the frame centre in the original signal.- Zero-padded by
frameSizeat front and back so edge samples are fully windowed.
const frames = stft(waveform, { frameSize: 2048, hopSize: 512 })
for (const f of frames) {
console.log(f.time, f.mag[100]) // time in samples, magnitude at bin 100
}Reconstructs a time-domain signal from STFT frames.
frames— array of{ mag, phase, time? }or{ re, im, time? }objects.opts.signalLength— expected output length. Inferred from last frame if omitted.- When inferred, the tail may include padding; pass
signalLengthfor exact control.
- When inferred, the tail may include padding; pass
- Returns
Float64Array. - If
re/imare present, they are used directly (no polar round-trip). Otherwisemag/phaseare converted to cartesian.
const recovered = istft(frames, { frameSize: 2048, hopSize: 512, signalLength: waveform.length })Processes each frame through a callback and overlap-adds the result.
process(mag, phase, state, ctx)→{ mag, phase }mag,phase—Float64Array(half + 1)state— persistent object across framesctx—{ N, half, hop, anaHop, synHop, freqPerBin, frameStart, sampleRate, opts }ctx.frameStart— sample index of the frame start in the original signal. Negative at boundaries due to zero-padding.ctx.opts— cloned copy ofopts. Use this to pass custom parameters (e.g.ratio,ratioFn) through to your process callback.
opts.anaHop— analysis hop (default:hopSize).opts.synHop— synthesis hop (default:hopSize). WhenanaHop !== synHop, the output is time-stretched or compressed.- Returns
Float32Arrayof lengthround(data.length * synHop / anaHop)(same as input whenanaHop === synHop).
const result = stftBatch(signal, (mag, phase, state, ctx) => {
// Simple spectral gate
for (let k = 0; k < mag.length; k++) if (mag[k] < 0.1) mag[k] = 0
return { mag, phase }
}, { frameSize: 2048, hopSize: 512 })Streaming version of stftBatch. Returns { write(chunk), flush() }.
- Supports
opts.anaHop/opts.synHopfor time-stretching in streaming context.
const stream = stftStream((mag, phase) => ({ mag, phase }), { frameSize: 2048 })
for (const chunk of audioChunks) {
const processed = stream.write(chunk)
// emit processed...
}
const tail = stream.flush()Streaming version of stft. Returns { write(chunk), flush() } that emit frames.
- Supports
opts.anaHopfor non-uniform analysis spacing.
const stream = stftAnalysisStream({ frameSize: 2048, hopSize: 512 })
const frames = stream.write(chunk1)
frames.push(...stream.write(chunk2))
frames.push(...stream.flush())rfft, fft, and ifft 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:
const out = new Float64Array(N / 2)
rfft(signal, out) // safe to keepN=4096 real-valued FFT, complex output, 20k iterations (lower is better):
fft.js (indutny) 16.5µs ×1.0 — radix-4, interleaved output
fourier-transform 17.8µs ×1.1 — split-radix, separate re/im
ooura 23.6µs ×1.4 — Ooura C port
ml-fft 37.0µs ×2.2
dsp.js 48.1µs ×2.9 — our split-radix ancestor
kissfft-wasm 49.4µs ×3.0 — WASM KissFFT
ndarray-fft 63.1µs ×3.8
als-fft 2311.4µs ×140
fft-js 2329.2µs ×141 — naive recursive
Raw transform speed is identical to fft.js. The gap is the cost of returning separate re/im arrays vs interleaved output.
npm run benchmark to reproduce.
Forward split-radix real FFT from dsp.js by @corbanbrook, derived from RealFFT. Inverse split-radix DIF algorithm from FXT by Joerg Arndt.
MIT