Xenova HF Staff commited on
Commit
a0c8d5e
·
verified ·
1 Parent(s): 76bc6f6

sync 2e7068faf55e

Browse files
README.md CHANGED
@@ -1,3 +1,86 @@
1
  ---
 
2
  license: apache-2.0
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ library_name: kernels
3
  license: apache-2.0
4
+ tags:
5
+ - kernel
6
+ - webgpu
7
+ - wgsl
8
  ---
9
+ # ai.onnx.STFT
10
+
11
+ `ai.onnx` · standard ONNX operator · ONNX opset ≥ 17
12
+
13
+ ## Description
14
+
15
+ Computes the short-time Fourier transform (STFT) of a rank-3 real or complex signal by sliding a complete frame over the input and applying the DFT. The `frameStep` scalar represents the required positive `frame_step` input. At least one of `window` or `frameLength` is required; when both are supplied, their lengths must match. The output contains the real and imaginary components of every frequency bin for each complete frame.
16
+
17
+ See the [ONNX `STFT` spec](https://onnx.ai/onnx/operators/onnx__STFT.html) for the reference semantics.
18
+
19
+ ## Inputs
20
+
21
+ | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
22
+ | --- | --- | --- | --- | --- | --- | --- |
23
+ | `signal` | `signal` | `T` | `3` | — | Real input signal with shape `(batch_size, signal_length, 1)`, or complex input with shape `(batch_size, signal_length, 2)`. | required |
24
+ | `window` | `window` | `T` | `1` | — | Optional 1-D window tensor with shape `(window_length)` to multiply by each frame before the DFT. | optional |
25
+
26
+ ## Outputs
27
+
28
+ | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
29
+ | --- | --- | --- | --- | --- | --- | --- |
30
+ | `output` | `output` | `T` | `4` | derived; see description | STFT result with shape `(batch_size, frames, dft_unique_bins, 2)` in one-sided mode or `(batch_size, frames, frame_length, 2)` in two-sided mode, with real and imaginary parts in the last dimension. `frames = floor((signal_length - frame_length) / frame_step) + 1`; partial trailing frames are not emitted. | required |
31
+
32
+ ## Runtime arguments
33
+
34
+ | Name | Kind | Semantic | Description | Presence |
35
+ | --- | --- | --- | --- | --- |
36
+ | `frameStep` | `i32` | `frame_step` | Positive number of signal positions between the starts of consecutive frames. | required |
37
+ | `frameLength` | `i32` | `frame_length` | Optional number of signal values in each frame; must equal the window length when both are supplied. | optional |
38
+
39
+ ## Attributes
40
+
41
+ Default values (overridable per request):
42
+
43
+ | Attribute | Default | Description |
44
+ | --- | --- | --- |
45
+ | `onesided` | `1` | When 1 (default), returns `floor(n_fft / 2) + 1` non-redundant frequency bins with indices `[0, ..., floor(n_fft / 2)]` for a real-valued input (RFFT); set to 0 to return the full spectrum. |
46
+
47
+ ## Type constraints
48
+
49
+ | Variable | Allowed dtypes |
50
+ | --- | --- |
51
+ | `T` | `float32` |
52
+
53
+ ## Files
54
+
55
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, provenance)
56
+ - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
57
+ - [`test.json`](build/webgpu/test.json) — correctness cases
58
+ - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
59
+ - [`dft-tiled-real.wgsl.jinja`](build/webgpu/dft-tiled-real.wgsl.jinja)
60
+ - [`fft-radix2-dit-shared-inplace.wgsl.jinja`](build/webgpu/fft-radix2-dit-shared-inplace.wgsl.jinja)
61
+ - [`fft-radix2-dit-storage-pass.wgsl.jinja`](build/webgpu/fft-radix2-dit-storage-pass.wgsl.jinja)
62
+ - [`fft-radix2-dit-storage.wgsl.jinja`](build/webgpu/fft-radix2-dit-storage.wgsl.jinja)
63
+ - [`fft-real-packed-stockham-shared.wgsl.jinja`](build/webgpu/fft-real-packed-stockham-shared.wgsl.jinja)
64
+ - [`fft-stockham-shared.wgsl.jinja`](build/webgpu/fft-stockham-shared.wgsl.jinja)
65
+ - [`fft-stockham-twiddles.wgsl.jinja`](build/webgpu/fft-stockham-twiddles.wgsl.jinja)
66
+ - [`stft-general.wgsl.jinja`](build/webgpu/stft-general.wgsl.jinja)
67
+
68
+ ## Use with `@huggingface/kernels`
69
+
70
+ The loader derives every required output's shape and logical dtype from the manifest contract and this call.
71
+ It then allocates the result tensors automatically.
72
+
73
+ The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
74
+
75
+ Replace each `*Data` placeholder with a typed array containing the corresponding input data.
76
+
77
+ ```js
78
+ import { getKernel } from "@huggingface/kernels";
79
+
80
+ const kernel = await getKernel("webgpu-kernels/ai.onnx.STFT", { version: 1 });
81
+ const { output } = await kernel({
82
+ signal: { data: signalData, shape: [1, 4, 1] },
83
+ window: { data: windowData, shape: [4] },
84
+ frameStep: 4,
85
+ });
86
+ ```
build/webgpu/bench.json ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "op": "ai.onnx.STFT",
3
+ "tunableSpace": { "WORKGROUP_SIZE": [64, 128, 256] },
4
+ "cases": [
5
+ {
6
+ "name": "batch4_signal1024_frame256_step128",
7
+ "inputs": {
8
+ "signal": { "dtype": "float32", "shape": [4, 1024, 1] },
9
+ "window": { "dtype": "float32", "shape": [256] }
10
+ },
11
+ "outputs": { "output": { "dtype": "float32", "shape": [4, 7, 129, 2] } },
12
+ "args": { "frameStep": 128 }
13
+ },
14
+ {
15
+ "name": "fft_frame256_signal2048_step128_batch4",
16
+ "preset": "smoke",
17
+ "vars": { "frames": 60, "n": 256, "log2n": 8 },
18
+ "inputs": {
19
+ "signal": { "dtype": "float32", "shape": [4, 2048, 1], "dist": "normal", "seed": 911 },
20
+ "window": { "dtype": "float32", "shape": [256], "dist": "uniform", "seed": 912 }
21
+ },
22
+ "outputs": { "output": { "dtype": "float32", "shape": [4, 15, 129, 2] } },
23
+ "bench": { "metrics": [{ "type": "gflops", "value": "5 * args.frames * args.n * args.log2n" }] },
24
+ "args": { "frameStep": 128 }
25
+ },
26
+ {
27
+ "name": "fft_frame512_signal4096_step256_batch2",
28
+ "preset": "smoke",
29
+ "vars": { "frames": 30, "n": 512, "log2n": 9 },
30
+ "inputs": {
31
+ "signal": { "dtype": "float32", "shape": [2, 4096, 1], "dist": "normal", "seed": 913 },
32
+ "window": { "dtype": "float32", "shape": [512], "dist": "uniform", "seed": 914 }
33
+ },
34
+ "outputs": { "output": { "dtype": "float32", "shape": [2, 15, 257, 2] } },
35
+ "bench": { "metrics": [{ "type": "gflops", "value": "5 * args.frames * args.n * args.log2n" }] },
36
+ "args": { "frameStep": 256 }
37
+ },
38
+ {
39
+ "name": "fft_frame1024_signal131072_step256_audio",
40
+ "preset": "smoke",
41
+ "vars": { "frames": 509, "n": 1024, "log2n": 10 },
42
+ "inputs": {
43
+ "signal": { "dtype": "float32", "shape": [1, 131072, 1], "dist": "normal", "seed": 915 },
44
+ "window": { "dtype": "float32", "shape": [1024], "dist": "uniform", "seed": 916 }
45
+ },
46
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 509, 513, 2] } },
47
+ "bench": { "primary": true, "metrics": [{ "type": "gflops", "value": "5 * args.frames * args.n * args.log2n" }] },
48
+ "args": { "frameStep": 256 }
49
+ },
50
+ {
51
+ "name": "pow2_gate_FFT_win512_signal8192_step160",
52
+ "preset": "smoke",
53
+ "vars": { "batch": 2, "frames": 49, "bins": 257, "n": 512, "log2n": 9 },
54
+ "inputs": {
55
+ "signal": { "dtype": "float32", "shape": [2, 8192, 1], "dist": "normal", "seed": 2001 },
56
+ "window": { "dtype": "float32", "shape": [512], "dist": "uniform", "seed": 2002 }
57
+ },
58
+ "outputs": { "output": { "dtype": "float32", "shape": [2, 49, 257, 2] } },
59
+ "bench": { "metrics": [{ "type": "gflops", "value": "5 * args.batch * args.frames * args.n * args.log2n" }] },
60
+ "args": { "frameStep": 160 }
61
+ },
62
+ {
63
+ "name": "mixed_radix_win480_signal8192_step160_tail15",
64
+ "preset": "smoke",
65
+ "vars": { "batch": 2, "frames": 49, "bins": 241, "n": 480, "log2radix2": 5, "tail": 15 },
66
+ "inputs": {
67
+ "signal": { "dtype": "float32", "shape": [2, 8192, 1], "dist": "normal", "seed": 2001 },
68
+ "window": { "dtype": "float32", "shape": [480], "dist": "uniform", "seed": 2003 }
69
+ },
70
+ "outputs": { "output": { "dtype": "float32", "shape": [2, 49, 241, 2] } },
71
+ "bench": {
72
+ "metrics": [
73
+ {
74
+ "type": "gflops",
75
+ "value": "args.batch * args.frames * (5 * args.n * args.log2radix2 + 8 * args.bins * args.tail)"
76
+ }
77
+ ]
78
+ },
79
+ "args": { "frameStep": 160 }
80
+ },
81
+ {
82
+ "name": "mixed_radix_win400_signal16000_step160_batch4_tail25",
83
+ "preset": "smoke",
84
+ "vars": { "batch": 4, "frames": 98, "bins": 201, "n": 400, "log2radix2": 4, "tail": 25 },
85
+ "inputs": {
86
+ "signal": { "dtype": "float32", "shape": [4, 16000, 1], "dist": "normal", "seed": 2010 },
87
+ "window": { "dtype": "float32", "shape": [400], "dist": "uniform", "seed": 2011 }
88
+ },
89
+ "outputs": { "output": { "dtype": "float32", "shape": [4, 98, 201, 2] } },
90
+ "bench": {
91
+ "metrics": [
92
+ {
93
+ "type": "gflops",
94
+ "value": "args.batch * args.frames * (5 * args.n * args.log2radix2 + 8 * args.bins * args.tail)"
95
+ }
96
+ ]
97
+ },
98
+ "args": { "frameStep": 160 }
99
+ },
100
+ {
101
+ "name": "radix2_win512_signal16000_step160_batch4",
102
+ "preset": "smoke",
103
+ "vars": { "batch": 4, "frames": 97, "bins": 257, "n": 512, "log2n": 9 },
104
+ "inputs": {
105
+ "signal": { "dtype": "float32", "shape": [4, 16000, 1], "dist": "normal", "seed": 2010 },
106
+ "window": { "dtype": "float32", "shape": [512], "dist": "uniform", "seed": 2012 }
107
+ },
108
+ "outputs": { "output": { "dtype": "float32", "shape": [4, 97, 257, 2] } },
109
+ "bench": { "metrics": [{ "type": "gflops", "value": "5 * args.batch * args.frames * args.n * args.log2n" }] },
110
+ "args": { "frameStep": 160 }
111
+ },
112
+ {
113
+ "name": "dispatch_cliff_NAIVE_batch48_frames1397_win512_step160",
114
+ "preset": "stress",
115
+ "vars": { "batch": 48, "frames": 1397, "bins": 257, "n": 512, "log2n": 9, "signalLength": 224000 },
116
+ "inputs": {
117
+ "signal": { "dtype": "float32", "shape": [48, 224000, 1], "dist": "normal", "seed": 2020 },
118
+ "window": { "dtype": "float32", "shape": [512], "dist": "uniform", "seed": 2021 }
119
+ },
120
+ "outputs": { "output": { "dtype": "float32", "shape": [48, 1397, 257, 2] } },
121
+ "bench": {
122
+ "metrics": [
123
+ { "type": "gflops", "value": "5 * args.batch * args.frames * args.n * args.log2n" },
124
+ {
125
+ "type": "bandwidth",
126
+ "value": "4 * (args.batch * args.signalLength + args.n + args.batch * args.frames * args.bins * 2)"
127
+ }
128
+ ]
129
+ },
130
+ "args": { "frameStep": 160 }
131
+ },
132
+ {
133
+ "name": "dispatch_cliff_sibling_FFT_batch32_frames1397_win512_step160",
134
+ "preset": "stress",
135
+ "vars": { "batch": 32, "frames": 1397, "bins": 257, "n": 512, "log2n": 9, "signalLength": 224000 },
136
+ "inputs": {
137
+ "signal": { "dtype": "float32", "shape": [32, 224000, 1], "dist": "normal", "seed": 2020 },
138
+ "window": { "dtype": "float32", "shape": [512], "dist": "uniform", "seed": 2022 }
139
+ },
140
+ "outputs": { "output": { "dtype": "float32", "shape": [32, 1397, 257, 2] } },
141
+ "bench": {
142
+ "metrics": [
143
+ { "type": "gflops", "value": "5 * args.batch * args.frames * args.n * args.log2n" },
144
+ {
145
+ "type": "bandwidth",
146
+ "value": "4 * (args.batch * args.signalLength + args.n + args.batch * args.frames * args.bins * 2)"
147
+ }
148
+ ]
149
+ },
150
+ "args": { "frameStep": 160 }
151
+ },
152
+ {
153
+ "name": "large_window_FFT_win4096_signal131072_step1024",
154
+ "preset": "smoke",
155
+ "vars": { "batch": 1, "frames": 125, "bins": 2049, "n": 4096, "log2n": 12 },
156
+ "inputs": {
157
+ "signal": { "dtype": "float32", "shape": [1, 131072, 1], "dist": "normal", "seed": 2030 },
158
+ "window": { "dtype": "float32", "shape": [4096], "dist": "uniform", "seed": 2031 }
159
+ },
160
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 125, 2049, 2] } },
161
+ "bench": { "metrics": [{ "type": "gflops", "value": "5 * args.batch * args.frames * args.n * args.log2n" }] },
162
+ "args": { "frameStep": 1024 }
163
+ },
164
+ {
165
+ "name": "large_window_FFT_win8192_signal131072_step2048",
166
+ "preset": "stress",
167
+ "vars": { "batch": 1, "frames": 61, "bins": 4097, "n": 8192, "log2n": 13 },
168
+ "inputs": {
169
+ "signal": { "dtype": "float32", "shape": [1, 131072, 1], "dist": "normal", "seed": 2030 },
170
+ "window": { "dtype": "float32", "shape": [8192], "dist": "uniform", "seed": 2033 }
171
+ },
172
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 61, 4097, 2] } },
173
+ "bench": { "metrics": [{ "type": "gflops", "value": "5 * args.batch * args.frames * args.n * args.log2n" }] },
174
+ "args": { "frameStep": 2048 }
175
+ },
176
+ {
177
+ "name": "large_window_sibling_FFT_win2048_signal131072_step1024",
178
+ "preset": "smoke",
179
+ "vars": { "batch": 1, "frames": 127, "bins": 1025, "n": 2048, "log2n": 11 },
180
+ "inputs": {
181
+ "signal": { "dtype": "float32", "shape": [1, 131072, 1], "dist": "normal", "seed": 2030 },
182
+ "window": { "dtype": "float32", "shape": [2048], "dist": "uniform", "seed": 2032 }
183
+ },
184
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 127, 1025, 2] } },
185
+ "bench": { "metrics": [{ "type": "gflops", "value": "5 * args.batch * args.frames * args.n * args.log2n" }] },
186
+ "args": { "frameStep": 1024 }
187
+ },
188
+ {
189
+ "name": "mixed_radix_win1000_tail125_batch2_step250_twosided",
190
+ "preset": "stress",
191
+ "vars": { "batch": 2, "frames": 13, "bins": 1000, "n": 1000 },
192
+ "attrs": { "onesided": 0 },
193
+ "inputs": {
194
+ "signal": { "dtype": "float32", "shape": [2, 4000, 1], "dist": "normal", "seed": 3120 },
195
+ "window": { "dtype": "float32", "shape": [1000], "dist": "uniform", "seed": 3121 }
196
+ },
197
+ "outputs": { "output": { "dtype": "float32", "shape": [2, 13, 1000, 2], "dist": "empty" } },
198
+ "bench": { "metrics": [{ "type": "gflops", "value": "5 * args.batch * args.frames * args.bins * args.n" }] },
199
+ "args": { "frameStep": 250 }
200
+ },
201
+ {
202
+ "name": "tiled_win2047_nonpow2_boundary_batch2_step256",
203
+ "preset": "stress",
204
+ "vars": { "batch": 2, "frames": 30, "bins": 1024, "n": 2047 },
205
+ "inputs": {
206
+ "signal": { "dtype": "float32", "shape": [2, 9471, 1], "dist": "normal", "seed": 3101 },
207
+ "window": { "dtype": "float32", "shape": [2047], "dist": "uniform", "seed": 3102 }
208
+ },
209
+ "outputs": { "output": { "dtype": "float32", "shape": [2, 30, 1024, 2], "dist": "empty" } },
210
+ "bench": { "metrics": [{ "type": "gflops", "value": "5 * args.batch * args.frames * args.bins * args.n" }] },
211
+ "args": { "frameStep": 256 }
212
+ },
213
+ {
214
+ "name": "mixed_radix_win1536_batch4_step384",
215
+ "preset": "stress",
216
+ "vars": { "batch": 4, "frames": 13, "bins": 769, "n": 1536 },
217
+ "inputs": {
218
+ "signal": { "dtype": "float32", "shape": [4, 6144, 1], "dist": "normal", "seed": 3110 },
219
+ "window": { "dtype": "float32", "shape": [1536], "dist": "uniform", "seed": 3111 }
220
+ },
221
+ "outputs": { "output": { "dtype": "float32", "shape": [4, 13, 769, 2], "dist": "empty" } },
222
+ "bench": { "metrics": [{ "type": "gflops", "value": "5 * args.batch * args.frames * args.bins * args.n" }] },
223
+ "args": { "frameStep": 384 }
224
+ },
225
+ {
226
+ "name": "tiled_win513_nonpow2_boundary_batch2_signal8193_step160",
227
+ "preset": "stress",
228
+ "vars": { "batch": 2, "frames": 49, "bins": 257, "n": 513 },
229
+ "attrs": { "onesided": 1 },
230
+ "inputs": {
231
+ "signal": { "dtype": "float32", "shape": [2, 8193, 1], "dist": "normal", "seed": 3120 },
232
+ "window": { "dtype": "float32", "shape": [513], "dist": "uniform", "seed": 3121 }
233
+ },
234
+ "outputs": { "output": { "dtype": "float32", "shape": [2, 49, 257, 2], "dist": "empty" } },
235
+ "bench": { "metrics": [{ "type": "gflops", "value": "5 * args.batch * args.frames * args.bins * args.n" }] },
236
+ "args": { "frameStep": 160 }
237
+ },
238
+ {
239
+ "name": "audio-no-window-frame512-batch8-signal160000-pathology",
240
+ "preset": "stress",
241
+ "provenance": {
242
+ "notes": "Large no-window transform that exercises rank3_no_window; the windowed FFT control isolates the optional-window route gap."
243
+ },
244
+ "vars": { "batch": 8, "frames": 997, "bins": 257, "n": 512, "log2n": 9 },
245
+ "attrs": { "onesided": 1 },
246
+ "inputs": { "signal": { "dtype": "float32", "shape": [8, 160000, 1], "dist": "normal", "seed": 3130 } },
247
+ "outputs": { "output": { "dtype": "float32", "shape": [8, 997, 257, 2], "dist": "empty" } },
248
+ "bench": { "metrics": [{ "type": "gflops", "value": "5 * args.batch * args.frames * args.n * args.log2n" }] },
249
+ "args": { "frameStep": 160, "frameLength": 512 }
250
+ }
251
+ ]
252
+ }
build/webgpu/dft-tiled-real.wgsl.jinja ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Portable direct transform for large real signals. A 256-lane workgroup owns
2
+ // 256 adjacent frequency bins and stages one input row or windowed STFT frame
3
+ // once, avoiding a global reload per (frequency, sample) pair. Twiddles advance
4
+ // by complex multiplication and reset from sin/cos every 64 samples to bound
5
+ // phase drift without transcendental functions in the inner loop.
6
+ {{ env.wgsl.resourceDeclarations }}
7
+ // Backend-stable unit-circle twiddles for DFT kernels. Retain the phase as a
8
+ // rational turn and route all polynomial evaluation through the shared precise
9
+ // trig core so FFT, direct DFT, STFT, and cosine windows agree exactly.
10
+ //
11
+ // phase is in [0, period). sign is -1 for a forward transform and +1 for an
12
+ // inverse transform. The result is (cos, sin).
13
+ // Backend-stable f32 sine/cosine core.
14
+ //
15
+ // Shader transcendental accuracy is implementation-defined, and some portable
16
+ // backends are only accurate to roughly 1e-4. Each path retains the most accurate
17
+ // available phase representation, reduces it to [-pi, pi], then uses these
18
+ // polynomials. The half-pi core is degree 13 for sine and degree 12 for cosine;
19
+ // truncation error is well below one f32 ULP over its documented interval.
20
+ const PRECISE_TRIG_PI: f32 = 3.141592653589793;
21
+ const PRECISE_TRIG_TWO_PI: f32 = 6.283185307179586;
22
+ const PRECISE_TRIG_HALF_PI: f32 = 1.5707963267948966;
23
+
24
+ // Input must be in [-pi/2, pi/2]. Returns (cos(x), sin(x)).
25
+ fn precise_sincos_half_pi(x: f32) -> vec2<f32> {
26
+ let x2 = x * x;
27
+
28
+ var sinPolynomial = 1.6059043836821613e-10;
29
+ sinPolynomial = fma(sinPolynomial, x2, -2.505210838544172e-8);
30
+ sinPolynomial = fma(sinPolynomial, x2, 2.7557319223985893e-6);
31
+ sinPolynomial = fma(sinPolynomial, x2, -1.984126984126984e-4);
32
+ sinPolynomial = fma(sinPolynomial, x2, 8.333333333333333e-3);
33
+ sinPolynomial = fma(sinPolynomial, x2, -1.6666666666666666e-1);
34
+ let sine = x * fma(sinPolynomial, x2, 1.0);
35
+
36
+ var cosPolynomial = 2.08767569878681e-9;
37
+ cosPolynomial = fma(cosPolynomial, x2, -2.755731922398589e-7);
38
+ cosPolynomial = fma(cosPolynomial, x2, 2.48015873015873e-5);
39
+ cosPolynomial = fma(cosPolynomial, x2, -1.388888888888889e-3);
40
+ cosPolynomial = fma(cosPolynomial, x2, 4.1666666666666664e-2);
41
+ cosPolynomial = fma(cosPolynomial, x2, -5.0e-1);
42
+ let cosine = fma(cosPolynomial, x2, 1.0);
43
+
44
+ return vec2<f32>(cosine, sine);
45
+ }
46
+
47
+ // Input must be in [-pi, pi]. Returns (cos(x), sin(x)).
48
+ fn precise_sincos_centered(x: f32) -> vec2<f32> {
49
+ var folded = x;
50
+ var cosineSign = 1.0;
51
+ if (folded > PRECISE_TRIG_HALF_PI) {
52
+ folded = PRECISE_TRIG_PI - folded;
53
+ cosineSign = -1.0;
54
+ } else if (folded < -PRECISE_TRIG_HALF_PI) {
55
+ folded = -PRECISE_TRIG_PI - folded;
56
+ cosineSign = -1.0;
57
+ }
58
+ let value = precise_sincos_half_pi(folded);
59
+ return vec2<f32>(cosineSign * value.x, value.y);
60
+ }
61
+
62
+
63
+ fn dft_precise_twiddle(phase: u32, period: u32, sign: f32) -> vec2<f32> {
64
+ let reducedPhase = phase % period;
65
+ if (reducedPhase == 0u) {
66
+ return vec2<f32>(1.0, sign * 0.0);
67
+ }
68
+ if (period % 4u == 0u) {
69
+ let quarter = period / 4u;
70
+ if (reducedPhase == quarter) {
71
+ return vec2<f32>(0.0, sign);
72
+ }
73
+ if (reducedPhase == 2u * quarter) {
74
+ return vec2<f32>(-1.0, sign * 0.0);
75
+ }
76
+ if (reducedPhase == 3u * quarter) {
77
+ return vec2<f32>(0.0, -sign);
78
+ }
79
+ }
80
+ // Center the rational turn before multiplying by 2*pi. This avoids the
81
+ // cancellation in `2*pi*(N-1)/N - 2*pi` for bins near the end of a row.
82
+ let turns = f32(reducedPhase) / f32(period);
83
+ let centeredTurns = turns - floor(turns + 0.5);
84
+ return precise_sincos_centered(sign * PRECISE_TRIG_TWO_PI * centeredTurns);
85
+ }
86
+
87
+ const N: u32 = {{ source.transformLength }}u;
88
+ const NUM_BINS: u32 = {{ source.numBins }}u;
89
+ const WG: u32 = {{ source.workgroupSize }}u;
90
+ const TWIDDLE_RESET: u32 = 64u;
91
+
92
+ var<workgroup> signalTile: array<f32, N>;
93
+
94
+ @compute @workgroup_size(WG, 1, 1)
95
+ fn main(
96
+ @builtin(workgroup_id) wg: vec3<u32>,
97
+ @builtin(local_invocation_id) lid: vec3<u32>,
98
+ ) {
99
+ let lane = lid.x;
100
+ let frame = wg.y;
101
+ let batch = wg.z;
102
+ let start = frame * params.frameStep;
103
+
104
+ for (var n = lane; n < N; n = n + WG) {
105
+ let signalIndex = start + n;
106
+ signalTile[n] = signal[batch * params.signalLength + signalIndex] * window[n];
107
+ }
108
+ workgroupBarrier();
109
+
110
+ let freq = wg.x * WG + lane;
111
+ if (freq >= NUM_BINS) {
112
+ return;
113
+ }
114
+
115
+ let stepTwiddle = dft_precise_twiddle(freq, N, -1.0);
116
+ var real = 0.0;
117
+ var imag = 0.0;
118
+
119
+ for (var base = 0u; base < N; base = base + TWIDDLE_RESET) {
120
+ // Reduce the reset phase in integer space first. Computing angleStep*base
121
+ // directly loses enough f32 precision at high bins/long signals to create
122
+ // visible drift even though each 64-sample recurrence remains stable.
123
+ let phase = (freq * base) % N;
124
+ var twiddle = dft_precise_twiddle(phase, N, -1.0);
125
+ let end = min(N, base + TWIDDLE_RESET);
126
+ for (var n = base; n < end; n = n + 1u) {
127
+ let value = signalTile[n];
128
+ real = real + value * twiddle.x;
129
+ imag = imag + value * twiddle.y;
130
+ twiddle = vec2<f32>(
131
+ twiddle.x * stepTwiddle.x - twiddle.y * stepTwiddle.y,
132
+ twiddle.x * stepTwiddle.y + twiddle.y * stepTwiddle.x,
133
+ );
134
+ }
135
+ }
136
+
137
+ let outputBase = ((batch * params.numFrames + frame) * NUM_BINS + freq) * 2u;
138
+ output[outputBase] = real;
139
+ output[outputBase + 1u] = imag;
140
+ }
build/webgpu/fft-radix2-dit-shared-inplace.wgsl.jinja ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Workgroup-resident in-place radix-2 DIT FFT.
2
+ //
3
+ // This route fills the gap between the double-buffered Stockham kernel
4
+ // (16 bytes/element) and the global-scratch DIT fallback. A single complex
5
+ // array needs only 8 bytes/element, extending the resident FFT range one
6
+ // power-of-two rung beyond the 16-byte/element Stockham kernel on every device.
7
+ // Each workgroup owns one transform; all stages remain local and use uniform
8
+ // barriers, so only the input and requested spectrum touch global memory.
9
+ // STFT is forward-only, so the transform sign is fixed at -1.
10
+ {{ env.wgsl.resourceDeclarations }}
11
+ // Backend-stable unit-circle twiddles for DFT kernels. Retain the phase as a
12
+ // rational turn and route all polynomial evaluation through the shared precise
13
+ // trig core so FFT, direct DFT, STFT, and cosine windows agree exactly.
14
+ //
15
+ // phase is in [0, period). sign is -1 for a forward transform and +1 for an
16
+ // inverse transform. The result is (cos, sin).
17
+ // Backend-stable f32 sine/cosine core.
18
+ //
19
+ // Shader transcendental accuracy is implementation-defined, and some portable
20
+ // backends are only accurate to roughly 1e-4. Each path retains the most accurate
21
+ // available phase representation, reduces it to [-pi, pi], then uses these
22
+ // polynomials. The half-pi core is degree 13 for sine and degree 12 for cosine;
23
+ // truncation error is well below one f32 ULP over its documented interval.
24
+ const PRECISE_TRIG_PI: f32 = 3.141592653589793;
25
+ const PRECISE_TRIG_TWO_PI: f32 = 6.283185307179586;
26
+ const PRECISE_TRIG_HALF_PI: f32 = 1.5707963267948966;
27
+
28
+ // Input must be in [-pi/2, pi/2]. Returns (cos(x), sin(x)).
29
+ fn precise_sincos_half_pi(x: f32) -> vec2<f32> {
30
+ let x2 = x * x;
31
+
32
+ var sinPolynomial = 1.6059043836821613e-10;
33
+ sinPolynomial = fma(sinPolynomial, x2, -2.505210838544172e-8);
34
+ sinPolynomial = fma(sinPolynomial, x2, 2.7557319223985893e-6);
35
+ sinPolynomial = fma(sinPolynomial, x2, -1.984126984126984e-4);
36
+ sinPolynomial = fma(sinPolynomial, x2, 8.333333333333333e-3);
37
+ sinPolynomial = fma(sinPolynomial, x2, -1.6666666666666666e-1);
38
+ let sine = x * fma(sinPolynomial, x2, 1.0);
39
+
40
+ var cosPolynomial = 2.08767569878681e-9;
41
+ cosPolynomial = fma(cosPolynomial, x2, -2.755731922398589e-7);
42
+ cosPolynomial = fma(cosPolynomial, x2, 2.48015873015873e-5);
43
+ cosPolynomial = fma(cosPolynomial, x2, -1.388888888888889e-3);
44
+ cosPolynomial = fma(cosPolynomial, x2, 4.1666666666666664e-2);
45
+ cosPolynomial = fma(cosPolynomial, x2, -5.0e-1);
46
+ let cosine = fma(cosPolynomial, x2, 1.0);
47
+
48
+ return vec2<f32>(cosine, sine);
49
+ }
50
+
51
+ // Input must be in [-pi, pi]. Returns (cos(x), sin(x)).
52
+ fn precise_sincos_centered(x: f32) -> vec2<f32> {
53
+ var folded = x;
54
+ var cosineSign = 1.0;
55
+ if (folded > PRECISE_TRIG_HALF_PI) {
56
+ folded = PRECISE_TRIG_PI - folded;
57
+ cosineSign = -1.0;
58
+ } else if (folded < -PRECISE_TRIG_HALF_PI) {
59
+ folded = -PRECISE_TRIG_PI - folded;
60
+ cosineSign = -1.0;
61
+ }
62
+ let value = precise_sincos_half_pi(folded);
63
+ return vec2<f32>(cosineSign * value.x, value.y);
64
+ }
65
+
66
+
67
+ fn dft_precise_twiddle(phase: u32, period: u32, sign: f32) -> vec2<f32> {
68
+ let reducedPhase = phase % period;
69
+ if (reducedPhase == 0u) {
70
+ return vec2<f32>(1.0, sign * 0.0);
71
+ }
72
+ if (period % 4u == 0u) {
73
+ let quarter = period / 4u;
74
+ if (reducedPhase == quarter) {
75
+ return vec2<f32>(0.0, sign);
76
+ }
77
+ if (reducedPhase == 2u * quarter) {
78
+ return vec2<f32>(-1.0, sign * 0.0);
79
+ }
80
+ if (reducedPhase == 3u * quarter) {
81
+ return vec2<f32>(0.0, -sign);
82
+ }
83
+ }
84
+ // Center the rational turn before multiplying by 2*pi. This avoids the
85
+ // cancellation in `2*pi*(N-1)/N - 2*pi` for bins near the end of a row.
86
+ let turns = f32(reducedPhase) / f32(period);
87
+ let centeredTurns = turns - floor(turns + 0.5);
88
+ return precise_sincos_centered(sign * PRECISE_TRIG_TWO_PI * centeredTurns);
89
+ }
90
+
91
+
92
+ const N: u32 = {{ source.fftLength }}u;
93
+ const N_HALF: u32 = N / 2u;
94
+ const NUM_BINS: u32 = {{ source.numBins }}u;
95
+ const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
96
+ const SIGN: f32 = -1.0;
97
+ var<workgroup> fftData: array<vec2<f32>, {{ source.fftLength }}>;
98
+
99
+ @compute @workgroup_size(WG, 1, 1)
100
+ fn main(@builtin(workgroup_id) wgid: vec3<u32>,
101
+ @builtin(local_invocation_id) lid: vec3<u32>,
102
+ @builtin(num_workgroups) nwg: vec3<u32>) {
103
+ let transform = wgid.x + wgid.y * nwg.x;
104
+ let batch = transform / params.numFrames;
105
+ let frame = transform - batch * params.numFrames;
106
+ let start = frame * params.frameStep;
107
+
108
+ for (var position = lid.x; position < N; position = position + WG) {
109
+ let n = reverseBits(position) >> (32u - {{ source.log2Length }}u);
110
+ let signalIndex = start + n;
111
+ let inputBase = (batch * params.signalLength + signalIndex) * {{ source.inComp }}u;
112
+ let w = window[n];
113
+ var value = vec2<f32>(signal[inputBase] * w, 0.0);
114
+ {% if source.inComp == 2 %}
115
+ value.y = signal[inputBase + 1u] * w;
116
+ {% endif %}
117
+ fftData[position] = value;
118
+ }
119
+ workgroupBarrier();
120
+
121
+ {% for stage in range(1, source.log2Length + 1) %}
122
+ {
123
+ const SPAN: u32 = 1u << {{ stage }}u;
124
+ const HALF: u32 = SPAN >> 1u;
125
+ for (var butterfly = lid.x; butterfly < N_HALF; butterfly = butterfly + WG) {
126
+ let group = butterfly / HALF;
127
+ let j = butterfly - group * HALF;
128
+ let lo = group * SPAN + j;
129
+ let hi = lo + HALF;
130
+ let a = fftData[lo];
131
+ let b = fftData[hi];
132
+ let twiddle = dft_precise_twiddle(j, SPAN, SIGN);
133
+ let t = vec2<f32>(
134
+ b.x * twiddle.x - b.y * twiddle.y,
135
+ b.x * twiddle.y + b.y * twiddle.x,
136
+ );
137
+ fftData[lo] = a + t;
138
+ fftData[hi] = a - t;
139
+ }
140
+ workgroupBarrier();
141
+ }
142
+ {% endfor %}
143
+
144
+ for (var bin = lid.x; bin < NUM_BINS; bin = bin + WG) {
145
+ let dst = (transform * NUM_BINS + bin) * 2u;
146
+ output[dst] = fftData[bin].x;
147
+ output[dst + 1u] = fftData[bin].y;
148
+ }
149
+ }
build/webgpu/fft-radix2-dit-storage-pass.wgsl.jinja ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Globally staged radix-2 DIT FFT. A single-workgroup storage FFT is efficient
2
+ // when a device can keep a whole transform resident, but architectures that
3
+ // serialize storage barriers or spill a large workgroup can become severely
4
+ // under-occupied. This route exposes every transform element or butterfly to
5
+ // the global scheduler. Separate dispatches provide the required stage barriers
6
+ // while preserving the same bit-reversed DIT algorithm and scratch layout.
7
+ // STFT is forward-only, so the transform sign is fixed at -1.
8
+ {{ env.wgsl.resourceDeclarations }}
9
+ {% if source.phase == "local_prefix" or source.phase == "stage" %}
10
+ // Backend-stable unit-circle twiddles for DFT kernels. Retain the phase as a
11
+ // rational turn and route all polynomial evaluation through the shared precise
12
+ // trig core so FFT, direct DFT, STFT, and cosine windows agree exactly.
13
+ //
14
+ // phase is in [0, period). sign is -1 for a forward transform and +1 for an
15
+ // inverse transform. The result is (cos, sin).
16
+ {% set needPreciseTrigCentered = true %}
17
+ {% set needPreciseTrigTwoPi = true %}
18
+ // Backend-stable f32 sine/cosine core.
19
+ //
20
+ // Shader transcendental accuracy is implementation-defined, and some portable
21
+ // backends are only accurate to roughly 1e-4. Each path retains the most accurate
22
+ // available phase representation, reduces it to [-pi, pi], then uses these
23
+ // polynomials. The half-pi core is degree 13 for sine and degree 12 for cosine;
24
+ // truncation error is well below one f32 ULP over its documented interval.
25
+ {% if needPreciseTrigCentered %}
26
+ const PRECISE_TRIG_PI: f32 = 3.141592653589793;
27
+ {% endif %}
28
+ {% if needPreciseTrigTwoPi %}
29
+ const PRECISE_TRIG_TWO_PI: f32 = 6.283185307179586;
30
+ {% endif %}
31
+ {% if needPreciseTrigCentered %}
32
+ const PRECISE_TRIG_HALF_PI: f32 = 1.5707963267948966;
33
+ {% endif %}
34
+
35
+ // Input must be in [-pi/2, pi/2]. Returns (cos(x), sin(x)).
36
+ fn precise_sincos_half_pi(x: f32) -> vec2<f32> {
37
+ let x2 = x * x;
38
+
39
+ var sinPolynomial = 1.6059043836821613e-10;
40
+ sinPolynomial = fma(sinPolynomial, x2, -2.505210838544172e-8);
41
+ sinPolynomial = fma(sinPolynomial, x2, 2.7557319223985893e-6);
42
+ sinPolynomial = fma(sinPolynomial, x2, -1.984126984126984e-4);
43
+ sinPolynomial = fma(sinPolynomial, x2, 8.333333333333333e-3);
44
+ sinPolynomial = fma(sinPolynomial, x2, -1.6666666666666666e-1);
45
+ let sine = x * fma(sinPolynomial, x2, 1.0);
46
+
47
+ var cosPolynomial = 2.08767569878681e-9;
48
+ cosPolynomial = fma(cosPolynomial, x2, -2.755731922398589e-7);
49
+ cosPolynomial = fma(cosPolynomial, x2, 2.48015873015873e-5);
50
+ cosPolynomial = fma(cosPolynomial, x2, -1.388888888888889e-3);
51
+ cosPolynomial = fma(cosPolynomial, x2, 4.1666666666666664e-2);
52
+ cosPolynomial = fma(cosPolynomial, x2, -5.0e-1);
53
+ let cosine = fma(cosPolynomial, x2, 1.0);
54
+
55
+ return vec2<f32>(cosine, sine);
56
+ }
57
+ {% if needPreciseTrigCentered %}
58
+
59
+ // Input must be in [-pi, pi]. Returns (cos(x), sin(x)).
60
+ fn precise_sincos_centered(x: f32) -> vec2<f32> {
61
+ var folded = x;
62
+ var cosineSign = 1.0;
63
+ if (folded > PRECISE_TRIG_HALF_PI) {
64
+ folded = PRECISE_TRIG_PI - folded;
65
+ cosineSign = -1.0;
66
+ } else if (folded < -PRECISE_TRIG_HALF_PI) {
67
+ folded = -PRECISE_TRIG_PI - folded;
68
+ cosineSign = -1.0;
69
+ }
70
+ let value = precise_sincos_half_pi(folded);
71
+ return vec2<f32>(cosineSign * value.x, value.y);
72
+ }
73
+ {% endif %}
74
+
75
+
76
+ fn dft_precise_twiddle(phase: u32, period: u32, sign: f32) -> vec2<f32> {
77
+ let reducedPhase = phase % period;
78
+ if (reducedPhase == 0u) {
79
+ return vec2<f32>(1.0, sign * 0.0);
80
+ }
81
+ if (period % 4u == 0u) {
82
+ let quarter = period / 4u;
83
+ if (reducedPhase == quarter) {
84
+ return vec2<f32>(0.0, sign);
85
+ }
86
+ if (reducedPhase == 2u * quarter) {
87
+ return vec2<f32>(-1.0, sign * 0.0);
88
+ }
89
+ if (reducedPhase == 3u * quarter) {
90
+ return vec2<f32>(0.0, -sign);
91
+ }
92
+ }
93
+ // Center the rational turn before multiplying by 2*pi. This avoids the
94
+ // cancellation in `2*pi*(N-1)/N - 2*pi` for bins near the end of a row.
95
+ let turns = f32(reducedPhase) / f32(period);
96
+ let centeredTurns = turns - floor(turns + 0.5);
97
+ return precise_sincos_centered(sign * PRECISE_TRIG_TWO_PI * centeredTurns);
98
+ }
99
+
100
+
101
+ {% endif %}
102
+ const N: u32 = {{ source.fftLength }}u;
103
+ {% if source.phase == "stage" %}
104
+ const N_HALF: u32 = N / 2u;
105
+ {% endif %}
106
+ {% if source.phase == "finish" %}
107
+ const NUM_BINS: u32 = {{ source.numBins }}u;
108
+ {% endif %}
109
+ const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
110
+ {% if source.phase == "stage" %}
111
+ const SPAN: u32 = 1u << {{ source.stage }}u;
112
+ const HALF: u32 = SPAN >> 1u;
113
+ const SIGN: f32 = -1.0;
114
+ {% elif source.phase == "local_prefix" %}
115
+ const LOCAL_N: u32 = {{ source.localLength }}u;
116
+ const BLOCKS_PER_TRANSFORM: u32 = N / LOCAL_N;
117
+ const SIGN: f32 = -1.0;
118
+ var<workgroup> localData: array<vec2<f32>, {{ source.localLength }}>;
119
+ {% endif %}
120
+
121
+ {% if source.phase != "local_prefix" %}fn folded_global_index(gid: vec3<u32>, nwg: vec3<u32>) -> u32 {
122
+ return gid.x + gid.y * nwg.x * WG;
123
+ }{% endif %}
124
+
125
+
126
+ @compute @workgroup_size(WG, 1, 1)
127
+ fn main({% if source.phase != "local_prefix" %}@builtin(global_invocation_id) gid: vec3<u32>,
128
+ {% else %}@builtin(local_invocation_id) lid: vec3<u32>,
129
+ @builtin(workgroup_id) wgid: vec3<u32>,
130
+ {% endif %}
131
+ @builtin(num_workgroups) nwg: vec3<u32>) {
132
+ {% if source.phase != "local_prefix" %}
133
+ let flat = folded_global_index(gid, nwg);
134
+ {% endif %}
135
+ let transforms = arrayLength(&scratch) / (N * 2u);
136
+
137
+ {% if source.phase == "local_prefix" %}
138
+ let flatGroup = wgid.x + wgid.y * nwg.x;
139
+ let transform = flatGroup / BLOCKS_PER_TRANSFORM;
140
+ if (transform >= transforms) { return; }
141
+ let block = flatGroup - transform * BLOCKS_PER_TRANSFORM;
142
+ let position = block * LOCAL_N + lid.x;
143
+ let n = reverseBits(position) >> (32u - {{ source.log2Length }}u);
144
+ let batch = transform / params.numFrames;
145
+ let frame = transform - batch * params.numFrames;
146
+ let start = frame * params.frameStep;
147
+ let signalIndex = start + n;
148
+ let inputBase = (batch * params.signalLength + signalIndex) * {{ source.inComp }}u;
149
+ let w = window[n];
150
+ var value = vec2<f32>(signal[inputBase] * w, 0.0);
151
+ {% if source.inComp == 2 %}
152
+ value.y = signal[inputBase + 1u] * w;
153
+ {% endif %}
154
+ localData[lid.x] = value;
155
+ workgroupBarrier();
156
+
157
+ {% for stage in range(1, source.localLog2Length + 1) %}
158
+ if (lid.x < LOCAL_N / 2u) {
159
+ const LOCAL_SPAN_{{ stage }}: u32 = 1u << {{ stage }}u;
160
+ const LOCAL_HALF_{{ stage }}: u32 = LOCAL_SPAN_{{ stage }} >> 1u;
161
+ let group = lid.x / LOCAL_HALF_{{ stage }};
162
+ let j = lid.x - group * LOCAL_HALF_{{ stage }};
163
+ let lo = group * LOCAL_SPAN_{{ stage }} + j;
164
+ let hi = lo + LOCAL_HALF_{{ stage }};
165
+ let a = localData[lo];
166
+ let b = localData[hi];
167
+ let twiddle = dft_precise_twiddle(j, LOCAL_SPAN_{{ stage }}, SIGN);
168
+ let t = vec2<f32>(
169
+ b.x * twiddle.x - b.y * twiddle.y,
170
+ b.x * twiddle.y + b.y * twiddle.x,
171
+ );
172
+ localData[lo] = a + t;
173
+ localData[hi] = a - t;
174
+ }
175
+ workgroupBarrier();
176
+ {% endfor %}
177
+
178
+ let dst = (transform * N + position) * 2u;
179
+ scratch[dst] = localData[lid.x].x;
180
+ scratch[dst + 1u] = localData[lid.x].y;
181
+
182
+ {% elif source.phase == "stage" %}
183
+ let count = transforms * N_HALF;
184
+ if (flat >= count) { return; }
185
+ let transform = flat / N_HALF;
186
+ let butterfly = flat - transform * N_HALF;
187
+ let group = butterfly / HALF;
188
+ let j = butterfly - group * HALF;
189
+ let lo = (transform * N + group * SPAN + j) * 2u;
190
+ let hi = lo + HALF * 2u;
191
+ let a = vec2<f32>(scratch[lo], scratch[lo + 1u]);
192
+ let b = vec2<f32>(scratch[hi], scratch[hi + 1u]);
193
+ let twiddle = dft_precise_twiddle(j, SPAN, SIGN);
194
+ let t = vec2<f32>(
195
+ b.x * twiddle.x - b.y * twiddle.y,
196
+ b.x * twiddle.y + b.y * twiddle.x,
197
+ );
198
+ let sum = a + t;
199
+ let difference = a - t;
200
+ scratch[lo] = sum.x;
201
+ scratch[lo + 1u] = sum.y;
202
+ scratch[hi] = difference.x;
203
+ scratch[hi + 1u] = difference.y;
204
+
205
+ {% else %}
206
+ let count = transforms * NUM_BINS;
207
+ if (flat >= count) { return; }
208
+ let transform = flat / NUM_BINS;
209
+ let bin = flat - transform * NUM_BINS;
210
+ let src = (transform * N + bin) * 2u;
211
+ let dst = (transform * NUM_BINS + bin) * 2u;
212
+ output[dst] = scratch[src];
213
+ output[dst + 1u] = scratch[src + 1u];
214
+ {% endif %}
215
+ }
build/webgpu/fft-radix2-dit-storage.wgsl.jinja ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Portable single-dispatch radix-2 DIT FFT for transforms too large for the
2
+ // guaranteed workgroup-storage budget. One workgroup owns each transform and
3
+ // uses either its full-spectrum output or a private global scratch row as an
4
+ // in-place complex buffer. Uniform storageBarrier calls replace the global
5
+ // barrier that would otherwise require one dispatch per FFT stage.
6
+ //
7
+ // Direct output is used only when each transform owns a contiguous full-spectrum
8
+ // row. Scratch-backed DFT supports one-sided output. STFT accepts real or
9
+ // complex windowed frames and copies NUM_BINS after the final stage.
10
+ {{ env.wgsl.resourceDeclarations }}
11
+ {% set STFT = source.mode == "stft" %}
12
+
13
+ const PI: f32 = 3.141592653589793;
14
+ const N: u32 = {{ source.fftLength }}u;
15
+ const N_HALF: u32 = N / 2u;
16
+ const LOG2_N: u32 = {{ source.log2Length }}u;
17
+ const NUM_BINS: u32 = {{ source.numBins }}u;
18
+ const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
19
+ const SIGN: f32 = {{ "1.0" if source.inverse else "-1.0" }};
20
+
21
+ @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
22
+ fn main(
23
+ @builtin(workgroup_id) wgid: vec3<u32>,
24
+ @builtin(local_invocation_id) lid: vec3<u32>,
25
+ @builtin(num_workgroups) nwg: vec3<u32>,
26
+ ) {
27
+ let tid = lid.x;
28
+ let transform = wgid.x + wgid.y * nwg.x;
29
+ let transformBase = transform * N * 2u;
30
+
31
+ let batch = transform / params.numFrames;
32
+ let frame = transform - batch * params.numFrames;
33
+ let start = frame * params.frameStep;
34
+
35
+ for (var n = tid; n < N; n = n + WG) {
36
+ var value = vec2<f32>(0.0);
37
+ let signalIndex = start + n;
38
+ let inputBase = (batch * params.signalLength + signalIndex) * {{ source.inComp }}u;
39
+ let w = window[n];
40
+ value.x = signal[inputBase] * w;
41
+ {% if source.inComp == 2 %}
42
+ value.y = signal[inputBase + 1u] * w;
43
+ {% endif %}
44
+ let reversed = reverseBits(n) >> (32u - LOG2_N);
45
+ let dst = transformBase + reversed * 2u;
46
+ scratch[dst] = value.x;
47
+ scratch[dst + 1u] = value.y;
48
+ }
49
+ storageBarrier();
50
+
51
+ {% for stage in range(1, source.log2Length + 1) %}
52
+ {
53
+ const SPAN: u32 = 1u << {{ stage }}u;
54
+ const HALF: u32 = SPAN >> 1u;
55
+ for (var butterfly = tid; butterfly < N_HALF; butterfly = butterfly + WG) {
56
+ let group = butterfly / HALF;
57
+ let j = butterfly - group * HALF;
58
+ let lo = transformBase + (group * SPAN + j) * 2u;
59
+ let hi = lo + HALF * 2u;
60
+ let a = vec2<f32>(scratch[lo], scratch[lo + 1u]);
61
+ let b = vec2<f32>(scratch[hi], scratch[hi + 1u]);
62
+ let angle = SIGN * 2.0 * PI * f32(j) / f32(SPAN);
63
+ let c = cos(angle);
64
+ let sn = sin(angle);
65
+ let t = vec2<f32>(b.x * c - b.y * sn, b.x * sn + b.y * c);
66
+ let sum = a + t;
67
+ let difference = a - t;
68
+ scratch[lo] = sum.x;
69
+ scratch[lo + 1u] = sum.y;
70
+ scratch[hi] = difference.x;
71
+ scratch[hi + 1u] = difference.y;
72
+ }
73
+ storageBarrier();
74
+ }
75
+ {% endfor %}
76
+
77
+ const SCALE: f32 = 1.0;
78
+ for (var k = tid; k < NUM_BINS; k = k + WG) {
79
+ let src = transformBase + k * 2u;
80
+ let dst = (transform * NUM_BINS + k) * 2u;
81
+ output[dst] = scratch[src] * SCALE;
82
+ output[dst + 1u] = scratch[src + 1u] * SCALE;
83
+ }
84
+ }
build/webgpu/fft-real-packed-stockham-shared.wgsl.jinja ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+
3
+ // Real-input RFFT specialization. Pack even/odd samples into one N/2-point
4
+ // complex transform, then reconstruct the N/2+1 non-redundant bins. This cuts
5
+ // the Stockham butterfly count almost in half while preserving the same fused
6
+ // window load as the full complex STFT kernel.
7
+ const N: u32 = {{ source.fftLength }}u;
8
+ const PACKED_N: u32 = N / 2u;
9
+ const PACKED_HALF: u32 = PACKED_N / 2u;
10
+ const NUM_BINS: u32 = PACKED_N + 1u;
11
+ const FFT_LANES: u32 = {{ tunables.WORKGROUP_SIZE }}u;
12
+ const TRANSFORMS_PER_WG: u32 = {{ source.transformsPerWorkgroup | default(1) }}u;
13
+ const WG: u32 = FFT_LANES * TRANSFORMS_PER_WG;
14
+ const STAGE_TWIDDLES: u32 = PACKED_N - 1u;
15
+ const SHARED_STRIDE: u32 = 2u * PACKED_N;
16
+
17
+ var<workgroup> shRe: array<f32, SHARED_STRIDE * TRANSFORMS_PER_WG>;
18
+ var<workgroup> shIm: array<f32, SHARED_STRIDE * TRANSFORMS_PER_WG>;
19
+
20
+ fn loadPacked(frameBase: u32, packed: u32, validTransform: bool) -> vec2<f32> {
21
+ let even = 2u * packed;
22
+ let odd = even + 1u;
23
+ var value = vec2<f32>(0.0);
24
+ if (validTransform) {
25
+ value.x = signal[frameBase + even] * window[even];
26
+ value.y = signal[frameBase + odd] * window[odd];
27
+ }
28
+ return value;
29
+ }
30
+
31
+ @compute @workgroup_size(WG)
32
+ fn main(@builtin(workgroup_id) wgid: vec3<u32>,
33
+ @builtin(local_invocation_id) lid: vec3<u32>) {
34
+ let transform = lid.x / FFT_LANES;
35
+ let tid = lid.x - transform * FFT_LANES;
36
+ let batch = wgid.y;
37
+ let frame = wgid.x * TRANSFORMS_PER_WG + transform;
38
+ let validTransform = frame < params.numFrames;
39
+ let start = frame * params.frameStep;
40
+ let frameBase = batch * params.signalLength + start;
41
+ let sharedBase = transform * SHARED_STRIDE;
42
+
43
+ // Fuse pairs of radix-2 Stockham stages into radix-4 butterflies. An odd
44
+ // packed log2 length starts with one radix-2 stage, after which every stage
45
+ // can be paired. The second stage's root B is sufficient: A=B^2 and C=B^3.
46
+ // The first butterfly loads windowed samples directly, avoiding a separate
47
+ // shared-memory load phase and its barrier.
48
+ var srcOff = sharedBase + PACKED_N;
49
+ var dstOff = sharedBase;
50
+ var radix = 1u;
51
+ if ((countTrailingZeros(PACKED_N) & 1u) != 0u) {
52
+ for (var j = tid; j < PACKED_HALF; j = j + FFT_LANES) {
53
+ let a = loadPacked(frameBase, j, validTransform);
54
+ let b = loadPacked(frameBase, PACKED_HALF + j, validTransform);
55
+ shRe[srcOff + 2u * j] = a.x + b.x;
56
+ shIm[srcOff + 2u * j] = a.y + b.y;
57
+ shRe[srcOff + 2u * j + 1u] = a.x - b.x;
58
+ shIm[srcOff + 2u * j + 1u] = a.y - b.y;
59
+ }
60
+ workgroupBarrier();
61
+ radix = 2u;
62
+ } else {
63
+ for (var j = tid; j < PACKED_N / 4u; j = j + FFT_LANES) {
64
+ let x0 = loadPacked(frameBase, j, validTransform);
65
+ let x1 = loadPacked(frameBase, PACKED_N / 4u + j, validTransform);
66
+ let x2 = loadPacked(frameBase, PACKED_N / 2u + j, validTransform);
67
+ let x3 = loadPacked(frameBase, 3u * PACKED_N / 4u + j, validTransform);
68
+ let evenSum = x0 + x2;
69
+ let evenDiff = x0 - x2;
70
+ let oddSum = x1 + x3;
71
+ let oddDiff = x1 - x3;
72
+ shRe[srcOff + 4u * j] = evenSum.x + oddSum.x;
73
+ shIm[srcOff + 4u * j] = evenSum.y + oddSum.y;
74
+ shRe[srcOff + 4u * j + 1u] = evenDiff.x + oddDiff.y;
75
+ shIm[srcOff + 4u * j + 1u] = evenDiff.y - oddDiff.x;
76
+ shRe[srcOff + 4u * j + 2u] = evenSum.x - oddSum.x;
77
+ shIm[srcOff + 4u * j + 2u] = evenSum.y - oddSum.y;
78
+ shRe[srcOff + 4u * j + 3u] = evenDiff.x - oddDiff.y;
79
+ shIm[srcOff + 4u * j + 3u] = evenDiff.y + oddDiff.x;
80
+ }
81
+ workgroupBarrier();
82
+ radix = 4u;
83
+ }
84
+ for (; radix < PACKED_N; radix = radix * 4u) {
85
+ for (var j = tid; j < PACKED_N / 4u; j = j + FFT_LANES) {
86
+ let jm = j & (radix - 1u);
87
+ let outBase = ((j / radix) * (4u * radix)) + jm;
88
+
89
+ let x0r = shRe[srcOff + j];
90
+ let x0i = shIm[srcOff + j];
91
+ let x1r = shRe[srcOff + PACKED_N / 4u + j];
92
+ let x1i = shIm[srcOff + PACKED_N / 4u + j];
93
+ let x2r = shRe[srcOff + PACKED_N / 2u + j];
94
+ let x2i = shIm[srcOff + PACKED_N / 2u + j];
95
+ let x3r = shRe[srcOff + 3u * PACKED_N / 4u + j];
96
+ let x3i = shIm[srcOff + 3u * PACKED_N / 4u + j];
97
+
98
+ let twiddleIndex = 2u * radix - 1u + jm;
99
+ let br = twiddles[2u * twiddleIndex];
100
+ let bi = twiddles[2u * twiddleIndex + 1u];
101
+ let ar = br * br - bi * bi;
102
+ let ai = 2.0 * br * bi;
103
+ let cr = ar * br - ai * bi;
104
+ let ci = ar * bi + ai * br;
105
+
106
+ let u1r = x1r * br - x1i * bi;
107
+ let u1i = x1r * bi + x1i * br;
108
+ let u2r = x2r * ar - x2i * ai;
109
+ let u2i = x2r * ai + x2i * ar;
110
+ let u3r = x3r * cr - x3i * ci;
111
+ let u3i = x3r * ci + x3i * cr;
112
+
113
+ let evenSumR = x0r + u2r;
114
+ let evenSumI = x0i + u2i;
115
+ let evenDiffR = x0r - u2r;
116
+ let evenDiffI = x0i - u2i;
117
+ let oddSumR = u1r + u3r;
118
+ let oddSumI = u1i + u3i;
119
+ let oddDiffR = u1r - u3r;
120
+ let oddDiffI = u1i - u3i;
121
+
122
+ shRe[dstOff + outBase] = evenSumR + oddSumR;
123
+ shIm[dstOff + outBase] = evenSumI + oddSumI;
124
+ shRe[dstOff + outBase + radix] = evenDiffR + oddDiffI;
125
+ shIm[dstOff + outBase + radix] = evenDiffI - oddDiffR;
126
+ shRe[dstOff + outBase + 2u * radix] = evenSumR - oddSumR;
127
+ shIm[dstOff + outBase + 2u * radix] = evenSumI - oddSumI;
128
+ shRe[dstOff + outBase + 3u * radix] = evenDiffR - oddDiffI;
129
+ shIm[dstOff + outBase + 3u * radix] = evenDiffI + oddDiffR;
130
+ }
131
+ workgroupBarrier();
132
+ let swap = srcOff;
133
+ srcOff = dstOff;
134
+ dstOff = swap;
135
+ }
136
+
137
+ let outRow = (batch * params.numFrames + frame) * NUM_BINS;
138
+ for (var k = tid; validTransform && k < NUM_BINS; k = k + FFT_LANES) {
139
+ let packedK = k & (PACKED_N - 1u);
140
+ let mirrorK = (PACKED_N - packedK) & (PACKED_N - 1u);
141
+ let zr = shRe[srcOff + packedK];
142
+ let zi = shIm[srcOff + packedK];
143
+ let mr = shRe[srcOff + mirrorK];
144
+ let mi = shIm[srcOff + mirrorK];
145
+ let sumRe = zr + mr;
146
+ let sumIm = zi - mi;
147
+ let diffRe = zr - mr;
148
+ let diffIm = zi + mi;
149
+ let rootIndex = STAGE_TWIDDLES + k;
150
+ let c = twiddles[2u * rootIndex];
151
+ let sn = twiddles[2u * rootIndex + 1u];
152
+ let outBase = (outRow + k) * 2u;
153
+ output[outBase] = 0.5 * (sumRe + c * diffIm + sn * diffRe);
154
+ output[outBase + 1u] = 0.5 * (sumIm - c * diffRe + sn * diffIm);
155
+ }
156
+ }
build/webgpu/fft-stockham-shared.wgsl.jinja ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+ {% set MIXED_TAIL = source.mixedRadixTail if source.mixedRadixTail is defined else false %}
3
+ {% set USE_TWIDDLE_TABLE = source.useTwiddleTable if source.useTwiddleTable is defined else false %}
4
+ {% set NEED_PRECISE_TWIDDLE = not USE_TWIDDLE_TABLE or MIXED_TAIL %}
5
+ {% if NEED_PRECISE_TWIDDLE %}
6
+ // Backend-stable unit-circle twiddles for DFT kernels. Retain the phase as a
7
+ // rational turn and route all polynomial evaluation through the shared precise
8
+ // trig core so FFT, direct DFT, STFT, and cosine windows agree exactly.
9
+ //
10
+ // phase is in [0, period). sign is -1 for a forward transform and +1 for an
11
+ // inverse transform. The result is (cos, sin).
12
+ {% set needPreciseTrigCentered = true %}
13
+ {% set needPreciseTrigTwoPi = true %}
14
+ // Backend-stable f32 sine/cosine core.
15
+ //
16
+ // Shader transcendental accuracy is implementation-defined, and some portable
17
+ // backends are only accurate to roughly 1e-4. Each path retains the most accurate
18
+ // available phase representation, reduces it to [-pi, pi], then uses these
19
+ // polynomials. The half-pi core is degree 13 for sine and degree 12 for cosine;
20
+ // truncation error is well below one f32 ULP over its documented interval.
21
+ {% if needPreciseTrigCentered %}
22
+ const PRECISE_TRIG_PI: f32 = 3.141592653589793;
23
+ {% endif %}
24
+ {% if needPreciseTrigTwoPi %}
25
+ const PRECISE_TRIG_TWO_PI: f32 = 6.283185307179586;
26
+ {% endif %}
27
+ {% if needPreciseTrigCentered %}
28
+ const PRECISE_TRIG_HALF_PI: f32 = 1.5707963267948966;
29
+ {% endif %}
30
+
31
+ // Input must be in [-pi/2, pi/2]. Returns (cos(x), sin(x)).
32
+ fn precise_sincos_half_pi(x: f32) -> vec2<f32> {
33
+ let x2 = x * x;
34
+
35
+ var sinPolynomial = 1.6059043836821613e-10;
36
+ sinPolynomial = fma(sinPolynomial, x2, -2.505210838544172e-8);
37
+ sinPolynomial = fma(sinPolynomial, x2, 2.7557319223985893e-6);
38
+ sinPolynomial = fma(sinPolynomial, x2, -1.984126984126984e-4);
39
+ sinPolynomial = fma(sinPolynomial, x2, 8.333333333333333e-3);
40
+ sinPolynomial = fma(sinPolynomial, x2, -1.6666666666666666e-1);
41
+ let sine = x * fma(sinPolynomial, x2, 1.0);
42
+
43
+ var cosPolynomial = 2.08767569878681e-9;
44
+ cosPolynomial = fma(cosPolynomial, x2, -2.755731922398589e-7);
45
+ cosPolynomial = fma(cosPolynomial, x2, 2.48015873015873e-5);
46
+ cosPolynomial = fma(cosPolynomial, x2, -1.388888888888889e-3);
47
+ cosPolynomial = fma(cosPolynomial, x2, 4.1666666666666664e-2);
48
+ cosPolynomial = fma(cosPolynomial, x2, -5.0e-1);
49
+ let cosine = fma(cosPolynomial, x2, 1.0);
50
+
51
+ return vec2<f32>(cosine, sine);
52
+ }
53
+ {% if needPreciseTrigCentered %}
54
+
55
+ // Input must be in [-pi, pi]. Returns (cos(x), sin(x)).
56
+ fn precise_sincos_centered(x: f32) -> vec2<f32> {
57
+ var folded = x;
58
+ var cosineSign = 1.0;
59
+ if (folded > PRECISE_TRIG_HALF_PI) {
60
+ folded = PRECISE_TRIG_PI - folded;
61
+ cosineSign = -1.0;
62
+ } else if (folded < -PRECISE_TRIG_HALF_PI) {
63
+ folded = -PRECISE_TRIG_PI - folded;
64
+ cosineSign = -1.0;
65
+ }
66
+ let value = precise_sincos_half_pi(folded);
67
+ return vec2<f32>(cosineSign * value.x, value.y);
68
+ }
69
+ {% endif %}
70
+
71
+
72
+ fn dft_precise_twiddle(phase: u32, period: u32, sign: f32) -> vec2<f32> {
73
+ let reducedPhase = phase % period;
74
+ if (reducedPhase == 0u) {
75
+ return vec2<f32>(1.0, sign * 0.0);
76
+ }
77
+ if (period % 4u == 0u) {
78
+ let quarter = period / 4u;
79
+ if (reducedPhase == quarter) {
80
+ return vec2<f32>(0.0, sign);
81
+ }
82
+ if (reducedPhase == 2u * quarter) {
83
+ return vec2<f32>(-1.0, sign * 0.0);
84
+ }
85
+ if (reducedPhase == 3u * quarter) {
86
+ return vec2<f32>(0.0, -sign);
87
+ }
88
+ }
89
+ // Center the rational turn before multiplying by 2*pi. This avoids the
90
+ // cancellation in `2*pi*(N-1)/N - 2*pi` for bins near the end of a row.
91
+ let turns = f32(reducedPhase) / f32(period);
92
+ let centeredTurns = turns - floor(turns + 0.5);
93
+ return precise_sincos_centered(sign * PRECISE_TRIG_TWO_PI * centeredTurns);
94
+ }
95
+
96
+ {% endif %}
97
+ {% set TAIL_ANCHOR = 32 %}
98
+ {% set REANCHOR_TAIL = MIXED_TAIL
99
+ and source.fftLength / source.radix2Length > TAIL_ANCHOR %}
100
+
101
+ // Radix-2 Stockham autosort FFT: one transform per workgroup, entirely in
102
+ // shared memory. log2(N) ping-pong stages alternate between the two halves of
103
+ // a 2*N scratch pair (no bit reversal; the autosort write pattern leaves the
104
+ // final stage in natural order). Threads stride over the N/2 butterflies of
105
+ // each stage with a workgroupBarrier between stages. Large transform batches
106
+ // may bind a compact precomputed twiddle table; smaller batches evaluate the
107
+ // same roots inline and avoid the extra dispatch.
108
+ // Mode "dft" views the input as [outer, N, inner, inComp] and the output as
109
+ // [outer, numBins, inner, 2]; params.inner carries the (runtime) inner count
110
+ // and dispatch.x = outer * inner workgroups. Mode "stft" runs one workgroup
111
+ // per (batch, frame): the window multiply is fused into the frame load. STFT
112
+ // selection admits only complete frames, so every frame load is in bounds. The
113
+ // inverse+onesided (IRFFT) combination is outside this specialization.
114
+ // Mode "logmel" runs one workgroup per spectrogram frame of
115
+ // a real mono signal: the frame load is centered (start - N/2) with reflection
116
+ // below zero and zeros at/after params.audioLength, and the epilogue folds the
117
+ // one-sided power spectrum through packed triangular mel bands into log10
118
+ // mel-major output instead of storing complex bins.
119
+
120
+ const N: u32 = {{ source.fftLength }}u;
121
+ const N_HALF: u32 = N / 2u;
122
+ {% if MIXED_TAIL %}
123
+ const RADIX2_N: u32 = {{ source.radix2Length }}u;
124
+ const RADIX2_HALF: u32 = RADIX2_N / 2u;
125
+ const TAIL_RADIX: u32 = N / RADIX2_N;
126
+ {% if REANCHOR_TAIL %}
127
+ const TAIL_ANCHOR: u32 = {{ TAIL_ANCHOR }}u;
128
+ {% endif %}
129
+ {% endif %}
130
+ const NUM_BINS: u32 = {{ source.numBins }}u;
131
+ const WG: u32 = {{ tunables.WORKGROUP_SIZE }}u;
132
+ {% if NEED_PRECISE_TWIDDLE %}
133
+ // Twiddle = exp(SIGN * i * 2*pi * k / span); SIGN flips for the inverse
134
+ // transform, exactly like the naive kernels' `sign` select.
135
+ const SIGN: f32 = {{ "1.0" if source.inverse else "-1.0" }};
136
+ {% endif %}
137
+
138
+ {% if MIXED_TAIL %}
139
+ // Walk consecutive powers of one unit root in each mixed-radix tail. A single
140
+ // backend-stable root evaluation per output bin replaces one precise
141
+ // polynomial evaluation per tail residue for every supported factorization.
142
+ fn complex_mul(a: vec2<f32>, b: vec2<f32>) -> vec2<f32> {
143
+ return vec2<f32>(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x);
144
+ }
145
+ {% endif %}
146
+
147
+ // Ping-pong scratch: stage reads [srcOff, srcOff+N), writes [dstOff, dstOff+N).
148
+ var<workgroup> shRe: array<f32, 2u * N>;
149
+ var<workgroup> shIm: array<f32, 2u * N>;
150
+
151
+ @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
152
+ fn main(@builtin(workgroup_id) wgid: vec3<u32>,
153
+ @builtin(local_invocation_id) lid: vec3<u32>,
154
+ @builtin(num_workgroups) nwg: vec3<u32>) {
155
+ let tid = lid.x;
156
+ // Flat workgroup index, 2D-folded: dispatches that would exceed the
157
+ // maxComputeWorkgroupsPerDimension limit on a single axis split the
158
+ // one-workgroup-per-(batch,frame) [or per-(outer,inner)] grid across x/y and
159
+ // recover the flat index here. Collapses to wgid.x when nwg.y == 1 (every DFT
160
+ // dispatch and any STFT dispatch under the per-axis limit).
161
+ let flat = wgid.x + wgid.y * nwg.x;
162
+
163
+ let batch = flat / params.numFrames;
164
+ let frame = flat - batch * params.numFrames;
165
+ let start = frame * params.frameStep;
166
+ // Windowed frame load (window multiply fused into the first read). The mixed
167
+ // path transposes n = n2 + TAIL_RADIX*n1 into contiguous n2 Stockham rows.
168
+ for (var n = tid; n < N; n = n + WG) {
169
+ {% if MIXED_TAIL %}
170
+ let sample_n = (n / RADIX2_N) + TAIL_RADIX * (n % RADIX2_N);
171
+ {% else %}
172
+ let sample_n = n;
173
+ {% endif %}
174
+ let signal_index = start + sample_n;
175
+ let base = (batch * params.signalLength + signal_index) * {{ source.inComp }}u;
176
+ {% if source.hasWindow is defined and not source.hasWindow %}
177
+ let w = 1.0;
178
+ {% else %}
179
+ let w = window[sample_n];
180
+ {% endif %}
181
+ let xr = signal[base] * w;
182
+ {% if source.inComp == 2 %}
183
+ let xi = signal[base + 1u] * w;
184
+ {% else %}
185
+ let xi = 0.0;
186
+ {% endif %}
187
+ shRe[n] = xr;
188
+ shIm[n] = xi;
189
+ }
190
+ workgroupBarrier();
191
+
192
+ // Stockham stages. Stage s (ns = 2^s) reads a = src[j], b = src[j + RADIX2_N/2],
193
+ // applies w = exp(SIGN*i*pi*(j mod ns)/ns) to b, and writes
194
+ // dst[(j div ns)*2*ns + (j mod ns)] = a + w*b
195
+ // dst[(j div ns)*2*ns + (j mod ns) + ns] = a - w*b
196
+ // |angle| <= pi, so twiddle accuracy never depends on large-argument
197
+ // sin/cos reduction (unlike the naive kernels' k*n angles).
198
+ var srcOff = 0u;
199
+ var dstOff = N;
200
+ {% if MIXED_TAIL %}
201
+ for (var s = 0u; (1u << s) < RADIX2_N; s = s + 1u) {
202
+ let ns = 1u << s;
203
+ for (var j = tid; j < N_HALF; j = j + WG) {
204
+ let sequence = j / RADIX2_HALF;
205
+ let local_j = j - sequence * RADIX2_HALF;
206
+ let sequence_base = sequence * RADIX2_N;
207
+ let jm = local_j & (ns - 1u);
208
+ let outBase = sequence_base + ((local_j >> s) << (s + 1u)) + jm;
209
+ let ar = shRe[srcOff + sequence_base + local_j];
210
+ let ai = shIm[srcOff + sequence_base + local_j];
211
+ let br = shRe[srcOff + sequence_base + RADIX2_HALF + local_j];
212
+ let bi = shIm[srcOff + sequence_base + RADIX2_HALF + local_j];
213
+ let twiddle = dft_precise_twiddle(jm, 2u * ns, SIGN);
214
+ let c = twiddle.x;
215
+ let sn = twiddle.y;
216
+ let tr = br * c - bi * sn;
217
+ let ti = br * sn + bi * c;
218
+ shRe[dstOff + outBase] = ar + tr;
219
+ shIm[dstOff + outBase] = ai + ti;
220
+ shRe[dstOff + outBase + ns] = ar - tr;
221
+ shIm[dstOff + outBase + ns] = ai - ti;
222
+ }
223
+ {% else %}
224
+ for (var s = 0u; (1u << s) < N; s = s + 1u) {
225
+ let ns = 1u << s;
226
+ for (var j = tid; j < N_HALF; j = j + WG) {
227
+ let jm = j & (ns - 1u);
228
+ let outBase = ((j >> s) << (s + 1u)) + jm;
229
+ let ar = shRe[srcOff + j];
230
+ let ai = shIm[srcOff + j];
231
+ let br = shRe[srcOff + N_HALF + j];
232
+ let bi = shIm[srcOff + N_HALF + j];
233
+ {% if USE_TWIDDLE_TABLE %}
234
+ let twiddleIndex = ns - 1u + jm;
235
+ let c = twiddles[2u * twiddleIndex];
236
+ let sn = twiddles[2u * twiddleIndex + 1u];
237
+ {% else %}
238
+ let twiddle = dft_precise_twiddle(jm, 2u * ns, SIGN);
239
+ let c = twiddle.x;
240
+ let sn = twiddle.y;
241
+ {% endif %}
242
+ let tr = br * c - bi * sn;
243
+ let ti = br * sn + bi * c;
244
+ shRe[dstOff + outBase] = ar + tr;
245
+ shIm[dstOff + outBase] = ai + ti;
246
+ shRe[dstOff + outBase + ns] = ar - tr;
247
+ shIm[dstOff + outBase + ns] = ai - ti;
248
+ }
249
+ {% endif %}
250
+ workgroupBarrier();
251
+ let swap = srcOff;
252
+ srcOff = dstOff;
253
+ dstOff = swap;
254
+ }
255
+
256
+ // The final swap leaves the natural-order result at srcOff. The 1/N inverse
257
+ // scale is a power-of-two reciprocal, so the multiply is exact and matches
258
+ // the naive kernels' division.
259
+ let outRow = (batch * params.numFrames + frame) * NUM_BINS;
260
+ for (var k = tid; k < NUM_BINS; k = k + WG) {
261
+ let outBase = (outRow + k) * 2u;
262
+ {% if MIXED_TAIL %}
263
+ let k1 = k % RADIX2_N;
264
+ var sum_re = 0.0;
265
+ var sum_im = 0.0;
266
+ let tail_step = dft_precise_twiddle(k, N, SIGN);
267
+ var tail_twiddle = vec2<f32>(1.0, SIGN * 0.0);
268
+ for (var n2 = 0u; n2 < TAIL_RADIX; n2 = n2 + 1u) {
269
+ {% if REANCHOR_TAIL %}
270
+ // Restart the walk from a precise root every TAIL_ANCHOR steps. At
271
+ // n2 == 0 this reproduces the initializer exactly (phase 0 returns the
272
+ // literal unit), so the anchored and unanchored entries agree bit
273
+ // for bit and only the chain length changes.
274
+ if (n2 % TAIL_ANCHOR == 0u) {
275
+ tail_twiddle = dft_precise_twiddle(k * n2, N, SIGN);
276
+ }
277
+ {% endif %}
278
+ let v_index = srcOff + n2 * RADIX2_N + k1;
279
+ let vr = shRe[v_index];
280
+ let vi = shIm[v_index];
281
+ sum_re = sum_re + vr * tail_twiddle.x - vi * tail_twiddle.y;
282
+ sum_im = sum_im + vr * tail_twiddle.y + vi * tail_twiddle.x;
283
+ tail_twiddle = complex_mul(tail_twiddle, tail_step);
284
+ }
285
+ output[outBase] = sum_re;
286
+ output[outBase + 1u] = sum_im;
287
+ {% else %}
288
+ output[outBase] = shRe[srcOff + k];
289
+ output[outBase + 1u] = shIm[srcOff + k];
290
+ {% endif %}
291
+ }
292
+ }
build/webgpu/fft-stockham-twiddles.wgsl.jinja ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+ // Backend-stable unit-circle twiddles for DFT kernels. Retain the phase as a
3
+ // rational turn and route all polynomial evaluation through the shared precise
4
+ // trig core so FFT, direct DFT, STFT, and cosine windows agree exactly.
5
+ //
6
+ // phase is in [0, period). sign is -1 for a forward transform and +1 for an
7
+ // inverse transform. The result is (cos, sin).
8
+ // Backend-stable f32 sine/cosine core.
9
+ //
10
+ // Shader transcendental accuracy is implementation-defined, and some portable
11
+ // backends are only accurate to roughly 1e-4. Each path retains the most accurate
12
+ // available phase representation, reduces it to [-pi, pi], then uses these
13
+ // polynomials. The half-pi core is degree 13 for sine and degree 12 for cosine;
14
+ // truncation error is well below one f32 ULP over its documented interval.
15
+ const PRECISE_TRIG_PI: f32 = 3.141592653589793;
16
+ const PRECISE_TRIG_TWO_PI: f32 = 6.283185307179586;
17
+ const PRECISE_TRIG_HALF_PI: f32 = 1.5707963267948966;
18
+
19
+ // Input must be in [-pi/2, pi/2]. Returns (cos(x), sin(x)).
20
+ fn precise_sincos_half_pi(x: f32) -> vec2<f32> {
21
+ let x2 = x * x;
22
+
23
+ var sinPolynomial = 1.6059043836821613e-10;
24
+ sinPolynomial = fma(sinPolynomial, x2, -2.505210838544172e-8);
25
+ sinPolynomial = fma(sinPolynomial, x2, 2.7557319223985893e-6);
26
+ sinPolynomial = fma(sinPolynomial, x2, -1.984126984126984e-4);
27
+ sinPolynomial = fma(sinPolynomial, x2, 8.333333333333333e-3);
28
+ sinPolynomial = fma(sinPolynomial, x2, -1.6666666666666666e-1);
29
+ let sine = x * fma(sinPolynomial, x2, 1.0);
30
+
31
+ var cosPolynomial = 2.08767569878681e-9;
32
+ cosPolynomial = fma(cosPolynomial, x2, -2.755731922398589e-7);
33
+ cosPolynomial = fma(cosPolynomial, x2, 2.48015873015873e-5);
34
+ cosPolynomial = fma(cosPolynomial, x2, -1.388888888888889e-3);
35
+ cosPolynomial = fma(cosPolynomial, x2, 4.1666666666666664e-2);
36
+ cosPolynomial = fma(cosPolynomial, x2, -5.0e-1);
37
+ let cosine = fma(cosPolynomial, x2, 1.0);
38
+
39
+ return vec2<f32>(cosine, sine);
40
+ }
41
+
42
+ // Input must be in [-pi, pi]. Returns (cos(x), sin(x)).
43
+ fn precise_sincos_centered(x: f32) -> vec2<f32> {
44
+ var folded = x;
45
+ var cosineSign = 1.0;
46
+ if (folded > PRECISE_TRIG_HALF_PI) {
47
+ folded = PRECISE_TRIG_PI - folded;
48
+ cosineSign = -1.0;
49
+ } else if (folded < -PRECISE_TRIG_HALF_PI) {
50
+ folded = -PRECISE_TRIG_PI - folded;
51
+ cosineSign = -1.0;
52
+ }
53
+ let value = precise_sincos_half_pi(folded);
54
+ return vec2<f32>(cosineSign * value.x, value.y);
55
+ }
56
+
57
+
58
+ fn dft_precise_twiddle(phase: u32, period: u32, sign: f32) -> vec2<f32> {
59
+ let reducedPhase = phase % period;
60
+ if (reducedPhase == 0u) {
61
+ return vec2<f32>(1.0, sign * 0.0);
62
+ }
63
+ if (period % 4u == 0u) {
64
+ let quarter = period / 4u;
65
+ if (reducedPhase == quarter) {
66
+ return vec2<f32>(0.0, sign);
67
+ }
68
+ if (reducedPhase == 2u * quarter) {
69
+ return vec2<f32>(-1.0, sign * 0.0);
70
+ }
71
+ if (reducedPhase == 3u * quarter) {
72
+ return vec2<f32>(0.0, -sign);
73
+ }
74
+ }
75
+ // Center the rational turn before multiplying by 2*pi. This avoids the
76
+ // cancellation in `2*pi*(N-1)/N - 2*pi` for bins near the end of a row.
77
+ let turns = f32(reducedPhase) / f32(period);
78
+ let centeredTurns = turns - floor(turns + 0.5);
79
+ return precise_sincos_centered(sign * PRECISE_TRIG_TWO_PI * centeredTurns);
80
+ }
81
+
82
+
83
+ // One compact twiddle table shared by every transform in a large Stockham
84
+ // dispatch. Stage s uses ns=2^s distinct roots at table offset ns-1, matching
85
+ // the exact angle expression in the shared Stockham FFT path. Precomputing
86
+ // each root once avoids repeating sin/cos for every frame while preserving the
87
+ // same f32 twiddle values and butterfly arithmetic.
88
+ // STFT is forward-only, so the transform sign is fixed at -1.
89
+ const N: u32 = {{ source.fftLength }}u;
90
+ const SIGN: f32 = -1.0;
91
+ {% if source.realPacked %}
92
+ const PACKED_N: u32 = N / 2u;
93
+ const STAGE_TWIDDLES: u32 = PACKED_N - 1u;
94
+
95
+ {% endif %}
96
+ @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
97
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
98
+ let tableIndex = gid.x;
99
+ {% if source.realPacked %}
100
+ if (tableIndex >= N) {
101
+ return;
102
+ }
103
+ if (tableIndex < STAGE_TWIDDLES) {
104
+ let ns = 1u << (31u - countLeadingZeros(tableIndex + 1u));
105
+ let jm = tableIndex - (ns - 1u);
106
+ let twiddle = dft_precise_twiddle(jm, 2u * ns, SIGN);
107
+ twiddles[2u * tableIndex] = twiddle.x;
108
+ twiddles[2u * tableIndex + 1u] = twiddle.y;
109
+ return;
110
+ }
111
+ let k = tableIndex - STAGE_TWIDDLES;
112
+ let twiddle = dft_precise_twiddle(k, N, SIGN);
113
+ twiddles[2u * tableIndex] = twiddle.x;
114
+ twiddles[2u * tableIndex + 1u] = twiddle.y;
115
+ {% else %}
116
+ if (tableIndex >= N - 1u) {
117
+ return;
118
+ }
119
+ let ns = 1u << (31u - countLeadingZeros(tableIndex + 1u));
120
+ let jm = tableIndex - (ns - 1u);
121
+ let twiddle = dft_precise_twiddle(jm, 2u * ns, SIGN);
122
+ twiddles[2u * tableIndex] = twiddle.x;
123
+ twiddles[2u * tableIndex + 1u] = twiddle.y;
124
+ {% endif %}
125
+ }
build/webgpu/manifest.json ADDED
@@ -0,0 +1,644 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "domain": "ai.onnx",
3
+ "name": "STFT",
4
+ "sinceVersion": 17,
5
+ "description": "Computes the short-time Fourier transform (STFT) of a rank-3 real or complex signal by sliding a complete frame over the input and applying the DFT. The `frameStep` scalar represents the required positive `frame_step` input. At least one of `window` or `frameLength` is required; when both are supplied, their lengths must match. The output contains the real and imaginary components of every frequency bin for each complete frame.",
6
+ "inputs": [
7
+ {
8
+ "role": "signal",
9
+ "dtype": "T",
10
+ "rank": 3,
11
+ "description": "Real input signal with shape `(batch_size, signal_length, 1)`, or complex input with shape `(batch_size, signal_length, 2)`."
12
+ },
13
+ {
14
+ "role": "window",
15
+ "dtype": "T",
16
+ "rank": 1,
17
+ "optional": true,
18
+ "description": "Optional 1-D window tensor with shape `(window_length)` to multiply by each frame before the DFT."
19
+ }
20
+ ],
21
+ "outputs": [
22
+ {
23
+ "role": "output",
24
+ "dtype": "T",
25
+ "rank": 4,
26
+ "description": "STFT result with shape `(batch_size, frames, dft_unique_bins, 2)` in one-sided mode or `(batch_size, frames, frame_length, 2)` in two-sided mode, with real and imaginary parts in the last dimension. `frames = floor((signal_length - frame_length) / frame_step) + 1`; partial trailing frames are not emitted.",
27
+ "shape": ["dim(shapes.signal, 0)", "expectedFrames", "floor(frameLength / 2) + 1 if attrs.onesided == 1 else frameLength", 2]
28
+ }
29
+ ],
30
+ "attributes": { "onesided": 1 },
31
+ "attributeDescriptions": {
32
+ "onesided": "When 1 (default), returns `floor(n_fft / 2) + 1` non-redundant frequency bins with indices `[0, ..., floor(n_fft / 2)]` for a real-valued input (RFFT); set to 0 to return the full spectrum."
33
+ },
34
+ "attributeConstraints": { "onesided": { "values": [0, 1] } },
35
+ "typeConstraints": { "T": ["float32"] },
36
+ "args": {
37
+ "signal": { "kind": "tensor", "semantic": "signal", "role": "input" },
38
+ "window": { "kind": "tensor", "semantic": "window", "role": "input", "required": false },
39
+ "output": { "kind": "tensor", "semantic": "output", "role": "output" },
40
+ "frameStep": {
41
+ "kind": "i32",
42
+ "semantic": "frame_step",
43
+ "role": "attribute",
44
+ "description": "Positive number of signal positions between the starts of consecutive frames."
45
+ },
46
+ "frameLength": {
47
+ "kind": "i32",
48
+ "semantic": "frame_length",
49
+ "role": "attribute",
50
+ "required": false,
51
+ "description": "Optional number of signal values in each frame; must equal the window length when both are supplied."
52
+ }
53
+ },
54
+ "tunables": {
55
+ "WORKGROUP_SIZE": 128,
56
+ "TWIDDLE_TABLE_MIN_TRANSFORMS": 4096,
57
+ "TILED_REAL_MIN_LENGTH": 512,
58
+ "GLOBAL_STAGE_LOCAL_LENGTH": 256,
59
+ "GLOBAL_STAGE_MAX_DEVICE_STORAGE_BYTES": 16384
60
+ },
61
+ "derive": {
62
+ "windowLength": "dim(shapes.window, 0) if (present.window and ranks.window == 1) else 0",
63
+ "frameLength": "windowLength if (present.window and ranks.window == 1) else (args.frameLength if args.frameLength is defined else 0)",
64
+ "frameLengthMatchesWindow": "args.frameLength is undefined or not present.window or args.frameLength == windowLength",
65
+ "frameControlsValid": "args.frameStep > 0 and frameLength > 0 and frameLength <= dim(shapes.signal, 1) and frameLengthMatchesWindow",
66
+ "expectedFrames": "floor((dim(shapes.signal, 1) - frameLength) / args.frameStep) + 1 if frameControlsValid else 0",
67
+ "rank3OutputLayout": "ranks.signal == 3 and ranks.output == 4 and dim(shapes.output, 0) == dim(shapes.signal, 0) and dim(shapes.output, 1) == expectedFrames and dim(shapes.signal, 2) >= 1 and dim(shapes.signal, 2) <= 2 and dim(shapes.output, 3) == 2",
68
+ "rank3WindowLayout": "present.window and ranks.window == 1 and rank3OutputLayout and frameControlsValid",
69
+ "rank3WindowSpectrum": "false if not present.window else ((attrs.onesided == 0 and dim(shapes.output, 2) == dim(shapes.window, 0)) or (attrs.onesided != 0 and dim(shapes.signal, 2) == 1 and dim(shapes.output, 2) == floor(dim(shapes.window, 0) / 2) + 1))",
70
+ "rank3WindowDispatchFits": "dim(shapes.output, 1) <= device.limits.maxComputeWorkgroupsPerDimension and dim(shapes.output, 0) <= device.limits.maxComputeWorkgroupsPerDimension",
71
+ "rank3WindowSharedFftFits": "false if not present.window else (rank3WindowDispatchFits and pow2ceil(dim(shapes.window, 0)) == dim(shapes.window, 0) and dim(shapes.window, 0) >= 4 and dim(shapes.window, 0) * 16 <= device.limits.maxComputeWorkgroupStorageSize)",
72
+ "rank3WindowFftRadix2": "1 if not present.window else pick([[dim(shapes.window, 0) % 256 == 0, 256], [dim(shapes.window, 0) % 128 == 0, 128], [dim(shapes.window, 0) % 64 == 0, 64], [dim(shapes.window, 0) % 32 == 0, 32], [dim(shapes.window, 0) % 16 == 0, 16], [dim(shapes.window, 0) % 8 == 0, 8], [dim(shapes.window, 0) % 4 == 0, 4]], 1)",
73
+ "rank3WindowFftTailRadix": "1 if not present.window else dim(shapes.window, 0) / rank3WindowFftRadix2",
74
+ "rank3WindowMixedFftFits": "false if not present.window else (rank3WindowDispatchFits and pow2ceil(dim(shapes.window, 0)) != dim(shapes.window, 0) and rank3WindowFftRadix2 >= 4 and rank3WindowFftTailRadix > 1 and rank3WindowFftTailRadix <= 128 and dim(shapes.window, 0) * 16 <= device.limits.maxComputeWorkgroupStorageSize)",
75
+ "rank3WindowFftLog2": "0 if not present.window else log2ceil(dim(shapes.window, 0))",
76
+ "rank3WindowRadix2ScratchFits": "false if not present.window else (rank3WindowDispatchFits and pow2ceil(dim(shapes.window, 0)) == dim(shapes.window, 0) and dim(shapes.window, 0) >= 4 and dim(shapes.window, 0) * 16 > device.limits.maxComputeWorkgroupStorageSize and dim(shapes.output, 0) * dim(shapes.output, 1) > 0 and dim(shapes.output, 0) * dim(shapes.output, 1) * dim(shapes.window, 0) * 2 * 4 <= device.limits.maxStorageBufferBindingSize and dim(shapes.output, 0) * dim(shapes.output, 1) * dim(shapes.window, 0) * 2 * 4 <= device.limits.maxBufferSize)",
77
+ "rank3WindowInplaceFftFits": "false if not present.window else (rank3WindowDispatchFits and pow2ceil(dim(shapes.window, 0)) == dim(shapes.window, 0) and dim(shapes.window, 0) >= 4 and dim(shapes.window, 0) * 16 > device.limits.maxComputeWorkgroupStorageSize and dim(shapes.window, 0) * 8 <= device.limits.maxComputeWorkgroupStorageSize and dim(shapes.output, 0) * dim(shapes.output, 1) > 0)",
78
+ "rank3WindowGlobalFftFits": "false if not present.window else (rank3WindowRadix2ScratchFits and dim(shapes.window, 0) >= 2 * tunables.GLOBAL_STAGE_LOCAL_LENGTH and dim(shapes.window, 0) * 8 > device.limits.maxComputeWorkgroupStorageSize)",
79
+ "rank3WindowTiledRealFits": "false if not present.window else (rank3WindowDispatchFits and ranks.signal == 3 and dim(shapes.signal, 2) == 1 and dim(shapes.window, 0) >= tunables.TILED_REAL_MIN_LENGTH and dim(shapes.window, 0) * 4 <= device.limits.maxComputeWorkgroupStorageSize and dim(shapes.output, 0) > 0 and dim(shapes.output, 1) > 0 and ceilDiv(dim(shapes.output, 2), 256) <= device.limits.maxComputeWorkgroupsPerDimension)",
80
+ "packedFourWayFits": "false if not present.window else (device.limits.maxComputeInvocationsPerWorkgroup >= 256 and device.limits.maxComputeWorkgroupSizeX >= 256 and dim(shapes.window, 0) * 32 <= device.limits.maxComputeWorkgroupStorageSize)",
81
+ "packedTransformsPerWorkgroup": "4 if packedFourWayFits else 1",
82
+ "rank3NoWindowLayout": "not present.window and args.frameLength is defined and rank3OutputLayout and frameControlsValid",
83
+ "rank3NoWindowSpectrum": "(attrs.onesided == 0 and dim(shapes.output, 2) == frameLength) or (attrs.onesided != 0 and dim(shapes.signal, 2) == 1 and dim(shapes.output, 2) == floor(frameLength / 2) + 1)",
84
+ "rank3NoWindowSharedFftFits": "false if ranks.signal != 3 else ((attrs.onesided == 0 or (dim(shapes.signal, 2) == 1 and dim(shapes.output, 2) >= 3)) and pow2ceil(frameLength) == frameLength and frameLength >= 4 and frameLength * 16 <= device.limits.maxComputeWorkgroupStorageSize and dim(shapes.output, 1) <= device.limits.maxComputeWorkgroupsPerDimension and dim(shapes.output, 0) <= device.limits.maxComputeWorkgroupsPerDimension)"
85
+ },
86
+ "bindingSets": {
87
+ "stftWindowFft": [
88
+ {
89
+ "name": "signal",
90
+ "arg": "signal",
91
+ "semantic": "signal",
92
+ "buffer": { "type": "read-only-storage" },
93
+ "elementType": "f32"
94
+ },
95
+ {
96
+ "name": "window",
97
+ "arg": "window",
98
+ "semantic": "window",
99
+ "buffer": { "type": "read-only-storage" },
100
+ "elementType": "f32"
101
+ },
102
+ { "name": "output", "arg": "output", "semantic": "output", "buffer": { "type": "storage" }, "elementType": "f32" },
103
+ {
104
+ "name": "params",
105
+ "semantic": "kernel.params",
106
+ "buffer": { "type": "uniform" },
107
+ "struct": {
108
+ "name": "Params",
109
+ "fields": [
110
+ { "name": "signalLength", "type": "u32", "value": "dim(shapes.signal, 1)" },
111
+ { "name": "numFrames", "type": "u32", "value": "dim(shapes.output, 1)" },
112
+ { "name": "frameStep", "type": "u32", "value": "args.frameStep" }
113
+ ]
114
+ }
115
+ }
116
+ ],
117
+ "stftTwiddleWrite": [
118
+ { "name": "twiddles", "semantic": "fft_twiddles", "buffer": { "type": "storage" }, "elementType": "f32" }
119
+ ],
120
+ "stftWindowFftTwiddles": [
121
+ {
122
+ "name": "signal",
123
+ "arg": "signal",
124
+ "semantic": "signal",
125
+ "buffer": { "type": "read-only-storage" },
126
+ "elementType": "f32"
127
+ },
128
+ {
129
+ "name": "window",
130
+ "arg": "window",
131
+ "semantic": "window",
132
+ "buffer": { "type": "read-only-storage" },
133
+ "elementType": "f32"
134
+ },
135
+ {
136
+ "name": "twiddles",
137
+ "semantic": "fft_twiddles",
138
+ "buffer": { "type": "read-only-storage" },
139
+ "elementType": "f32"
140
+ },
141
+ { "name": "output", "arg": "output", "semantic": "output", "buffer": { "type": "storage" }, "elementType": "f32" },
142
+ {
143
+ "name": "params",
144
+ "semantic": "kernel.params",
145
+ "buffer": { "type": "uniform" },
146
+ "struct": {
147
+ "name": "Params",
148
+ "fields": [
149
+ { "name": "signalLength", "type": "u32", "value": "dim(shapes.signal, 1)" },
150
+ { "name": "numFrames", "type": "u32", "value": "dim(shapes.output, 1)" },
151
+ { "name": "frameStep", "type": "u32", "value": "args.frameStep" }
152
+ ]
153
+ }
154
+ }
155
+ ],
156
+ "stftNoWindowFft": [
157
+ {
158
+ "name": "signal",
159
+ "arg": "signal",
160
+ "semantic": "signal",
161
+ "buffer": { "type": "read-only-storage" },
162
+ "elementType": "f32"
163
+ },
164
+ { "name": "output", "arg": "output", "semantic": "output", "buffer": { "type": "storage" }, "elementType": "f32" },
165
+ {
166
+ "name": "params",
167
+ "semantic": "kernel.params",
168
+ "buffer": { "type": "uniform" },
169
+ "struct": {
170
+ "name": "Params",
171
+ "fields": [
172
+ { "name": "signalLength", "type": "u32", "value": "dim(shapes.signal, 1)" },
173
+ { "name": "numFrames", "type": "u32", "value": "dim(shapes.output, 1)" },
174
+ { "name": "frameStep", "type": "u32", "value": "args.frameStep" }
175
+ ]
176
+ }
177
+ }
178
+ ],
179
+ "stftWindowScratch": [
180
+ {
181
+ "name": "signal",
182
+ "arg": "signal",
183
+ "semantic": "signal",
184
+ "buffer": { "type": "read-only-storage" },
185
+ "elementType": "f32"
186
+ },
187
+ {
188
+ "name": "window",
189
+ "arg": "window",
190
+ "semantic": "window",
191
+ "buffer": { "type": "read-only-storage" },
192
+ "elementType": "f32"
193
+ },
194
+ { "name": "scratch", "semantic": "fft_scratch", "buffer": { "type": "storage" }, "elementType": "f32" },
195
+ { "name": "output", "arg": "output", "semantic": "output", "buffer": { "type": "storage" }, "elementType": "f32" },
196
+ {
197
+ "name": "params",
198
+ "semantic": "kernel.params",
199
+ "buffer": { "type": "uniform" },
200
+ "struct": {
201
+ "name": "Params",
202
+ "fields": [
203
+ { "name": "signalLength", "type": "u32", "value": "dim(shapes.signal, 1)" },
204
+ { "name": "numFrames", "type": "u32", "value": "dim(shapes.output, 1)" },
205
+ { "name": "frameStep", "type": "u32", "value": "args.frameStep" }
206
+ ]
207
+ }
208
+ }
209
+ ],
210
+ "stftWindowScratchLocalPrefix": [
211
+ {
212
+ "name": "signal",
213
+ "arg": "signal",
214
+ "semantic": "signal",
215
+ "buffer": { "type": "read-only-storage" },
216
+ "elementType": "f32"
217
+ },
218
+ {
219
+ "name": "window",
220
+ "arg": "window",
221
+ "semantic": "window",
222
+ "buffer": { "type": "read-only-storage" },
223
+ "elementType": "f32"
224
+ },
225
+ { "name": "scratch", "semantic": "fft_scratch", "buffer": { "type": "storage" }, "elementType": "f32" },
226
+ {
227
+ "name": "params",
228
+ "semantic": "kernel.params",
229
+ "buffer": { "type": "uniform" },
230
+ "struct": {
231
+ "name": "Params",
232
+ "fields": [
233
+ { "name": "signalLength", "type": "u32", "value": "dim(shapes.signal, 1)" },
234
+ { "name": "numFrames", "type": "u32", "value": "dim(shapes.output, 1)" },
235
+ { "name": "frameStep", "type": "u32", "value": "args.frameStep" }
236
+ ]
237
+ }
238
+ }
239
+ ],
240
+ "stftWindowScratchStage": [
241
+ { "name": "scratch", "semantic": "fft_scratch", "buffer": { "type": "storage" }, "elementType": "f32" }
242
+ ],
243
+ "stftWindowScratchFinish": [
244
+ { "name": "scratch", "semantic": "fft_scratch", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
245
+ { "name": "output", "arg": "output", "semantic": "output", "buffer": { "type": "storage" }, "elementType": "f32" }
246
+ ],
247
+ "stftWindowGeneral": [
248
+ {
249
+ "name": "signal",
250
+ "arg": "signal",
251
+ "semantic": "signal",
252
+ "buffer": { "type": "read-only-storage" },
253
+ "elementType": "f32"
254
+ },
255
+ {
256
+ "name": "window",
257
+ "arg": "window",
258
+ "semantic": "window",
259
+ "buffer": { "type": "read-only-storage" },
260
+ "elementType": "f32"
261
+ },
262
+ { "name": "output", "arg": "output", "semantic": "output", "buffer": { "type": "storage" }, "elementType": "f32" },
263
+ {
264
+ "name": "params",
265
+ "semantic": "kernel.params",
266
+ "buffer": { "type": "uniform" },
267
+ "struct": {
268
+ "name": "Params",
269
+ "fields": [
270
+ { "name": "count", "type": "u32", "value": "numel(shapes.output) / 2" },
271
+ { "name": "signalLength", "type": "u32", "value": "dim(shapes.signal, 1)" },
272
+ { "name": "signalComponents", "type": "u32", "value": "dim(shapes.signal, 2)" },
273
+ { "name": "frameLength", "type": "u32", "value": "dim(shapes.window, 0)" },
274
+ { "name": "frames", "type": "u32", "value": "dim(shapes.output, 1)" },
275
+ { "name": "bins", "type": "u32", "value": "dim(shapes.output, 2)" },
276
+ { "name": "frameStep", "type": "u32", "value": "args.frameStep" }
277
+ ]
278
+ }
279
+ }
280
+ ],
281
+ "stftNoWindowGeneral": [
282
+ {
283
+ "name": "signal",
284
+ "arg": "signal",
285
+ "semantic": "signal",
286
+ "buffer": { "type": "read-only-storage" },
287
+ "elementType": "f32"
288
+ },
289
+ { "name": "output", "arg": "output", "semantic": "output", "buffer": { "type": "storage" }, "elementType": "f32" },
290
+ {
291
+ "name": "params",
292
+ "semantic": "kernel.params",
293
+ "buffer": { "type": "uniform" },
294
+ "struct": {
295
+ "name": "Params",
296
+ "fields": [
297
+ { "name": "count", "type": "u32", "value": "numel(shapes.output) / 2" },
298
+ { "name": "signalLength", "type": "u32", "value": "dim(shapes.signal, 1)" },
299
+ { "name": "signalComponents", "type": "u32", "value": "dim(shapes.signal, 2)" },
300
+ { "name": "frameLength", "type": "u32", "value": "args.frameLength" },
301
+ { "name": "frames", "type": "u32", "value": "dim(shapes.output, 1)" },
302
+ { "name": "bins", "type": "u32", "value": "dim(shapes.output, 2)" },
303
+ { "name": "frameStep", "type": "u32", "value": "args.frameStep" }
304
+ ]
305
+ }
306
+ }
307
+ ]
308
+ },
309
+ "variants": [
310
+ {
311
+ "id": "rank3_no_window_fft",
312
+ "priority": 20,
313
+ "when": ["rank3NoWindowLayout", "rank3NoWindowSpectrum", "rank3NoWindowSharedFftFits"],
314
+ "passes": [
315
+ {
316
+ "id": "main",
317
+ "name": "STFT.NoWindowFftShared",
318
+ "source": {
319
+ "shader": "fft-stockham-shared.wgsl.jinja",
320
+ "inputs": {
321
+ "mode": "\"stft\"",
322
+ "fftLength": "frameLength",
323
+ "numBins": "dim(shapes.output, 2)",
324
+ "inComp": "dim(shapes.signal, 2)",
325
+ "inverse": false,
326
+ "hasWindow": false
327
+ }
328
+ },
329
+ "bindings": "stftNoWindowFft",
330
+ "dispatch": { "x": "dim(shapes.output, 1)", "y": "dim(shapes.output, 0)" }
331
+ }
332
+ ]
333
+ },
334
+ {
335
+ "id": "rank3_window_fft_shared_inplace",
336
+ "priority": 32,
337
+ "requires": { "limits": { "maxComputeInvocationsPerWorkgroup": 256, "maxComputeWorkgroupSizeX": 256 } },
338
+ "tunables": { "WORKGROUP_SIZE": 256 },
339
+ "when": ["rank3WindowLayout", "rank3WindowSpectrum", "rank3WindowInplaceFftFits"],
340
+ "passes": [
341
+ {
342
+ "id": "main",
343
+ "name": "STFT.FftSharedInplace",
344
+ "source": {
345
+ "shader": "fft-radix2-dit-shared-inplace.wgsl.jinja",
346
+ "inputs": {
347
+ "fftLength": "dim(shapes.window, 0)",
348
+ "log2Length": "rank3WindowFftLog2",
349
+ "numBins": "dim(shapes.output, 2)",
350
+ "inComp": "dim(shapes.signal, 2)"
351
+ }
352
+ },
353
+ "bindings": "stftWindowFft",
354
+ "dispatch": { "x": "dim(shapes.output, 1)", "y": "dim(shapes.output, 0)" }
355
+ }
356
+ ]
357
+ },
358
+ {
359
+ "id": "rank3_window_fft_global_stages",
360
+ "priority": 31,
361
+ "requires": { "limits": { "maxComputeInvocationsPerWorkgroup": 256, "maxComputeWorkgroupSizeX": 256 } },
362
+ "tunables": { "WORKGROUP_SIZE": 256 },
363
+ "demoteWhen": ["device.limits.maxComputeWorkgroupStorageSize > tunables.GLOBAL_STAGE_MAX_DEVICE_STORAGE_BYTES"],
364
+ "when": ["rank3WindowLayout", "rank3WindowSpectrum", "rank3WindowGlobalFftFits"],
365
+ "intermediates": [
366
+ {
367
+ "id": "fft_scratch",
368
+ "dtype": "float32",
369
+ "shape": "[dim(shapes.output, 0) * dim(shapes.output, 1) * dim(shapes.window, 0) * 2]"
370
+ }
371
+ ],
372
+ "passes": [
373
+ {
374
+ "id": "local_prefix",
375
+ "name": "STFT.FftLocalPrefix",
376
+ "source": {
377
+ "shader": "fft-radix2-dit-storage-pass.wgsl.jinja",
378
+ "inputs": {
379
+ "phase": "\"local_prefix\"",
380
+ "fftLength": "dim(shapes.window, 0)",
381
+ "log2Length": "rank3WindowFftLog2",
382
+ "localLength": "tunables.GLOBAL_STAGE_LOCAL_LENGTH",
383
+ "localLog2Length": "log2ceil(tunables.GLOBAL_STAGE_LOCAL_LENGTH)",
384
+ "numBins": "dim(shapes.output, 2)",
385
+ "inComp": "dim(shapes.signal, 2)"
386
+ }
387
+ },
388
+ "bindings": "stftWindowScratchLocalPrefix",
389
+ "dispatch": {
390
+ "workgroups": "dim(shapes.output, 0) * dim(shapes.output, 1) * dim(shapes.window, 0) / tunables.GLOBAL_STAGE_LOCAL_LENGTH"
391
+ }
392
+ },
393
+ {
394
+ "id": "stages",
395
+ "repeat": { "count": "rank3WindowFftLog2 - log2ceil(tunables.GLOBAL_STAGE_LOCAL_LENGTH)", "index": "stage" },
396
+ "passes": [
397
+ {
398
+ "id": "butterflies",
399
+ "name": "STFT.FftGlobalStage",
400
+ "source": {
401
+ "shader": "fft-radix2-dit-storage-pass.wgsl.jinja",
402
+ "inputs": {
403
+ "phase": "\"stage\"",
404
+ "stage": "repeat.stage + log2ceil(tunables.GLOBAL_STAGE_LOCAL_LENGTH) + 1",
405
+ "fftLength": "dim(shapes.window, 0)",
406
+ "log2Length": "rank3WindowFftLog2",
407
+ "numBins": "dim(shapes.output, 2)",
408
+ "inComp": "dim(shapes.signal, 2)"
409
+ }
410
+ },
411
+ "bindings": "stftWindowScratchStage",
412
+ "dispatch": {
413
+ "threads": "dim(shapes.output, 0) * dim(shapes.output, 1) * dim(shapes.window, 0) / 2",
414
+ "workgroupSize": "tunables.WORKGROUP_SIZE"
415
+ }
416
+ }
417
+ ]
418
+ },
419
+ {
420
+ "id": "finish",
421
+ "name": "STFT.FftGlobalFinish",
422
+ "source": {
423
+ "shader": "fft-radix2-dit-storage-pass.wgsl.jinja",
424
+ "inputs": {
425
+ "phase": "\"finish\"",
426
+ "fftLength": "dim(shapes.window, 0)",
427
+ "log2Length": "rank3WindowFftLog2",
428
+ "numBins": "dim(shapes.output, 2)",
429
+ "inComp": "dim(shapes.signal, 2)"
430
+ }
431
+ },
432
+ "bindings": "stftWindowScratchFinish",
433
+ "dispatch": {
434
+ "threads": "dim(shapes.output, 0) * dim(shapes.output, 1) * dim(shapes.output, 2)",
435
+ "workgroupSize": "tunables.WORKGROUP_SIZE"
436
+ }
437
+ }
438
+ ]
439
+ },
440
+ {
441
+ "id": "rank3_window_fft_storage_inplace",
442
+ "priority": 30,
443
+ "when": ["rank3WindowLayout", "rank3WindowSpectrum", "rank3WindowRadix2ScratchFits"],
444
+ "intermediates": [
445
+ {
446
+ "id": "fft_scratch",
447
+ "dtype": "float32",
448
+ "shape": "[dim(shapes.output, 0) * dim(shapes.output, 1) * dim(shapes.window, 0) * 2]"
449
+ }
450
+ ],
451
+ "passes": [
452
+ {
453
+ "id": "main",
454
+ "name": "STFT.FftStorageInplace",
455
+ "source": {
456
+ "shader": "fft-radix2-dit-storage.wgsl.jinja",
457
+ "inputs": {
458
+ "mode": "\"stft\"",
459
+ "fftLength": "dim(shapes.window, 0)",
460
+ "log2Length": "rank3WindowFftLog2",
461
+ "numBins": "dim(shapes.output, 2)",
462
+ "inComp": "dim(shapes.signal, 2)",
463
+ "inverse": false
464
+ }
465
+ },
466
+ "bindings": "stftWindowScratch",
467
+ "dispatch": { "x": "dim(shapes.output, 1)", "y": "dim(shapes.output, 0)" }
468
+ }
469
+ ]
470
+ },
471
+ {
472
+ "id": "rank3_window_tiled_real",
473
+ "priority": 19,
474
+ "requires": { "limits": { "maxComputeInvocationsPerWorkgroup": 256, "maxComputeWorkgroupSizeX": 256 } },
475
+ "tunables": { "WORKGROUP_SIZE": 256 },
476
+ "when": ["rank3WindowLayout", "rank3WindowSpectrum", "rank3WindowTiledRealFits"],
477
+ "passes": [
478
+ {
479
+ "id": "main",
480
+ "name": "STFT.TiledReal",
481
+ "source": {
482
+ "shader": "dft-tiled-real.wgsl.jinja",
483
+ "inputs": {
484
+ "mode": "\"stft\"",
485
+ "transformLength": "dim(shapes.window, 0)",
486
+ "numBins": "dim(shapes.output, 2)",
487
+ "workgroupSize": "tunables.WORKGROUP_SIZE"
488
+ }
489
+ },
490
+ "bindings": "stftWindowFft",
491
+ "dispatch": {
492
+ "x": "ceilDiv(dim(shapes.output, 2), 256)",
493
+ "y": "dim(shapes.output, 1)",
494
+ "z": "dim(shapes.output, 0)"
495
+ }
496
+ }
497
+ ]
498
+ },
499
+ {
500
+ "id": "rank3_window_rfft_packed",
501
+ "priority": 22,
502
+ "tunables": { "WORKGROUP_SIZE": 64 },
503
+ "when": ["rank3WindowLayout", "rank3WindowSpectrum", "rank3WindowSharedFftFits", "attrs.onesided != 0", "dim(shapes.signal, 2) == 1", "dim(shapes.window, 0) >= 64", "dim(shapes.output, 0) * dim(shapes.output, 1) >= tunables.TWIDDLE_TABLE_MIN_TRANSFORMS"],
504
+ "intermediates": [{ "id": "fft_twiddles", "dtype": "float32", "shape": "[2 * dim(shapes.window, 0)]" }],
505
+ "passes": [
506
+ {
507
+ "id": "twiddles",
508
+ "name": "STFT.RealPackedTwiddleTable",
509
+ "source": {
510
+ "shader": "fft-stockham-twiddles.wgsl.jinja",
511
+ "inputs": { "fftLength": "dim(shapes.window, 0)", "realPacked": 1 }
512
+ },
513
+ "bindings": "stftTwiddleWrite",
514
+ "dispatch": { "threads": "dim(shapes.window, 0)", "workgroupSize": "tunables.WORKGROUP_SIZE" }
515
+ },
516
+ {
517
+ "id": "main",
518
+ "name": "STFT.RealPackedFftShared",
519
+ "source": {
520
+ "shader": "fft-real-packed-stockham-shared.wgsl.jinja",
521
+ "inputs": { "fftLength": "dim(shapes.window, 0)", "transformsPerWorkgroup": "packedTransformsPerWorkgroup" }
522
+ },
523
+ "bindings": "stftWindowFftTwiddles",
524
+ "dispatch": {
525
+ "x": "ceilDiv(dim(shapes.output, 1), packedTransformsPerWorkgroup)",
526
+ "y": "dim(shapes.output, 0)"
527
+ }
528
+ }
529
+ ]
530
+ },
531
+ {
532
+ "id": "rank3_window_fft_twiddles",
533
+ "priority": 21,
534
+ "when": ["rank3WindowLayout", "rank3WindowSpectrum", "rank3WindowSharedFftFits", "dim(shapes.window, 0) >= 64", "dim(shapes.output, 0) * dim(shapes.output, 1) >= tunables.TWIDDLE_TABLE_MIN_TRANSFORMS"],
535
+ "intermediates": [{ "id": "fft_twiddles", "dtype": "float32", "shape": "[2 * (dim(shapes.window, 0) - 1)]" }],
536
+ "passes": [
537
+ {
538
+ "id": "twiddles",
539
+ "name": "STFT.FftTwiddleTable",
540
+ "source": {
541
+ "shader": "fft-stockham-twiddles.wgsl.jinja",
542
+ "inputs": { "fftLength": "dim(shapes.window, 0)", "realPacked": 0 }
543
+ },
544
+ "bindings": "stftTwiddleWrite",
545
+ "dispatch": { "threads": "dim(shapes.window, 0) - 1", "workgroupSize": "tunables.WORKGROUP_SIZE" }
546
+ },
547
+ {
548
+ "id": "main",
549
+ "name": "STFT.FftSharedTwiddles",
550
+ "source": {
551
+ "shader": "fft-stockham-shared.wgsl.jinja",
552
+ "inputs": {
553
+ "mode": "\"stft\"",
554
+ "fftLength": "dim(shapes.window, 0)",
555
+ "numBins": "dim(shapes.output, 2)",
556
+ "inComp": "dim(shapes.signal, 2)",
557
+ "inverse": false,
558
+ "useTwiddleTable": true
559
+ }
560
+ },
561
+ "bindings": "stftWindowFftTwiddles",
562
+ "dispatch": { "x": "dim(shapes.output, 1)", "y": "dim(shapes.output, 0)" }
563
+ }
564
+ ]
565
+ },
566
+ {
567
+ "id": "rank3_window_fft_mixed",
568
+ "priority": 25,
569
+ "when": ["rank3WindowLayout", "rank3WindowSpectrum", "rank3WindowMixedFftFits"],
570
+ "passes": [
571
+ {
572
+ "id": "main",
573
+ "name": "STFT.FftMixedRecurrent",
574
+ "source": {
575
+ "shader": "fft-stockham-shared.wgsl.jinja",
576
+ "inputs": {
577
+ "mode": "\"stft\"",
578
+ "fftLength": "dim(shapes.window, 0)",
579
+ "radix2Length": "rank3WindowFftRadix2",
580
+ "mixedRadixTail": true,
581
+ "numBins": "dim(shapes.output, 2)",
582
+ "inComp": "dim(shapes.signal, 2)",
583
+ "inverse": false
584
+ }
585
+ },
586
+ "bindings": "stftWindowFft",
587
+ "dispatch": { "x": "dim(shapes.output, 1)", "y": "dim(shapes.output, 0)" }
588
+ }
589
+ ]
590
+ },
591
+ {
592
+ "id": "rank3_window_fft",
593
+ "priority": 20,
594
+ "when": ["rank3WindowLayout", "rank3WindowSpectrum", "rank3WindowSharedFftFits"],
595
+ "passes": [
596
+ {
597
+ "id": "main",
598
+ "name": "STFT.FftShared",
599
+ "source": {
600
+ "shader": "fft-stockham-shared.wgsl.jinja",
601
+ "inputs": {
602
+ "mode": "\"stft\"",
603
+ "fftLength": "dim(shapes.window, 0)",
604
+ "numBins": "dim(shapes.output, 2)",
605
+ "inComp": "dim(shapes.signal, 2)",
606
+ "inverse": false
607
+ }
608
+ },
609
+ "bindings": "stftWindowFft",
610
+ "dispatch": { "x": "dim(shapes.output, 1)", "y": "dim(shapes.output, 0)" }
611
+ }
612
+ ]
613
+ },
614
+ {
615
+ "id": "rank3_window",
616
+ "priority": 0,
617
+ "supersededBy": ["rank3_window_fft_shared_inplace", "rank3_window_fft_global_stages", "rank3_window_fft_storage_inplace", "rank3_window_fft_mixed", "rank3_window_rfft_packed", "rank3_window_fft_twiddles", "rank3_window_fft", "rank3_window_tiled_real"],
618
+ "when": ["rank3WindowLayout", "rank3WindowSpectrum"],
619
+ "passes": [
620
+ {
621
+ "id": "main",
622
+ "name": "STFT",
623
+ "source": { "shader": "stft-general.wgsl.jinja", "inputs": { "hasWindowInput": true } },
624
+ "bindings": "stftWindowGeneral",
625
+ "dispatch": { "threads": "numel(shapes.output) / 2", "workgroupSize": "tunables.WORKGROUP_SIZE" }
626
+ }
627
+ ]
628
+ },
629
+ {
630
+ "id": "rank3_no_window",
631
+ "priority": 10,
632
+ "when": ["rank3NoWindowLayout", "rank3NoWindowSpectrum"],
633
+ "passes": [
634
+ {
635
+ "id": "main",
636
+ "name": "STFT.GeneralNoWindow",
637
+ "source": { "shader": "stft-general.wgsl.jinja", "inputs": { "hasWindowInput": false } },
638
+ "bindings": "stftNoWindowGeneral",
639
+ "dispatch": { "threads": "numel(shapes.output) / 2", "workgroupSize": "tunables.WORKGROUP_SIZE" }
640
+ }
641
+ ]
642
+ }
643
+ ]
644
+ }
build/webgpu/metadata.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "ai.onnx.STFT",
3
+ "id": "_ai_onnx_stft_webgpu_285ab84",
4
+ "version": 1,
5
+ "license": "Apache-2.0",
6
+ "backend": { "type": "webgpu" },
7
+ "digest": {
8
+ "algorithm": "sha256",
9
+ "files": {
10
+ "bench.json": "OKXSfrXKgDJBKuWa61pzqlE0q6XeDp3+Ac78kZJzvEs=",
11
+ "dft-tiled-real.wgsl.jinja": "jWXC4IENyJwpE7YFeiMBTtrmtncyfXddBe6JQBB3+h4=",
12
+ "fft-radix2-dit-shared-inplace.wgsl.jinja": "IGz/c29DBNlDXBpYQk9d44DHXtmORm86rJSyJxDQ4oU=",
13
+ "fft-radix2-dit-storage-pass.wgsl.jinja": "kDmHLr6NfPBnO2OPHYAkBgCA/dUzsHq9yIJS/v4eIpk=",
14
+ "fft-radix2-dit-storage.wgsl.jinja": "rlD1QCYyRVzQf4VEZTseGFYvUzjKNRneHUatxpz5Y9k=",
15
+ "fft-real-packed-stockham-shared.wgsl.jinja": "w9XapiIdKLjNy/3BwXrzmnBzUYWCzSvOoHE6lMQQJok=",
16
+ "fft-stockham-shared.wgsl.jinja": "ihNoaimYQY8F/oomzVbEh9g/g08yolKLYVQn79cZG1o=",
17
+ "fft-stockham-twiddles.wgsl.jinja": "6/+Ugc8XhOPl7iNTPc0Lwd0Moh8AFw+RVQZSWCxJbKY=",
18
+ "manifest.json": "stekrpAUb0OTNiTqln6FHfTBeHeZmISRKX/dP8eEkVU=",
19
+ "stft-general.wgsl.jinja": "UGbzUabSlzivMPxP2LJ2YRNK5Z6msagBugJvn3Tpu0s=",
20
+ "test.json": "Y05Ud3usGRQ2tsWy3twV3mndN+3FjHo3LjOwTA24Nog="
21
+ }
22
+ },
23
+ "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
24
+ "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/ai.onnx.STFT" }
25
+ }
build/webgpu/stft-general.wgsl.jinja ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% macro flat_index_2d(name="i", bound="params.count", guardInline=false, note="dispatch-limit") %}
2
+ {% if note == "dispatch-limit" %}
3
+ // 2D-folded flat index: gid.y carries the high bits past the
4
+ // maxComputeWorkgroupsPerDimension dispatch limit (outputs > 16.7M elements).
5
+ {% elif note == "limit" %}
6
+ // 2D-folded flat index: gid.y carries the high bits past the
7
+ // maxComputeWorkgroupsPerDimension limit.
8
+ {% elif note == "device-axis" %}
9
+ // The flat dispatch is folded across x/y at the device's per-axis workgroup
10
+ // limit; gid.y carries the high portion of the output index.
11
+ {% elif note == "vec4-limit" %}
12
+ // 2D-folded flat vec4 index: gid.y carries the high bits past the
13
+ // maxComputeWorkgroupsPerDimension limit (the dispatch caps x and spills into y).
14
+ {% elif note == "element-limit" %}
15
+ // 2D-folded flat element index: gid.y carries the high bits past the
16
+ // maxComputeWorkgroupsPerDimension limit.
17
+ {% elif note == "dispatch" %}
18
+ // 2D-folded flat index: gid.y carries the high bits past the
19
+ // maxComputeWorkgroupsPerDimension dispatch limit.
20
+ {% endif %}
21
+ {% if bound == "" %}
22
+ let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
23
+ {%- elif guardInline %}
24
+ let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
25
+ if ({{ name }} >= {{ bound }}) { return; }
26
+ {%- else %}
27
+ let {{ name }} = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
28
+ if ({{ name }} >= {{ bound }}) {
29
+ return;
30
+ }
31
+ {%- endif %}
32
+ {% endmacro %}
33
+
34
+ {{ env.wgsl.resourceDeclarations }}
35
+ // Backend-stable unit-circle twiddles for DFT kernels. Retain the phase as a
36
+ // rational turn and route all polynomial evaluation through the shared precise
37
+ // trig core so FFT, direct DFT, STFT, and cosine windows agree exactly.
38
+ //
39
+ // phase is in [0, period). sign is -1 for a forward transform and +1 for an
40
+ // inverse transform. The result is (cos, sin).
41
+ // Backend-stable f32 sine/cosine core.
42
+ //
43
+ // Shader transcendental accuracy is implementation-defined, and some portable
44
+ // backends are only accurate to roughly 1e-4. Each path retains the most accurate
45
+ // available phase representation, reduces it to [-pi, pi], then uses these
46
+ // polynomials. The half-pi core is degree 13 for sine and degree 12 for cosine;
47
+ // truncation error is well below one f32 ULP over its documented interval.
48
+ const PRECISE_TRIG_PI: f32 = 3.141592653589793;
49
+ const PRECISE_TRIG_TWO_PI: f32 = 6.283185307179586;
50
+ const PRECISE_TRIG_HALF_PI: f32 = 1.5707963267948966;
51
+
52
+ // Input must be in [-pi/2, pi/2]. Returns (cos(x), sin(x)).
53
+ fn precise_sincos_half_pi(x: f32) -> vec2<f32> {
54
+ let x2 = x * x;
55
+
56
+ var sinPolynomial = 1.6059043836821613e-10;
57
+ sinPolynomial = fma(sinPolynomial, x2, -2.505210838544172e-8);
58
+ sinPolynomial = fma(sinPolynomial, x2, 2.7557319223985893e-6);
59
+ sinPolynomial = fma(sinPolynomial, x2, -1.984126984126984e-4);
60
+ sinPolynomial = fma(sinPolynomial, x2, 8.333333333333333e-3);
61
+ sinPolynomial = fma(sinPolynomial, x2, -1.6666666666666666e-1);
62
+ let sine = x * fma(sinPolynomial, x2, 1.0);
63
+
64
+ var cosPolynomial = 2.08767569878681e-9;
65
+ cosPolynomial = fma(cosPolynomial, x2, -2.755731922398589e-7);
66
+ cosPolynomial = fma(cosPolynomial, x2, 2.48015873015873e-5);
67
+ cosPolynomial = fma(cosPolynomial, x2, -1.388888888888889e-3);
68
+ cosPolynomial = fma(cosPolynomial, x2, 4.1666666666666664e-2);
69
+ cosPolynomial = fma(cosPolynomial, x2, -5.0e-1);
70
+ let cosine = fma(cosPolynomial, x2, 1.0);
71
+
72
+ return vec2<f32>(cosine, sine);
73
+ }
74
+
75
+ // Input must be in [-pi, pi]. Returns (cos(x), sin(x)).
76
+ fn precise_sincos_centered(x: f32) -> vec2<f32> {
77
+ var folded = x;
78
+ var cosineSign = 1.0;
79
+ if (folded > PRECISE_TRIG_HALF_PI) {
80
+ folded = PRECISE_TRIG_PI - folded;
81
+ cosineSign = -1.0;
82
+ } else if (folded < -PRECISE_TRIG_HALF_PI) {
83
+ folded = -PRECISE_TRIG_PI - folded;
84
+ cosineSign = -1.0;
85
+ }
86
+ let value = precise_sincos_half_pi(folded);
87
+ return vec2<f32>(cosineSign * value.x, value.y);
88
+ }
89
+
90
+
91
+ fn dft_precise_twiddle(phase: u32, period: u32, sign: f32) -> vec2<f32> {
92
+ let reducedPhase = phase % period;
93
+ if (reducedPhase == 0u) {
94
+ return vec2<f32>(1.0, sign * 0.0);
95
+ }
96
+ if (period % 4u == 0u) {
97
+ let quarter = period / 4u;
98
+ if (reducedPhase == quarter) {
99
+ return vec2<f32>(0.0, sign);
100
+ }
101
+ if (reducedPhase == 2u * quarter) {
102
+ return vec2<f32>(-1.0, sign * 0.0);
103
+ }
104
+ if (reducedPhase == 3u * quarter) {
105
+ return vec2<f32>(0.0, -sign);
106
+ }
107
+ }
108
+ // Center the rational turn before multiplying by 2*pi. This avoids the
109
+ // cancellation in `2*pi*(N-1)/N - 2*pi` for bins near the end of a row.
110
+ let turns = f32(reducedPhase) / f32(period);
111
+ let centeredTurns = turns - floor(turns + 0.5);
112
+ return precise_sincos_centered(sign * PRECISE_TRIG_TWO_PI * centeredTurns);
113
+ }
114
+
115
+
116
+ // General direct STFT for rank-3 real or complex signals. The frame length is
117
+ // the window length when a window is bound, otherwise the adapted frame_length
118
+ // scalar. Window samples default to 1.0 when no window input is present.
119
+
120
+ @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
121
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>) {
122
+ {{ flat_index_2d("index", guardInline=true, note="") }}
123
+
124
+ let bin = index % params.bins;
125
+ var t = index / params.bins;
126
+ let frame = t % params.frames;
127
+ let batch = t / params.frames;
128
+ let start = frame * params.frameStep;
129
+ let frame_length = params.frameLength;
130
+
131
+ var real = 0.0;
132
+ var imag = 0.0;
133
+ let phasePeriod = max(frame_length, 1u);
134
+ let phaseStep = bin % phasePeriod;
135
+ var phase = 0u;
136
+ for (var n = 0u; n < frame_length; n = n + 1u) {
137
+ let signal_index = start + n;
138
+ let signal_base = (batch * params.signalLength + signal_index) * params.signalComponents;
139
+ {% if source.hasWindowInput %}
140
+ let w = window[n];
141
+ {% else %}
142
+ let w = 1.0;
143
+ {% endif %}
144
+ let xr = signal[signal_base] * w;
145
+ var xi = 0.0;
146
+ if (params.signalComponents == 2u) {
147
+ xi = signal[signal_base + 1u] * w;
148
+ }
149
+ // Reduce the integer phase before conversion. The stable unit-circle
150
+ // approximation avoids backend-dependent residuals at exact DFT roots.
151
+ let twiddle = dft_precise_twiddle(phase, frame_length, -1.0);
152
+ let c = twiddle.x;
153
+ let s = twiddle.y;
154
+ real = real + xr * c - xi * s;
155
+ imag = imag + xr * s + xi * c;
156
+ // Overflow-safe modular increment. A direct `bin * n` wraps u32 for long
157
+ // transforms before the modulus is applied.
158
+ let distanceToWrap = phasePeriod - phaseStep;
159
+ phase = select(phase + phaseStep, phase - distanceToWrap, phase >= distanceToWrap);
160
+ }
161
+ let output_base = index * 2u;
162
+ output[output_base] = real;
163
+ output[output_base + 1u] = imag;
164
+ }
build/webgpu/test.json ADDED
@@ -0,0 +1,847 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "op": "ai.onnx.STFT",
3
+ "fixtureArrays": {
4
+ "onnx_backend_arange128_cos_window16_input_window": [1, 0.961939766, 0.853553391, 0.691341716, 0.5, 0.308658284, 0.146446609, 0.038060234, 0, 0.038060234, 0.146446609, 0.308658284, 0.5, 0.691341716, 0.853553391, 0.961939766],
5
+ "ort_complex_input_batched_twosided_frame_length_input_signal": [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 99, 0, 99, 0, 99, 0, 99, 0, 99, 0, 99, 0, 99, 0, 99, 0]
6
+ },
7
+ "cases": [
8
+ {
9
+ "name": "dispatch_cliff_rank3_over_8m_elements",
10
+ "attrs": { "onesided": 1 },
11
+ "inputs": {
12
+ "signal": { "dtype": "float32", "shape": [1, 2200000, 1] },
13
+ "window": { "dtype": "float32", "shape": [6], "data": { "kind": "constant", "value": 1.0 } }
14
+ },
15
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 2199995, 4, 2], "tolerance": 0.001 } },
16
+ "args": { "frameStep": 1 }
17
+ },
18
+ {
19
+ "name": "many_frames_fft_twiddle_table_win64",
20
+ "provenance": {
21
+ "notes": "Exercises the shared twiddle-table FFT selected for thousands of independent transforms without making correctness coverage depend on the much larger dispatch-cliff benchmark allocation."
22
+ },
23
+ "inputs": {
24
+ "signal": { "dtype": "float32", "shape": [1, 4159, 1] },
25
+ "window": { "dtype": "float32", "shape": [64] }
26
+ },
27
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 4096, 33, 2], "tolerance": 0.0001 } },
28
+ "args": { "frameStep": 1 }
29
+ },
30
+ {
31
+ "name": "many_frames_fft_twiddle_table_win64_twosided",
32
+ "provenance": {
33
+ "notes": "Twosided sibling of the twiddle-table selector above. The packed real-FFT kernel that otherwise wins at this transform count requires onesided != 0, so a full-spectrum output is what leaves the shared twiddle-table FFT as the selected route."
34
+ },
35
+ "attrs": { "onesided": 0 },
36
+ "inputs": {
37
+ "signal": {
38
+ "dtype": "float32",
39
+ "shape": [1, 4159, 1],
40
+ "data": { "kind": "fillFloat32", "sinStep": 0.017, "cosStep": 0.029, "scale": 1.0 }
41
+ },
42
+ "window": {
43
+ "dtype": "float32",
44
+ "shape": [64],
45
+ "data": { "kind": "fillFloat32", "sinStep": 0.007, "cosStep": 0.013, "scale": 0.5, "offset": 0.5 }
46
+ }
47
+ },
48
+ "outputs": {
49
+ "output": { "dtype": "float32", "shape": [1, 4096, 64, 2], "tolerance": 0.001, "relTolerance": 0.0001 }
50
+ },
51
+ "args": { "frameStep": 1 }
52
+ },
53
+ {
54
+ "name": "many_frames_packed_rfft_win512_radix4",
55
+ "provenance": {
56
+ "notes": "Selector and correctness lock for the packed real N=512 path. The 4097 transforms cross the twiddle-table threshold and leave one partial four-transform workgroup on capable devices."
57
+ },
58
+ "inputs": {
59
+ "signal": {
60
+ "dtype": "float32",
61
+ "shape": [1, 4608, 1],
62
+ "data": { "kind": "fillFloat32", "sinStep": 0.017, "cosStep": 0.029, "scale": 1.0 }
63
+ },
64
+ "window": {
65
+ "dtype": "float32",
66
+ "shape": [512],
67
+ "data": { "kind": "fillFloat32", "sinStep": 0.007, "cosStep": 0.013, "scale": 0.5, "offset": 0.5 }
68
+ }
69
+ },
70
+ "outputs": {
71
+ "output": { "dtype": "float32", "shape": [1, 4097, 257, 2], "tolerance": 0.01, "relTolerance": 0.0001 }
72
+ },
73
+ "args": { "frameStep": 1 }
74
+ },
75
+ {
76
+ "name": "real_window_onesided",
77
+ "inputs": {
78
+ "signal": {
79
+ "dtype": "float32",
80
+ "shape": [1, 6, 1],
81
+ "data": { "kind": "values", "values": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] }
82
+ },
83
+ "window": { "dtype": "float32", "shape": [4], "data": { "kind": "values", "values": [0.5, 1.0, 1.0, 0.5] } }
84
+ },
85
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 2, 3, 2], "tolerance": 0.00002 } },
86
+ "args": { "frameStep": 2 }
87
+ },
88
+ {
89
+ "name": "real_subnormal_impulse_window_onesided_gpu_gap",
90
+ "skipGpu": {
91
+ "category": "permanent",
92
+ "reason": "Portable WGSL floating-point semantics do not guarantee preservation of the subnormal values required by this fixture. Backend evidence: Metal FTZ: subnormal 1e-40 impulse flushes to zero on GPU, every one-sided bin reads 0 instead of the subnormal value (tolerance 0)."
93
+ },
94
+ "provenance": {
95
+ "source": "onnxruntime/test/providers/cpu/signal/signal_ops_test.cc",
96
+ "test": "SignalOpsTest.STFTFloat",
97
+ "notes": "Focused amplitude edge: a one-frame rectangular STFT of a subnormal impulse should preserve that value in every real one-sided bin."
98
+ },
99
+ "attrs": { "onesided": 1 },
100
+ "inputs": {
101
+ "signal": {
102
+ "dtype": "float32",
103
+ "shape": [1, 4, 1],
104
+ "data": { "kind": "values", "values": [1e-40, 0.0, 0.0, 0.0] }
105
+ },
106
+ "window": { "dtype": "float32", "shape": [4], "data": { "kind": "values", "values": [1.0, 1.0, 1.0, 1.0] } }
107
+ },
108
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 1, 3, 2], "tolerance": 0 } },
109
+ "args": { "frameStep": 1 }
110
+ },
111
+ {
112
+ "name": "complex_twosided",
113
+ "attrs": { "onesided": 0 },
114
+ "inputs": {
115
+ "signal": {
116
+ "dtype": "float32",
117
+ "shape": [1, 4, 2],
118
+ "data": { "kind": "values", "values": [1.0, 0.0, 2.0, 1.0, 3.0, -1.0, 4.0, 0.0] }
119
+ },
120
+ "window": { "dtype": "float32", "shape": [2], "data": { "kind": "values", "values": [1.0, 0.5] } }
121
+ },
122
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 2, 2, 2], "tolerance": 0.00002 } },
123
+ "args": { "frameStep": 2 }
124
+ },
125
+ {
126
+ "name": "real_three_frames_window",
127
+ "inputs": {
128
+ "signal": {
129
+ "dtype": "float32",
130
+ "shape": [1, 5, 1],
131
+ "data": { "kind": "values", "values": [1.0, 2.0, 3.0, 4.0, 5.0] }
132
+ },
133
+ "window": { "dtype": "float32", "shape": [3], "data": { "kind": "values", "values": [0.25, 1.0, 0.25] } }
134
+ },
135
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 3, 2, 2], "tolerance": 0.00002 } },
136
+ "args": { "frameStep": 1 }
137
+ },
138
+ {
139
+ "name": "ort_scalar_frame_step_windowed",
140
+ "provenance": {
141
+ "source": "onnxruntime/test/providers/cpu/signal/signal_ops_test.cc",
142
+ "test": "SignalOpsTest.STFTFloat",
143
+ "notes": "Compact scalar-frame-step projection of ORT's windowed STFT test."
144
+ },
145
+ "inputs": {
146
+ "signal": {
147
+ "dtype": "float32",
148
+ "shape": [1, 6, 1],
149
+ "data": { "kind": "values", "values": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] }
150
+ },
151
+ "window": { "dtype": "float32", "shape": [4], "data": { "kind": "values", "values": [1.0, 1.0, 1.0, 1.0] } }
152
+ },
153
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 2, 3, 2], "tolerance": 0.00002 } },
154
+ "args": { "frameStep": 2 }
155
+ },
156
+ {
157
+ "name": "real_twosided_window",
158
+ "attrs": { "onesided": 0 },
159
+ "inputs": {
160
+ "signal": {
161
+ "dtype": "float32",
162
+ "shape": [1, 6, 1],
163
+ "data": { "kind": "values", "values": [0.0, 1.0, 0.0, -1.0, 0.0, 1.0] }
164
+ },
165
+ "window": { "dtype": "float32", "shape": [4], "data": { "kind": "values", "values": [1.0, 0.5, 0.5, 1.0] } }
166
+ },
167
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 2, 4, 2], "tolerance": 0.00002 } },
168
+ "args": { "frameStep": 2 }
169
+ },
170
+ {
171
+ "name": "batch2_onesided_stride2_window3",
172
+ "inputs": {
173
+ "signal": {
174
+ "dtype": "float32",
175
+ "shape": [2, 5, 1],
176
+ "data": { "kind": "values", "values": [1.0, 2.0, 3.0, 4.0, 5.0, -1.0, 0.0, 1.0, 0.0, -1.0] }
177
+ },
178
+ "window": { "dtype": "float32", "shape": [3], "data": { "kind": "values", "values": [0.5, 1.0, 0.25] } }
179
+ },
180
+ "outputs": { "output": { "dtype": "float32", "shape": [2, 2, 2, 2], "tolerance": 0.00002 } },
181
+ "args": { "frameStep": 2 }
182
+ },
183
+ {
184
+ "name": "single_frame_full_length_onesided",
185
+ "inputs": {
186
+ "signal": {
187
+ "dtype": "float32",
188
+ "shape": [1, 4, 1],
189
+ "data": { "kind": "values", "values": [1.0, 0.0, -1.0, 0.0] }
190
+ },
191
+ "window": { "dtype": "float32", "shape": [4], "data": { "kind": "constant", "value": 1.0 } }
192
+ },
193
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 1, 3, 2], "tolerance": 0.00002 } },
194
+ "args": { "frameStep": 4 }
195
+ },
196
+ {
197
+ "name": "ort_constant_64_window16",
198
+ "provenance": {
199
+ "source": "onnxruntime/test/providers/cpu/signal/signal_ops_test.cc",
200
+ "test": "SignalOpsTest.STFTFloat"
201
+ },
202
+ "inputs": {
203
+ "signal": { "dtype": "float32", "shape": [1, 64, 1], "data": { "kind": "constant", "value": 1.0 } },
204
+ "window": { "dtype": "float32", "shape": [16], "data": { "kind": "constant", "value": 1.0 } }
205
+ },
206
+ "outputs": {
207
+ "output": {
208
+ "dtype": "float32",
209
+ "shape": [1, 7, 9, 2],
210
+ "tolerance": 0.00002,
211
+ "data": {
212
+ "kind": "values",
213
+ "values": [16.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 16.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 16.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 16.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 16.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 16.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 16.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
214
+ }
215
+ }
216
+ },
217
+ "args": { "frameStep": 8 }
218
+ },
219
+ {
220
+ "name": "onnx_backend_arange128_cos_window16",
221
+ "provenance": { "source": "cmake/external/onnx/onnx/backend/test/data/node/test_stft_with_window" },
222
+ "inputs": {
223
+ "signal": {
224
+ "dtype": "float32",
225
+ "shape": [1, 128, 1],
226
+ "data": { "kind": "linspace", "start": 0.0, "end": 127.0 }
227
+ },
228
+ "window": {
229
+ "dtype": "float32",
230
+ "shape": [16],
231
+ "data": {
232
+ "kind": "values",
233
+ "values": { "$ref": "#/fixtureArrays/onnx_backend_arange128_cos_window16_input_window" }
234
+ }
235
+ }
236
+ },
237
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 15, 9, 2], "tolerance": 0.00025 } },
238
+ "args": { "frameStep": 8 }
239
+ },
240
+ {
241
+ "name": "real_batch2_twosided_stride2",
242
+ "attrs": { "onesided": 0 },
243
+ "inputs": {
244
+ "signal": {
245
+ "dtype": "float32",
246
+ "shape": [2, 5, 1],
247
+ "data": { "kind": "values", "values": [1.0, 2.0, 3.0, 4.0, 5.0, -1.0, 0.0, 1.0, 0.0, -1.0] }
248
+ },
249
+ "window": { "dtype": "float32", "shape": [3], "data": { "kind": "values", "values": [1.0, 0.5, 0.25] } }
250
+ },
251
+ "outputs": { "output": { "dtype": "float32", "shape": [2, 2, 3, 2], "tolerance": 0.00002 } },
252
+ "args": { "frameStep": 2 }
253
+ },
254
+ {
255
+ "name": "onnx_backend_stft_rectangular_window",
256
+ "provenance": {
257
+ "source": "cmake/external/onnx/onnx/backend/test/data/node/test_stft",
258
+ "notes": "Official rectangular-window fixture: window is omitted and frame_length supplies the DFT size."
259
+ },
260
+ "inputs": {
261
+ "signal": {
262
+ "dtype": "float32",
263
+ "shape": [1, 128, 1],
264
+ "data": {
265
+ "kind": "values",
266
+ "values": [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 21.0, 22.0, 23.0, 24.0, 25.0, 26.0, 27.0, 28.0, 29.0, 30.0, 31.0, 32.0, 33.0, 34.0, 35.0, 36.0, 37.0, 38.0, 39.0, 40.0, 41.0, 42.0, 43.0, 44.0, 45.0, 46.0, 47.0, 48.0, 49.0, 50.0, 51.0, 52.0, 53.0, 54.0, 55.0, 56.0, 57.0, 58.0, 59.0, 60.0, 61.0, 62.0, 63.0, 64.0, 65.0, 66.0, 67.0, 68.0, 69.0, 70.0, 71.0, 72.0, 73.0, 74.0, 75.0, 76.0, 77.0, 78.0, 79.0, 80.0, 81.0, 82.0, 83.0, 84.0, 85.0, 86.0, 87.0, 88.0, 89.0, 90.0, 91.0, 92.0, 93.0, 94.0, 95.0, 96.0, 97.0, 98.0, 99.0, 100.0, 101.0, 102.0, 103.0, 104.0, 105.0, 106.0, 107.0, 108.0, 109.0, 110.0, 111.0, 112.0, 113.0, 114.0, 115.0, 116.0, 117.0, 118.0, 119.0, 120.0, 121.0, 122.0, 123.0, 124.0, 125.0, 126.0, 127.0]
267
+ }
268
+ }
269
+ },
270
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 15, 9, 2], "tolerance": 0.00025 } },
271
+ "args": { "frameStep": 8, "frameLength": 16 }
272
+ },
273
+ {
274
+ "name": "ort_constant_64_window16_twosided",
275
+ "provenance": {
276
+ "source": "onnxruntime/test/providers/cpu/signal/signal_ops_test.cc",
277
+ "test": "SignalOpsTest.STFTFloat",
278
+ "notes": "Two-sided projection of ORT's constant-window STFT setup; validates the full spectrum instead of the default one-sided bins."
279
+ },
280
+ "attrs": { "onesided": 0 },
281
+ "inputs": {
282
+ "signal": { "dtype": "float32", "shape": [1, 64, 1], "data": { "kind": "constant", "value": 1.0 } },
283
+ "window": { "dtype": "float32", "shape": [16], "data": { "kind": "constant", "value": 1.0 } }
284
+ },
285
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 7, 16, 2], "tolerance": 0.00025 } },
286
+ "args": { "frameStep": 8 }
287
+ },
288
+ {
289
+ "name": "ort_frame_length_without_window",
290
+ "provenance": {
291
+ "source": "onnxruntime/test/providers/cpu/signal/signal_ops_test.cc",
292
+ "test": "SignalOpsTest.STFTFloat",
293
+ "notes": "Covers the ONNX optional-input form where frame_length is provided and the window input is omitted."
294
+ },
295
+ "inputs": {
296
+ "signal": {
297
+ "dtype": "float32",
298
+ "shape": [1, 6, 1],
299
+ "data": { "kind": "values", "values": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] }
300
+ }
301
+ },
302
+ "outputs": {
303
+ "output": {
304
+ "dtype": "float32",
305
+ "shape": [1, 2, 3, 2],
306
+ "tolerance": 0.00002,
307
+ "data": { "kind": "values", "values": [10.0, 0.0, -2.0, 2.0, -2.0, 0.0, 18.0, 0.0, -2.0, 2.0, -2.0, 0.0] }
308
+ }
309
+ },
310
+ "args": { "frameStep": 2, "frameLength": 4 }
311
+ },
312
+ {
313
+ "name": "ort_complex_input_batched_twosided_frame_length",
314
+ "provenance": {
315
+ "source": "onnxruntime/test/providers/cpu/signal/signal_ops_test.cc",
316
+ "test": "SignalOpsTest.STFTFloatComplexInputBatched",
317
+ "notes": "Compact optional-window projection of ORT's batched complex-input STFT regression."
318
+ },
319
+ "attrs": { "onesided": 0 },
320
+ "inputs": {
321
+ "signal": {
322
+ "dtype": "float32",
323
+ "shape": [2, 8, 2],
324
+ "data": {
325
+ "kind": "values",
326
+ "values": { "$ref": "#/fixtureArrays/ort_complex_input_batched_twosided_frame_length_input_signal" }
327
+ }
328
+ }
329
+ },
330
+ "outputs": {
331
+ "output": {
332
+ "dtype": "float32",
333
+ "shape": [2, 3, 4, 2],
334
+ "tolerance": 0.00002,
335
+ "data": {
336
+ "kind": "values",
337
+ "values": [4.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 4.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 4.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 396.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 396.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 396.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
338
+ }
339
+ }
340
+ },
341
+ "args": { "frameStep": 2, "frameLength": 4 }
342
+ },
343
+ {
344
+ "name": "fft_window256_signal1024_batch2_onesided",
345
+ "inputs": {
346
+ "signal": {
347
+ "dtype": "float32",
348
+ "shape": [2, 1024, 1],
349
+ "data": { "kind": "fillFloat32", "sinStep": 0.017, "cosStep": 0.029, "scale": 1.0 }
350
+ },
351
+ "window": {
352
+ "dtype": "float32",
353
+ "shape": [256],
354
+ "data": { "kind": "fillFloat32", "sinStep": 0.013, "cosStep": 0.011, "scale": 0.5, "offset": 0.5 }
355
+ }
356
+ },
357
+ "outputs": {
358
+ "output": { "dtype": "float32", "shape": [2, 4, 129, 2], "tolerance": 0.001, "relTolerance": 0.0001 }
359
+ },
360
+ "args": { "frameStep": 256 }
361
+ },
362
+ {
363
+ "name": "fft_window512_complex_twosided_step256",
364
+ "attrs": { "onesided": 0 },
365
+ "inputs": {
366
+ "signal": {
367
+ "dtype": "float32",
368
+ "shape": [1, 2048, 2],
369
+ "data": { "kind": "fillFloat32", "sinStep": 0.019, "cosStep": 0.023, "scale": 1.0 }
370
+ },
371
+ "window": {
372
+ "dtype": "float32",
373
+ "shape": [512],
374
+ "data": { "kind": "fillFloat32", "sinStep": 0.009, "cosStep": 0.007, "scale": 0.5, "offset": 0.5 }
375
+ }
376
+ },
377
+ "outputs": {
378
+ "output": { "dtype": "float32", "shape": [1, 7, 512, 2], "tolerance": 0.01, "relTolerance": 0.0001 }
379
+ },
380
+ "args": { "frameStep": 256 }
381
+ },
382
+ {
383
+ "name": "ort_complex_batched_no_window_frame_length",
384
+ "provenance": {
385
+ "source": "onnxruntime/test/providers/cpu/signal/signal_ops_test.cc",
386
+ "test": "SignalOpsTest.STFTFloatComplexInputBatched",
387
+ "notes": "ORT's optional-window path uses frame_length as the rectangular-window length. This case locks the optimized no-window FFT route for batched complex input."
388
+ },
389
+ "attrs": { "onesided": 0 },
390
+ "inputs": {
391
+ "signal": {
392
+ "dtype": "float32",
393
+ "shape": [2, 8, 2],
394
+ "data": {
395
+ "kind": "values",
396
+ "values": { "$ref": "#/fixtureArrays/ort_complex_input_batched_twosided_frame_length_input_signal" }
397
+ }
398
+ }
399
+ },
400
+ "outputs": { "output": { "dtype": "float32", "shape": [2, 3, 4, 2], "tolerance": 0.00005 } },
401
+ "args": { "frameStep": 2, "frameLength": 4 }
402
+ },
403
+ {
404
+ "name": "ort_complex_batched_frame_offsets_twosided",
405
+ "provenance": {
406
+ "source": "onnxruntime/test/providers/cpu/signal/signal_ops_test.cc",
407
+ "test": "SignalOpsTest.STFTFloatComplexInputBatched",
408
+ "notes": "Compact projection of ORT's batched complex-input frame-offset regression."
409
+ },
410
+ "attrs": { "onesided": 0 },
411
+ "inputs": {
412
+ "signal": {
413
+ "dtype": "float32",
414
+ "shape": [2, 8, 2],
415
+ "data": {
416
+ "kind": "values",
417
+ "values": { "$ref": "#/fixtureArrays/ort_complex_input_batched_twosided_frame_length_input_signal" }
418
+ }
419
+ },
420
+ "window": { "dtype": "float32", "shape": [4], "data": { "kind": "constant", "value": 1.0 } }
421
+ },
422
+ "outputs": { "output": { "dtype": "float32", "shape": [2, 3, 4, 2], "tolerance": 0.00005 } },
423
+ "args": { "frameStep": 2 }
424
+ },
425
+ {
426
+ "name": "fft_window512_real_twosided_step256",
427
+ "provenance": {
428
+ "notes": "Real/full-spectrum coverage for the power-of-two FFT path; complements complex/twosided N=512 and real/onesided N=1024 fixtures."
429
+ },
430
+ "attrs": { "onesided": 0 },
431
+ "inputs": {
432
+ "signal": {
433
+ "dtype": "float32",
434
+ "shape": [1, 1024, 1],
435
+ "data": { "kind": "fillFloat32", "sinStep": 0.017, "cosStep": 0.031, "scale": 1.0 }
436
+ },
437
+ "window": {
438
+ "dtype": "float32",
439
+ "shape": [512],
440
+ "data": { "kind": "fillFloat32", "sinStep": 0.007, "cosStep": 0.013, "scale": 0.5, "offset": 0.5 }
441
+ }
442
+ },
443
+ "outputs": {
444
+ "output": { "dtype": "float32", "shape": [1, 3, 512, 2], "tolerance": 0.01, "relTolerance": 0.0001 }
445
+ },
446
+ "args": { "frameStep": 256 }
447
+ },
448
+ {
449
+ "name": "fft_window1024_onesided_step512",
450
+ "inputs": {
451
+ "signal": {
452
+ "dtype": "float32",
453
+ "shape": [1, 4096, 1],
454
+ "data": { "kind": "fillFloat32", "sinStep": 0.011, "cosStep": 0.027, "scale": 1.0 }
455
+ },
456
+ "window": {
457
+ "dtype": "float32",
458
+ "shape": [1024],
459
+ "data": { "kind": "fillFloat32", "sinStep": 0.005, "cosStep": 0.003, "scale": 0.5, "offset": 0.5 }
460
+ }
461
+ },
462
+ "outputs": {
463
+ "output": { "dtype": "float32", "shape": [1, 7, 513, 2], "tolerance": 0.004, "relTolerance": 0.0001 }
464
+ },
465
+ "args": { "frameStep": 512 }
466
+ },
467
+ {
468
+ "name": "fft_window4096_radix2_dit_onesided",
469
+ "provenance": {
470
+ "notes": "Locks the capability-selected 4096-point radix-2 DIT routes: workgroup-resident on devices with 32 KiB of shared storage, and globally staged/storage-backed on lower-limit devices."
471
+ },
472
+ "inputs": {
473
+ "signal": {
474
+ "dtype": "float32",
475
+ "shape": [1, 4096, 1],
476
+ "data": { "kind": "fillFloat32", "sinStep": 0.011, "cosStep": 0.027, "scale": 1.0 }
477
+ },
478
+ "window": {
479
+ "dtype": "float32",
480
+ "shape": [4096],
481
+ "data": { "kind": "fillFloat32", "sinStep": 0.005, "cosStep": 0.003, "scale": 0.5, "offset": 0.5 }
482
+ }
483
+ },
484
+ "outputs": {
485
+ "output": { "dtype": "float32", "shape": [1, 1, 2049, 2], "tolerance": 0.04, "relTolerance": 0.0002 }
486
+ },
487
+ "args": { "frameStep": 4096 }
488
+ },
489
+ {
490
+ "name": "fft_window2048_radix2_dit_onesided_min_storage",
491
+ "provenance": {
492
+ "notes": "The in-place radix-2 rung is selected inside a BAND — rank3WindowInplaceFftFits wants window*16 > maxComputeWorkgroupStorageSize (the two-buffer form does not fit) and window*8 <= it (the in-place one does) — so the band moves with the device and no single window covers every tier. window 4096 is the band at 32768 (the adapter here); 2048 is the band at the 16384 WebGPU guaranteed minimum, which is what the min-limits tier reports. Without this sibling the rung is rendered on every low-storage tier and executed on none of them. Deliberately no requires.limits: outside its band the case is still valid, it just selects the staged rung."
493
+ },
494
+ "inputs": {
495
+ "signal": {
496
+ "dtype": "float32",
497
+ "shape": [1, 2048, 1],
498
+ "data": { "kind": "fillFloat32", "sinStep": 0.011, "cosStep": 0.027, "scale": 1.0 }
499
+ },
500
+ "window": {
501
+ "dtype": "float32",
502
+ "shape": [2048],
503
+ "data": { "kind": "fillFloat32", "sinStep": 0.005, "cosStep": 0.003, "scale": 0.5, "offset": 0.5 }
504
+ }
505
+ },
506
+ "outputs": {
507
+ "output": { "dtype": "float32", "shape": [1, 1, 1025, 2], "tolerance": 0.04, "relTolerance": 0.0002 }
508
+ },
509
+ "args": { "frameStep": 2048 }
510
+ },
511
+ {
512
+ "name": "fft_window4096_radix2_dit_complex_twosided",
513
+ "provenance": {
514
+ "notes": "Exercises the complex load and full-spectrum store arms shared by the capability-selected 4096-point DIT routes."
515
+ },
516
+ "attrs": { "onesided": 0 },
517
+ "inputs": {
518
+ "signal": {
519
+ "dtype": "float32",
520
+ "shape": [1, 4096, 2],
521
+ "data": { "kind": "fillFloat32", "sinStep": 0.011, "cosStep": 0.027, "scale": 0.2 }
522
+ },
523
+ "window": {
524
+ "dtype": "float32",
525
+ "shape": [4096],
526
+ "data": { "kind": "fillFloat32", "sinStep": 0.005, "cosStep": 0.003, "scale": 0.5, "offset": 0.5 }
527
+ }
528
+ },
529
+ "outputs": {
530
+ "output": { "dtype": "float32", "shape": [1, 1, 4096, 2], "tolerance": 0.05, "relTolerance": 0.0003 }
531
+ },
532
+ "args": { "frameStep": 4096 }
533
+ },
534
+ {
535
+ "name": "empty_zero_dim",
536
+ "attrs": { "onesided": 1 },
537
+ "inputs": {
538
+ "signal": { "dtype": "float32", "shape": [0, 6, 1], "data": { "kind": "values", "values": [] } },
539
+ "window": { "dtype": "float32", "shape": [4], "data": { "kind": "values", "values": [0.5, 1.0, 1.0, 0.5] } }
540
+ },
541
+ "outputs": { "output": { "dtype": "float32", "shape": [0, 2, 3, 2], "tolerance": 0 } },
542
+ "args": { "frameStep": 2 }
543
+ },
544
+ {
545
+ "name": "mixed_stockham_n400_real_onesided_tail25",
546
+ "provenance": {
547
+ "notes": "Real onesided 400-point mixed-radix path: 16-point shared Stockham transforms over 25 residue classes, followed by the direct radix-25 tail."
548
+ },
549
+ "attrs": { "onesided": 1 },
550
+ "inputs": {
551
+ "signal": {
552
+ "dtype": "float32",
553
+ "shape": [1, 880, 1],
554
+ "data": { "kind": "fillFloat32", "sinStep": 0.021, "cosStep": 0.013, "scale": 1.0 }
555
+ },
556
+ "window": {
557
+ "dtype": "float32",
558
+ "shape": [400],
559
+ "data": { "kind": "fillFloat32", "sinStep": 0.0079, "cosStep": 0.0079, "scale": 0.5 }
560
+ }
561
+ },
562
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 4, 201, 2], "tolerance": 0.01, "relTolerance": 0.001 } },
563
+ "args": { "frameStep": 160 }
564
+ },
565
+ {
566
+ "name": "mixed_stockham_n288_real_onesided_tail9",
567
+ "provenance": {
568
+ "notes": "Generic mixed-radix recurrence with a 32-point shared Stockham factor and radix-9 tail."
569
+ },
570
+ "attrs": { "onesided": 1 },
571
+ "inputs": {
572
+ "signal": {
573
+ "dtype": "float32",
574
+ "shape": [1, 432, 1],
575
+ "data": { "kind": "fillFloat32", "sinStep": 0.023, "cosStep": 0.017, "scale": 0.7 }
576
+ },
577
+ "window": {
578
+ "dtype": "float32",
579
+ "shape": [288],
580
+ "data": { "kind": "fillFloat32", "sinStep": 0.011, "cosStep": 0.007, "scale": 0.5, "offset": 0.5 }
581
+ }
582
+ },
583
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 2, 145, 2], "tolerance": 0.02, "relTolerance": 0.001 } },
584
+ "args": { "frameStep": 144 }
585
+ },
586
+ {
587
+ "name": "mixed_stockham_n400_complex_twosided",
588
+ "provenance": { "notes": "Complex/full-spectrum branch of the 16x25 mixed-radix decomposition." },
589
+ "attrs": { "onesided": 0 },
590
+ "inputs": {
591
+ "signal": {
592
+ "dtype": "float32",
593
+ "shape": [1, 400, 2],
594
+ "data": { "kind": "fillFloat32", "sinStep": 0.017, "cosStep": 0.031, "scale": 0.3 }
595
+ },
596
+ "window": {
597
+ "dtype": "float32",
598
+ "shape": [400],
599
+ "data": { "kind": "fillFloat32", "sinStep": 0.0079, "cosStep": 0.0079, "scale": 0.5 }
600
+ }
601
+ },
602
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 1, 400, 2], "tolerance": 0.02, "relTolerance": 0.001 } },
603
+ "args": { "frameStep": 400 }
604
+ },
605
+ {
606
+ "name": "mixed_stockham_n480_real_twosided_tail15",
607
+ "provenance": {
608
+ "notes": "Real full-spectrum 480-point mixed-radix path: 32-point shared Stockham transforms over 15 residue classes, followed by the direct radix-15 tail."
609
+ },
610
+ "attrs": { "onesided": 0 },
611
+ "inputs": {
612
+ "signal": {
613
+ "dtype": "float32",
614
+ "shape": [1, 960, 1],
615
+ "data": { "kind": "fillFloat32", "sinStep": 0.017, "cosStep": 0.029, "scale": 1.0 }
616
+ },
617
+ "window": {
618
+ "dtype": "float32",
619
+ "shape": [480],
620
+ "data": { "kind": "fillFloat32", "sinStep": 0.0065, "cosStep": 0.0065, "scale": 0.5 }
621
+ }
622
+ },
623
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 3, 480, 2], "tolerance": 0.02, "relTolerance": 0.001 } },
624
+ "args": { "frameStep": 240 }
625
+ },
626
+ {
627
+ "name": "mixed_stockham_n496_real_onesided_tail31",
628
+ "provenance": {
629
+ "notes": "Generic mixed-radix recurrence at the supported tail bound: a 16-point shared Stockham factor and radix-31 tail."
630
+ },
631
+ "attrs": { "onesided": 1 },
632
+ "inputs": {
633
+ "signal": {
634
+ "dtype": "float32",
635
+ "shape": [1, 744, 1],
636
+ "data": { "kind": "fillFloat32", "sinStep": 0.019, "cosStep": 0.031, "scale": 0.6 }
637
+ },
638
+ "window": {
639
+ "dtype": "float32",
640
+ "shape": [496],
641
+ "data": { "kind": "fillFloat32", "sinStep": 0.009, "cosStep": 0.005, "scale": 0.5, "offset": 0.5 }
642
+ }
643
+ },
644
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 2, 249, 2], "tolerance": 0.02, "relTolerance": 0.001 } },
645
+ "args": { "frameStep": 248 }
646
+ },
647
+ {
648
+ "name": "mixed_stockham_n132_real_onesided_tail33",
649
+ "provenance": {
650
+ "notes": "First tail past the unanchored bound: a 4-point shared Stockham factor and radix-33 tail, so the tail walk re-anchors exactly once."
651
+ },
652
+ "attrs": { "onesided": 1 },
653
+ "inputs": {
654
+ "signal": {
655
+ "dtype": "float32",
656
+ "shape": [1, 198, 1],
657
+ "data": { "kind": "fillFloat32", "sinStep": 0.019, "cosStep": 0.031, "scale": 0.6 }
658
+ },
659
+ "window": {
660
+ "dtype": "float32",
661
+ "shape": [132],
662
+ "data": { "kind": "fillFloat32", "sinStep": 0.031, "cosStep": 0.017, "scale": 0.5, "offset": 0.5 }
663
+ }
664
+ },
665
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 2, 67, 2], "tolerance": 0.02, "relTolerance": 0.001 } },
666
+ "args": { "frameStep": 66 }
667
+ },
668
+ {
669
+ "name": "window_and_frame_length_copresent_fft",
670
+ "inputs": {
671
+ "signal": { "dtype": "float32", "shape": [1, 64, 1], "data": { "kind": "constant", "value": 1.0 } },
672
+ "window": {
673
+ "dtype": "float32",
674
+ "shape": [16],
675
+ "data": {
676
+ "kind": "values",
677
+ "values": { "$ref": "#/fixtureArrays/onnx_backend_arange128_cos_window16_input_window" }
678
+ }
679
+ }
680
+ },
681
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 7, 9, 2], "tolerance": 0.0002 } },
682
+ "args": { "frameStep": 8, "frameLength": 16 }
683
+ },
684
+ {
685
+ "name": "mixed_stockham_n1000_real_twosided_tail125",
686
+ "provenance": {
687
+ "notes": "Window length 1000 factors as 8 x 125 and exercises the shared Stockham mixed-radix route after the tail-bound transition. Odd lengths 513 and 777 retain independent tiled-real coverage."
688
+ },
689
+ "attrs": { "onesided": 0 },
690
+ "inputs": {
691
+ "signal": {
692
+ "dtype": "float32",
693
+ "shape": [1, 2000, 1],
694
+ "data": { "kind": "fillFloat32", "sinStep": 0.0091, "cosStep": 0.0071, "scale": 1.0 }
695
+ },
696
+ "window": {
697
+ "dtype": "float32",
698
+ "shape": [1000],
699
+ "data": { "kind": "fillFloat32", "sinStep": 0.0031, "cosStep": 0.0031, "scale": 0.5 }
700
+ }
701
+ },
702
+ "outputs": {
703
+ "output": { "dtype": "float32", "shape": [1, 2, 1000, 2], "tolerance": 0.05, "relTolerance": 0.002 }
704
+ },
705
+ "args": { "frameStep": 1000 }
706
+ },
707
+ {
708
+ "name": "fft_pow2_win8_drops_partial_last_frame",
709
+ "attrs": { "onesided": 1 },
710
+ "inputs": {
711
+ "signal": {
712
+ "dtype": "float32",
713
+ "shape": [1, 12, 1],
714
+ "data": { "kind": "values", "values": [1.0, -2.0, 3.0, -4.0, 5.0, -6.0, 7.0, -8.0, 9.0, -10.0, 11.0, -12.0] }
715
+ },
716
+ "window": {
717
+ "dtype": "float32",
718
+ "shape": [8],
719
+ "data": { "kind": "values", "values": [1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3] }
720
+ }
721
+ },
722
+ "outputs": {
723
+ "output": { "dtype": "float32", "shape": [1, 1, 5, 2], "tolerance": 0.0002, "relTolerance": 0.0001 }
724
+ },
725
+ "args": { "frameStep": 8 },
726
+ "provenance": {
727
+ "notes": "The final four samples cannot fill an eight-sample frame, so ONNX floor framing emits only the complete frame at offset zero."
728
+ }
729
+ },
730
+ {
731
+ "name": "fft_pow2_win4_complex_twosided_drops_partial_last_frame",
732
+ "attrs": { "onesided": 0 },
733
+ "inputs": {
734
+ "signal": {
735
+ "dtype": "float32",
736
+ "shape": [1, 6, 2],
737
+ "data": { "kind": "values", "values": [1.0, 0.5, 2.0, -0.5, 3.0, 1.0, -4.0, 0.0, 5.0, -2.0, 6.0, 0.25] }
738
+ },
739
+ "window": { "dtype": "float32", "shape": [4], "data": { "kind": "values", "values": [1.0, 0.75, 0.5, 0.25] } }
740
+ },
741
+ "outputs": {
742
+ "output": { "dtype": "float32", "shape": [1, 1, 4, 2], "tolerance": 0.0002, "relTolerance": 0.0001 }
743
+ },
744
+ "args": { "frameStep": 4 },
745
+ "provenance": {
746
+ "notes": "The frame at offset four would extend past the six-sample signal, so ONNX floor framing emits only the complete frame at offset zero."
747
+ }
748
+ },
749
+ {
750
+ "name": "fft_pow2_win16_window_with_interior_zeros",
751
+ "attrs": { "onesided": 1 },
752
+ "inputs": {
753
+ "signal": { "dtype": "float32", "shape": [1, 32, 1], "data": { "kind": "linspace", "start": 0.0, "end": 31.0 } },
754
+ "window": {
755
+ "dtype": "float32",
756
+ "shape": [16],
757
+ "data": {
758
+ "kind": "values",
759
+ "values": [0.0, 0.25, 0.5, 0.75, 1.0, 0.75, 0.5, 0.0, 0.0, 0.5, 0.75, 1.0, 0.75, 0.5, 0.25, 0.0]
760
+ }
761
+ }
762
+ },
763
+ "outputs": {
764
+ "output": { "dtype": "float32", "shape": [1, 3, 9, 2], "tolerance": 0.0002, "relTolerance": 0.0001 }
765
+ },
766
+ "args": { "frameStep": 8 }
767
+ },
768
+ {
769
+ "name": "dispatch_fold_fft_batch2_frames32800_win4_onesided",
770
+ "inputs": {
771
+ "signal": {
772
+ "dtype": "float32",
773
+ "shape": [2, 32803, 1],
774
+ "data": { "kind": "fillFloat32", "sinStep": 0.017, "cosStep": 0.029, "scale": 1.0 }
775
+ },
776
+ "window": { "dtype": "float32", "shape": [4], "data": { "kind": "values", "values": [0.5, 1.0, 1.0, 0.5] } }
777
+ },
778
+ "outputs": {
779
+ "output": { "dtype": "float32", "shape": [2, 32800, 3, 2], "tolerance": 0.001, "relTolerance": 0.0001 }
780
+ },
781
+ "args": { "frameStep": 1 }
782
+ },
783
+ {
784
+ "name": "rank3_window_win513_nonpow2_onesided_compact",
785
+ "provenance": {
786
+ "notes": "Compact selector and correctness lock for the first non-power-of-two window above 512. Three frames exercise frame addressing; 257 bins exercise the partial second 256-frequency block."
787
+ },
788
+ "attrs": { "onesided": 1 },
789
+ "inputs": {
790
+ "signal": {
791
+ "dtype": "float32",
792
+ "shape": [1, 1025, 1],
793
+ "data": { "kind": "fillFloat32", "sinStep": 0.0091, "cosStep": 0.0071, "scale": 1.0 }
794
+ },
795
+ "window": {
796
+ "dtype": "float32",
797
+ "shape": [513],
798
+ "data": { "kind": "fillFloat32", "sinStep": 0.0061, "cosStep": 0.0061, "scale": 0.5 }
799
+ }
800
+ },
801
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 3, 257, 2], "tolerance": 0.02, "relTolerance": 0.001 } },
802
+ "args": { "frameStep": 256 }
803
+ },
804
+ {
805
+ "name": "rank3_window_win777_tiled_real_generic",
806
+ "provenance": { "notes": "Non-513 correctness lock for the storage-bounded tiled real STFT path." },
807
+ "attrs": { "onesided": 1 },
808
+ "inputs": {
809
+ "signal": {
810
+ "dtype": "float32",
811
+ "shape": [1, 777, 1],
812
+ "data": { "kind": "fillFloat32", "sinStep": 0.0067, "cosStep": 0.0091, "scale": 0.5 }
813
+ },
814
+ "window": {
815
+ "dtype": "float32",
816
+ "shape": [777],
817
+ "data": { "kind": "fillFloat32", "sinStep": 0.0041, "cosStep": 0.0051, "scale": 0.5 }
818
+ }
819
+ },
820
+ "outputs": { "output": { "dtype": "float32", "shape": [1, 1, 389, 2], "tolerance": 0.04, "relTolerance": 0.002 } },
821
+ "args": { "frameStep": 777 }
822
+ },
823
+ {
824
+ "name": "rank3_window_fft8192_storage_generic",
825
+ "provenance": {
826
+ "notes": "Non-4096 correctness lock for the geometry-derived large radix-2 FFT; device storage capacity selects the single-dispatch or globally staged implementation."
827
+ },
828
+ "attrs": { "onesided": 1 },
829
+ "inputs": {
830
+ "signal": {
831
+ "dtype": "float32",
832
+ "shape": [1, 8192, 1],
833
+ "data": { "kind": "fillFloat32", "sinStep": 0.0045, "cosStep": 0.0085, "scale": 0.25 }
834
+ },
835
+ "window": {
836
+ "dtype": "float32",
837
+ "shape": [8192],
838
+ "data": { "kind": "fillFloat32", "sinStep": 0.0025, "cosStep": 0.0015, "scale": 0.5, "offset": 0.5 }
839
+ }
840
+ },
841
+ "outputs": {
842
+ "output": { "dtype": "float32", "shape": [1, 1, 4097, 2], "tolerance": 0.08, "relTolerance": 0.0005 }
843
+ },
844
+ "args": { "frameStep": 8192 }
845
+ }
846
+ ]
847
+ }