Skip to content

Commit 833df5e

Browse files
committed
Add stft
1 parent 1b01975 commit 833df5e

9 files changed

Lines changed: 1700 additions & 476 deletions

File tree

changelog.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,23 @@
1+
# 2.3.0
2+
3+
## Added
4+
5+
- `fourier-transform/stft` subpath export — STFT and inverse STFT.
6+
- `stft(signal, opts)` — analysis-only STFT returning frames with `re`, `im`, `mag`, `phase`, `time`.
7+
- `istft(frames, opts)` — synthesis from STFT frames.
8+
- `stftBatch(data, process, opts)` — batch STFT with per-frame processing callback and overlap-add synthesis.
9+
- `stftStream(process, opts)` — streaming STFT with processing callback.
10+
- `stftAnalysisStream(opts)` — streaming analysis-only STFT.
11+
- Zero-padded boundaries for perfect reconstruction.
12+
- Hann window with per-size cache.
13+
- Compatible with `pitch-shift`, `time-stretch`, and `pitch-detection` patterns.
14+
15+
# 2.2.0
16+
17+
## Added
18+
19+
- `cifft(re, im)` — in-place complex inverse FFT (1/N normalized).
20+
121
# 2.1.0
222

323
## Breaking

package-lock.json

Lines changed: 11 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,21 @@
44
"description": "Minimalistic and efficient FFT implementation",
55
"type": "module",
66
"sideEffects": false,
7-
"exports": "./index.js",
7+
"exports": {
8+
".": "./index.js",
9+
"./stft": "./stft.js"
10+
},
811
"scripts": {
9-
"test": "node --test test.js",
12+
"test": "node --test test/rfft.js test/fft.js test/stft.js",
1013
"benchmark": "node benchmark/run.js"
1114
},
1215
"repository": {
1316
"type": "git",
1417
"url": "git+https://github.com/scijs/fourier-transform.git"
1518
},
1619
"files": [
17-
"index.js"
20+
"index.js",
21+
"stft.js"
1822
],
1923
"keywords": [
2024
"fourier",
@@ -23,7 +27,10 @@
2327
"rfft",
2428
"ifft",
2529
"cfft",
26-
"dft"
30+
"dft",
31+
"stft",
32+
"istft",
33+
"short-time-fourier-transform"
2734
],
2835
"author": "Dmitry Iv. <dfcreative@gmail.com>",
2936
"license": "MIT",
@@ -40,6 +47,7 @@
4047
"ml-fft": "^1.3.5",
4148
"ndarray": "^1.0.19",
4249
"ndarray-fft": "^1.0.3",
43-
"ooura": "^2.1.6"
50+
"ooura": "^2.1.6",
51+
"tst": "^9.4.0"
4452
}
4553
}

readme.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,94 @@ In-place complex forward FFT (unnormalized). Both `re` and `im` must be `Float64
6969

7070
In-place complex inverse FFT (1/N normalized). Same signature as `cfft`.
7171

72+
## STFT
73+
74+
```js
75+
import { stft, istft, stftBatch, stftStream, stftAnalysisStream } from 'fourier-transform/stft'
76+
```
77+
78+
### `stft(signal, opts?)` — analysis
79+
80+
Returns an array of frames, each with `{ re, im, mag, phase, time }`.
81+
82+
- `signal``Float32Array`, `Float64Array`, or plain `Array`.
83+
- `opts.frameSize` — FFT size, power of 2. Default: `2048`.
84+
- `opts.hopSize` — hop between frames. Default: `frameSize / 4`.
85+
- `time` is the sample index of the frame centre in the original signal.
86+
- Zero-padded by `frameSize` at front and back so edge samples are fully windowed.
87+
88+
```js
89+
const frames = stft(waveform, { frameSize: 2048, hopSize: 512 })
90+
for (const f of frames) {
91+
console.log(f.time, f.mag[100]) // time in samples, magnitude at bin 100
92+
}
93+
```
94+
95+
### `istft(frames, opts?)` — synthesis
96+
97+
Reconstructs a time-domain signal from STFT frames.
98+
99+
- `frames` — array of `{ mag, phase, time? }` or `{ re, im, time? }` objects.
100+
- `opts.signalLength` — expected output length. Inferred from last frame if omitted.
101+
- When inferred, the tail may include padding; pass `signalLength` for exact control.
102+
- Returns `Float64Array`.
103+
- If `re`/`im` are present, they are used directly (no polar round-trip). Otherwise `mag`/`phase` are converted to cartesian.
104+
105+
```js
106+
const recovered = istft(frames, { frameSize: 2048, hopSize: 512, signalLength: waveform.length })
107+
```
108+
109+
### `stftBatch(data, process, opts?)` — batch with callback
110+
111+
Processes each frame through a callback and overlap-adds the result.
112+
113+
- `process(mag, phase, state, ctx)``{ mag, phase }`
114+
- `mag`, `phase``Float64Array(half + 1)`
115+
- `state` — persistent object across frames
116+
- `ctx``{ N, half, hop, anaHop, synHop, freqPerBin, frameStart, sampleRate, opts }`
117+
- `ctx.frameStart` — sample index of the frame start in the original signal. Negative at boundaries due to zero-padding.
118+
- `ctx.opts` — cloned copy of `opts`. Use this to pass custom parameters (e.g. `ratio`, `ratioFn`) through to your process callback.
119+
- `opts.anaHop` — analysis hop (default: `hopSize`).
120+
- `opts.synHop` — synthesis hop (default: `hopSize`). When `anaHop !== synHop`, the output is time-stretched or compressed.
121+
- Returns `Float32Array` of length `round(data.length * synHop / anaHop)` (same as input when `anaHop === synHop`).
122+
123+
```js
124+
const result = stftBatch(signal, (mag, phase, state, ctx) => {
125+
// Simple spectral gate
126+
for (let k = 0; k < mag.length; k++) if (mag[k] < 0.1) mag[k] = 0
127+
return { mag, phase }
128+
}, { frameSize: 2048, hopSize: 512 })
129+
```
130+
131+
### `stftStream(process, opts?)` — streaming with callback
132+
133+
Streaming version of `stftBatch`. Returns `{ write(chunk), flush() }`.
134+
135+
- Supports `opts.anaHop` / `opts.synHop` for time-stretching in streaming context.
136+
137+
```js
138+
const stream = stftStream((mag, phase) => ({ mag, phase }), { frameSize: 2048 })
139+
140+
for (const chunk of audioChunks) {
141+
const processed = stream.write(chunk)
142+
// emit processed...
143+
}
144+
const tail = stream.flush()
145+
```
146+
147+
### `stftAnalysisStream(opts?)` — streaming analysis
148+
149+
Streaming version of `stft`. Returns `{ write(chunk), flush() }` that emit frames.
150+
151+
- Supports `opts.anaHop` for non-uniform analysis spacing.
152+
153+
```js
154+
const stream = stftAnalysisStream({ frameSize: 2048, hopSize: 512 })
155+
const frames = stream.write(chunk1)
156+
frames.push(...stream.write(chunk2))
157+
frames.push(...stream.flush())
158+
```
159+
72160
### View semantics
73161

74162
`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:

0 commit comments

Comments
 (0)