1{
2 "instrawave": 1,
3 "name": "dulcimer",
4 "description": "One plucked string over a rigid box with a sound hole, radiating along its line and driving a bridge patch above the plate.",
5 "params": [
6 {
7 "key": "nx",
8 "label": "air cells along x",
9 "unit": "1",
10 "value": 96,
11 "min": 32,
12 "max": 256,
13 "class": "accuracy",
14 "hint": "The air grid; ny = nz = nx/2. The simulation is honest to about c/(8h). Must be even.",
15 "step": 2
16 },
17 {
18 "key": "Lx",
19 "label": "domain length",
20 "unit": "m",
21 "value": 1,
22 "min": 0.6,
23 "max": 2,
24 "class": "geometry",
25 "hint": "The air box around the instrument, twice as long in x as in y and z."
26 },
27 {
28 "key": "ir_seconds",
29 "label": "room response length",
30 "unit": "s",
31 "value": 0.5,
32 "min": 0.1,
33 "max": 2,
34 "class": "accuracy",
35 "hint": "How long the recorded room responses run; a room still ringing past this is truncated."
36 },
37 {
38 "key": "L1",
39 "label": "speaking length",
40 "unit": "m",
41 "value": 0.6,
42 "min": 0.3,
43 "max": 0.9,
44 "class": "geometry"
45 },
46 {
47 "key": "oy",
48 "label": "across",
49 "unit": "m",
50 "value": 0,
51 "min": -0.15,
52 "max": 0.15,
53 "class": "geometry"
54 },
55 {
56 "key": "oz",
57 "label": "height",
58 "unit": "m",
59 "value": 0.02,
60 "min": 0.005,
61 "max": 0.15,
62 "class": "geometry",
63 "hint": "Height of the string above z = 0."
64 },
65 {
66 "key": "boxl",
67 "label": "box length",
68 "unit": "m",
69 "value": 0.66,
70 "min": 0.3,
71 "max": 0.9,
72 "class": "geometry"
73 },
74 {
75 "key": "boxw",
76 "label": "box width",
77 "unit": "m",
78 "value": 0.16,
79 "min": 0.08,
80 "max": 0.4,
81 "class": "geometry"
82 },
83 {
84 "key": "boxd",
85 "label": "box depth",
86 "unit": "m",
87 "value": 0.08,
88 "min": 0.03,
89 "max": 0.25,
90 "class": "geometry",
91 "hint": "How far the box hangs below its top plate at z = 0."
92 },
93 {
94 "key": "thick",
95 "label": "wall thickness",
96 "unit": "m",
97 "value": 0.016,
98 "min": 0.008,
99 "max": 0.05,
100 "class": "geometry"
101 },
102 {
103 "key": "holer",
104 "label": "sound hole radius",
105 "unit": "m",
106 "value": 0.035,
107 "min": 0,
108 "max": 0.12,
109 "class": "geometry",
110 "hint": "0 seals the box; the hole is what lets the cavity speak."
111 },
112 {
113 "key": "holex",
114 "label": "sound hole x",
115 "unit": "m",
116 "value": -0.05,
117 "min": -0.3,
118 "max": 0.3,
119 "class": "geometry"
120 },
121 {
122 "key": "absorb",
123 "label": "surface absorption",
124 "unit": "1/s",
125 "value": 800,
126 "min": 0,
127 "max": 4000,
128 "class": "geometry"
129 },
130 {
131 "key": "mic_x",
132 "label": "microphone x",
133 "unit": "m",
134 "value": 0.12,
135 "min": -0.45,
136 "max": 0.45,
137 "class": "geometry"
138 },
139 {
140 "key": "mic_y",
141 "label": "microphone y",
142 "unit": "m",
143 "value": 0.08,
144 "min": -0.2,
145 "max": 0.2,
146 "class": "geometry"
147 },
148 {
149 "key": "mic_z",
150 "label": "microphone z",
151 "unit": "m",
152 "value": 0.1,
153 "min": -0.2,
154 "max": 0.2,
155 "class": "geometry"
156 },
157 {
158 "key": "f0",
159 "label": "pitch",
160 "unit": "Hz",
161 "value": 294,
162 "min": 80,
163 "max": 880,
164 "class": "tuning",
165 "hint": "The fundamental; the wave speed follows, cs = 2 L f0."
166 },
167 {
168 "key": "B",
169 "label": "inharmonicity",
170 "unit": "1",
171 "value": 0.0001,
172 "min": 0,
173 "max": 0.002,
174 "class": "tuning",
175 "hint": "Bending stiffness as the inharmonicity coefficient."
176 },
177 {
178 "key": "t60",
179 "label": "decay t60",
180 "unit": "s",
181 "value": 4,
182 "min": 0.2,
183 "max": 8,
184 "class": "tuning"
185 },
186 {
187 "key": "sig1",
188 "label": "brightness decay",
189 "unit": "m^2/s",
190 "value": 0.005,
191 "min": 0,
192 "max": 0.05,
193 "class": "tuning"
194 },
195 {
196 "key": "pluckpos",
197 "label": "pluck position",
198 "unit": "1",
199 "value": 0.22,
200 "min": 0.08,
201 "max": 0.92,
202 "class": "tuning"
203 },
204 {
205 "key": "amp",
206 "label": "pluck height",
207 "unit": "m",
208 "value": 0.002,
209 "min": 0.0002,
210 "max": 0.005,
211 "class": "tuning"
212 },
213 {
214 "key": "gair",
215 "label": "line radiation gain",
216 "unit": "1",
217 "value": 0.3,
218 "min": 0,
219 "max": 2,
220 "class": "tuning"
221 },
222 {
223 "key": "gbridge",
224 "label": "bridge drive gain",
225 "unit": "1",
226 "value": 1,
227 "min": 0,
228 "max": 2,
229 "class": "tuning",
230 "hint": "The pull at the far end driving a patch above the plate."
231 }
232 ],
233 "derived": {
234 "c0": "343",
235 "cfl": "0.5",
236 "ny": "nx / 2",
237 "nz": "nx / 2",
238 "npts": "nx * ny * nz",
239 "h": "Lx / nx",
240 "Ly": "ny * h",
241 "Lz": "nz * h",
242 "dt": "cfl * h / (sqrt(3) * c0)",
243 "ir_steps": "max(1, round(ir_seconds / dt))",
244 "mic_index": "clamp(round((mic_x + Lx/2) / h - 0.5), 0, nx - 1) + nx * (clamp(round((mic_y + Ly/2) / h - 0.5), 0, ny - 1) + ny * clamp(round((mic_z + Lz/2) / h - 0.5), 0, nz - 1))",
245 "cs_worst": "2 * L1 * f0__max",
246 "kap2_worst": "B__max * (cs_worst * cs_worst * L1 * L1) / (pi * pi)",
247 "a_worst": "cs_worst*cs_worst*dt*dt + 4*sig1__max*dt",
248 "h_min": "sqrt((a_worst + sqrt(a_worst*a_worst + 16*kap2_worst*dt*dt)) / 2) / 0.95",
249 "n1": "max(5, min(1024, floor(L1 / h_min) + 1))",
250 "h1": "L1 / (n1 - 1)",
251 "mx": "n1 - 2",
252 "n_rows": "mx + 1"
253 },
254 "shaders": {
255 "medium": {
256 "code": "// The medium: the body's rigid mask, the absorption, and where the object\n// couples into the air. One thread per air cell.\nconst PI = 3.14159265358979;\noverride npts: u32;\noverride nx: u32;\noverride ny: u32;\noverride h: f32;\noverride Lx: f32;\noverride Ly: f32;\noverride Lz: f32;\noverride boxl: f32;\noverride boxw: f32;\noverride boxd: f32;\noverride thick: f32;\noverride holer: f32;\noverride holex: f32;\noverride absorb: f32;\noverride oy: f32;\noverride oz: f32;\noverride L1: f32;\n@group(0) @binding(0) var<storage, read_write> wall: array<f32>;\n@group(0) @binding(1) var<storage, read_write> sig: array<f32>;\n@group(0) @binding(2) var<storage, read_write> prof: array<f32>;\n@group(0) @binding(3) var<storage, read_write> bprof: array<f32>;\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x + gid.y * 4194240u;\n if (i >= npts) { return; }\n let ix = i % nx;\n let iy = (i / nx) % ny;\n let iz = i / (nx * ny);\n let x = -Lx/2.0 + (f32(ix) + 0.5) * h;\n let y = -Ly/2.0 + (f32(iy) + 0.5) * h;\n let z = -Lz/2.0 + (f32(iz) + 0.5) * h;\n let sw = 0.45 * h;\n // The box: the outer solid minus its cavity, top plate at z = 0, with a\n // round sound hole cut out of the top plate only.\n let box_ox = 0.5 * (1.0 - tanh((abs(x) - boxl/2.0) / sw));\n let box_oy = 0.5 * (1.0 - tanh((abs(y) - boxw/2.0) / sw));\n let box_oz = 0.5 * (1.0 - tanh((abs(z + boxd/2.0) - boxd/2.0) / sw));\n let box_ix = 0.5 * (1.0 - tanh((abs(x) - (boxl/2.0 - thick)) / sw));\n let box_iy = 0.5 * (1.0 - tanh((abs(y) - (boxw/2.0 - thick)) / sw));\n let box_iz = 0.5 * (1.0 - tanh((abs(z + boxd/2.0) - (boxd/2.0 - thick)) / sw));\n var shell = max(0.0, box_ox * box_oy * box_oz - box_ix * box_iy * box_iz);\n let hole_r = sqrt((x - holex)*(x - holex) + y*y);\n let hole = 0.5 * (1.0 - tanh((hole_r - holer) / sw));\n let top = 0.5 * (1.0 - tanh((abs(z + thick/2.0) - thick/2.0) / sw));\n shell = max(0.0, shell - hole * top);\n let w = 1.0 - shell;\n wall[i] = w;\n // Open boundary at the domain edge (a quadratic absorbing ramp over the\n // outer 0.15 Ly of each face), plus the body's lossy surface skin.\n let spw = 0.15 * Ly;\n let dx = max(0.0, spw - (Lx/2.0 - abs(x))) / spw;\n let dy = max(0.0, spw - (Ly/2.0 - abs(y))) / spw;\n let dz = max(0.0, spw - (Lz/2.0 - abs(z))) / spw;\n let d = max(max(dx, dy), dz);\n sig[i] = 9000.0 * d * d + absorb * (4.0 * shell * (1.0 - shell));\n // The object radiates from its line: a tube two cells wide around\n // (y = oy, z = oz), fading at its ends, cross-section normalized to one.\n let aw = 2.0 * h;\n let tube = exp(-((y - oy)*(y - oy) + (z - oz)*(z - oz)) / (aw*aw)) / (PI * aw * aw);\n let span = 0.5 * (1.0 + tanh((L1/2.0 - abs(x)) / (1.5*h)));\n prof[i] = tube * span * w;\n // ...and its bridge force drives a patch of air just above the plate,\n // around the x = +L/2 end (a stand-in for a moving top plate).\n let pw = 1.5 * h;\n let pr = exp(-((x - L1/2.0)*(x - L1/2.0) + (y - oy)*(y - oy)) / (0.04*0.04)) / (PI * 0.04 * 0.04);\n let skin = exp(-(z - pw)*(z - pw) / (pw*pw)) / (sqrt(PI) * pw);\n bprof[i] = pr * skin * w;\n}\n",
257 "constants": [
258 "npts",
259 "nx",
260 "ny",
261 "h",
262 "Lx",
263 "Ly",
264 "Lz",
265 "boxl",
266 "boxw",
267 "boxd",
268 "thick",
269 "holer",
270 "holex",
271 "absorb",
272 "oy",
273 "oz",
274 "L1"
275 ]
276 },
277 "sines_x": {
278 "code": "// Sine mode m of the object grid, interpolated to air\n// x centers: sx[(mode-1) * nx + ix]. One thread per table entry.\nconst PI = 3.14159265358979;\noverride n1: u32;\noverride mx: u32;\noverride nx: u32;\noverride L1: f32;\noverride h1: f32;\noverride Lx: f32;\noverride h: f32;\n@group(0) @binding(0) var<storage, read_write> table: array<f32>;\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x + gid.y * 4194240u;\n if (i >= mx * nx) { return; }\n let mi = i % mx;\n let ia = i / mx;\n let xw = -Lx/2.0 + (f32(ia) + 0.5) * h;\n let u = (xw + L1/2.0) / h1;\n let j = i32(floor(u));\n var v = 0.0;\n if (j >= 0 && j < i32(n1) - 1) {\n let f = u - f32(j);\n let mm = f32(mi + 1u);\n let s0 = sin(mm * PI * f32(j) / f32(n1 - 1u));\n let s1 = sin(mm * PI * f32(j + 1) / f32(n1 - 1u));\n v = (1.0 - f) * s0 + f * s1;\n }\n table[mi * nx + ia] = v;\n}\n",
279 "constants": [
280 "n1",
281 "mx",
282 "nx",
283 "L1",
284 "h1",
285 "Lx",
286 "h"
287 ]
288 },
289 "impulse": {
290 "code": "// The reciprocal kick: one unit impulse at the microphone cell, entering\n// the way a source enters the first step. One thread.\noverride mic_index: u32;\noverride dt: f32;\n@group(0) @binding(0) var<storage, read_write> p: array<f32>;\n@group(0) @binding(1) var<storage, read> wall: array<f32>;\n@group(0) @binding(2) var<storage, read> sig: array<f32>;\n@compute @workgroup_size(1)\nfn main() {\n p[mic_index] = dt * dt * wall[mic_index] / (1.0 + sig[mic_index] * dt);\n}\n",
291 "constants": [
292 "mic_index",
293 "dt"
294 ]
295 },
296 "step": {
297 "code": "// One leapfrog step of the air: the wall-masked Laplacian (each stencil\n// face scaled by the mask at the neighbour it reads, the rigid condition in\n// divergence form) fused with the update. One thread per air cell.\noverride npts: u32;\noverride nx: u32;\noverride ny: u32;\noverride nz: u32;\noverride h: f32;\noverride dt: f32;\noverride c0: f32;\n@group(0) @binding(0) var<storage, read> p: array<f32>;\n@group(0) @binding(1) var<storage, read> pm: array<f32>;\n@group(0) @binding(2) var<storage, read_write> pn: array<f32>;\n@group(0) @binding(3) var<storage, read> wall: array<f32>;\n@group(0) @binding(4) var<storage, read> sig: array<f32>;\nfn face(ix: i32, iy: i32, iz: i32, pc: f32) -> f32 {\n if (ix < 0 || ix >= i32(nx) || iy < 0 || iy >= i32(ny) || iz < 0 || iz >= i32(nz)) {\n return -pc; // outside the domain: p = 0, open\n }\n let k = u32(ix) + nx * (u32(iy) + ny * u32(iz));\n return wall[k] * (p[k] - pc);\n}\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x + gid.y * 4194240u;\n if (i >= npts) { return; }\n let ix = i32(i % nx);\n let iy = i32((i / nx) % ny);\n let iz = i32(i / (nx * ny));\n let pc = p[i];\n let s = face(ix - 1, iy, iz, pc) + face(ix + 1, iy, iz, pc)\n + face(ix, iy - 1, iz, pc) + face(ix, iy + 1, iz, pc)\n + face(ix, iy, iz - 1, pc) + face(ix, iy, iz + 1, pc);\n let lp = wall[i] * s / (h * h);\n let sd = sig[i] * dt;\n pn[i] = wall[i] * ((2.0 * pc - (1.0 - sd) * pm[i] + (c0 * dt) * (c0 * dt) * lp) / (1.0 + sd));\n}\n",
298 "constants": [
299 "npts",
300 "nx",
301 "ny",
302 "nz",
303 "h",
304 "dt",
305 "c0"
306 ]
307 },
308 "plane": {
309 "code": "// Contract the field against the radiation profile over z:\n// one thread per air column.\noverride nx: u32;\noverride ny: u32;\noverride nz: u32;\n@group(0) @binding(0) var<storage, read_write> plane: array<f32>;\n@group(0) @binding(1) var<storage, read> p: array<f32>;\n@group(0) @binding(2) var<storage, read> prof: array<f32>;\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x + gid.y * 4194240u;\n if (i >= nx * ny) { return; }\n var s = 0.0;\n for (var iz = 0u; iz < nz; iz++) {\n let k = i + nx * ny * iz;\n s += prof[k] * p[k];\n }\n plane[i] = s;\n}\n",
310 "constants": [
311 "nx",
312 "ny",
313 "nz"
314 ]
315 },
316 "xc": {
317 "code": "// Contract the plane against every x sine row: tmp[m + mx * iy].\noverride nx: u32;\noverride ny: u32;\noverride mx: u32;\n@group(0) @binding(0) var<storage, read_write> tmp: array<f32>;\n@group(0) @binding(1) var<storage, read> plane: array<f32>;\n@group(0) @binding(2) var<storage, read> sx: array<f32>;\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x + gid.y * 4194240u;\n if (i >= mx * ny) { return; }\n let m = i % mx;\n let iy = i / mx;\n var s = 0.0;\n for (var ix = 0u; ix < nx; ix++) {\n s += sx[m * nx + ix] * plane[ix + nx * iy];\n }\n tmp[i] = s;\n}\n",
318 "constants": [
319 "nx",
320 "ny",
321 "mx"
322 ]
323 },
324 "proj": {
325 "code": "// A line object's y factor is one everywhere: finish by summing tmp over\n// iy and write this step's sample of every kernel row.\noverride ny: u32;\noverride mx: u32;\noverride ir_steps: u32;\n@group(0) @binding(0) var<storage, read_write> kernels: array<f32>;\n@group(0) @binding(1) var<storage, read> tmp: array<f32>;\n@group(0) @binding(2) var<storage, read> tick: array<u32>;\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x + gid.y * 4194240u;\n if (i >= mx) { return; }\n var s = 0.0;\n for (var iy = 0u; iy < ny; iy++) {\n s += tmp[i + mx * iy];\n }\n let t = tick[0];\n if (t < ir_steps) {\n kernels[i * ir_steps + t] = s;\n }\n}\n",
326 "constants": [
327 "ny",
328 "mx",
329 "ir_steps"
330 ]
331 },
332 "excite_x": {
333 "code": "// Project this axis's excitation profile onto each recorded sine mode:\n// bx[e * mx + (mode-1)]. One thread per (event, mode).\nconst PI = 3.14159265358979;\noverride n1: u32;\noverride mx: u32;\noverride ev_stride: u32;\noverride h1: f32;\noverride L1: f32;\n@group(0) @binding(0) var<storage, read_write> bout: array<f32>;\n@group(0) @binding(1) var<storage, read> ev: array<f32>;\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x + gid.y * 4194240u;\n if (i >= mx * arrayLength(&ev) / ev_stride) { return; }\n let mi = i % mx;\n let e = i / mx;\n let base = e * ev_stride;\n let pluckpos = ev[base + 5u];\n let amp = ev[base + 6u];\n let mm = f32(mi + 1u);\n var s = 0.0;\n for (var k = 1u; k < n1 - 1u; k++) {\n let xi = f32(k) * h1;\n let ta = xi / (pluckpos * L1);\n let tb = (L1 - xi) / (L1 - pluckpos * L1);\n let u0 = amp * 0.5 * (ta + tb - abs(ta - tb));\n s += sin(mm * PI * f32(k) / f32(n1 - 1u)) * u0;\n }\n bout[i] = s;\n}\n",
334 "constants": [
335 "n1",
336 "mx",
337 "ev_stride",
338 "h1",
339 "L1"
340 ]
341 },
342 "slots": {
343 "code": "// One thread per (event, mode) of the string: the stiff damped string's\n// roots, its radiation slots, and the bridge slots its arriving slope\n// drives (the last kernel row, row mx).\nconst PI = 3.14159265358979;\noverride mx: u32;\noverride n1: u32;\noverride ev_stride: u32;\noverride h1: f32;\noverride L1: f32;\noverride dt: f32;\n@group(0) @binding(0) var<storage, read_write> slots: array<f32>;\n@group(0) @binding(1) var<storage, read> ev: array<f32>;\n@group(0) @binding(2) var<storage, read> bx: array<f32>;\n\nfn cmul(ar: f32, ai: f32, br: f32, bi: f32) -> vec2f {\n return vec2f(ar * br - ai * bi, ar * bi + ai * br);\n}\n// c = a * gain * (z - 2 + 1/z) = a * gain * (z-1)^2 / z, the acceleration\n// series, with z-1 supplied stably (the naive z - 1 cancels).\nfn acc_scale(p: vec4f, zm1Re: f32, gain: f32) -> vec4f {\n let m2 = p.z * p.z + p.w * p.w;\n if (m2 == 0.0) { return vec4f(0.0); }\n let sq = cmul(zm1Re, p.w, zm1Re, p.w);\n let num = cmul(sq.x, sq.y, p.z, -p.w);\n let c = cmul(p.x, p.y, gain * num.x / m2, gain * num.y / m2);\n return vec4f(c.x, c.y, p.z, p.w);\n}\n// c = a * gain * z, the next-displacement series (what a string's bridge\n// slope is read from).\nfn next_scale(p: vec4f, gain: f32) -> vec4f {\n let c = cmul(p.x, p.y, gain * p.z, gain * p.w);\n return vec4f(c.x, c.y, p.z, p.w);\n}\n\n\nfn write_slot(idx: u32, p: vec4f, row: f32, start: f32) {\n slots[idx * 6u] = p.x;\n slots[idx * 6u + 1u] = p.y;\n slots[idx * 6u + 2u] = p.z;\n slots[idx * 6u + 3u] = p.w;\n slots[idx * 6u + 4u] = row;\n slots[idx * 6u + 5u] = start;\n}\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x + gid.y * 4194240u;\n let n_events = arrayLength(&ev) / ev_stride;\n if (i >= n_events * mx) { return; }\n let e = i / mx;\n let mi = i % mx;\n let base = e * ev_stride;\n let start = ev[base];\n let f0 = ev[base + 1u];\n let B = ev[base + 2u];\n let t60 = ev[base + 3u];\n let sig1 = ev[base + 4u];\n let gair = ev[base + 7u];\n let gbridge = ev[base + 8u];\n let m = f32(mi + 1u);\n let b = bx[i] * 2.0 / f32(n1 - 1u);\n let s1 = sin(m * PI / (2.0 * f32(n1 - 1u)));\n let lam = (4.0 / (h1 * h1)) * s1 * s1;\n let cs = 2.0 * L1 * f0;\n let kap2 = B * (cs * L1 / PI) * (cs * L1 / PI);\n let w2 = cs * cs * lam + kap2 * lam * lam;\n\n let d = (6.91 / t60) * dt;\n let opd = 1.0 + d;\n let g = d + 2.0 * sig1 * dt * lam;\n let x = dt * dt * w2 + 2.0 * sig1 * dt * lam;\n let zRe = (1.0 - 0.5 * x) / opd;\n let zm1Re = -(0.5 * x + d) / opd;\n let nn = dt * dt * w2 - g * d - 0.25 * x * x;\n var p0 = vec4f(0.0);\n var p1 = vec4f(0.0);\n var zm10 = 0.0;\n var zm11 = 0.0;\n if (nn > 0.0) {\n let zIm = sqrt(nn) / opd;\n let mag2 = (1.0 - g) / opd;\n let aRe = b;\n let aIm = b * ((0.5 * x - g) / opd) / zIm;\n p0 = vec4f(aRe, aIm, zRe, zIm);\n zm10 = zm1Re;\n } else {\n let sq = sqrt(-nn) / opd;\n let r1 = zRe + sq;\n let r2 = zRe - sq;\n if (r1 != r2 && r1 != 0.0 && r2 != 0.0) {\n let det = 1.0 / r1 - 1.0 / r2;\n let a1 = (b - b / r2) / det;\n let a2 = b - a1;\n p0 = vec4f(a1, 0.0, r1, 0.0);\n p1 = vec4f(a2, 0.0, r2, 0.0);\n zm10 = zm1Re + sq;\n zm11 = zm1Re - sq;\n }\n }\n let ga = gair / (dt * dt);\n write_slot(i * 4u, acc_scale(p0, zm10, ga), f32(mi), start);\n write_slot(i * 4u + 1u, acc_scale(p1, zm11, ga), f32(mi), start);\n // The bridge: the far end is pinned, so the arriving slope is\n // -u[n-2]/h1, and the generated step's term is\n // -(gbridge cs^2 / L) bprof fb; together, +gbridge cs^2/(L h1) times\n // sin(m pi (n1-2)/(n1-1)) times the next-displacement series.\n let gb = (gbridge * cs * cs / (L1 * h1)) * sin(m * PI * f32(n1 - 2u) / f32(n1 - 1u));\n write_slot(i * 4u + 2u, next_scale(p0, gb), f32(mx), start);\n write_slot(i * 4u + 3u, next_scale(p1, gb), f32(mx), start);\n}\n",
344 "constants": [
345 "mx",
346 "n1",
347 "ev_stride",
348 "h1",
349 "L1",
350 "dt"
351 ]
352 },
353 "synth": {
354 "code": "// One thread per slot: a complex one-pole over this tile of samples, the\n// kernel row as input while it lasts and silence after (the closed-form\n// tail). State persists across tiles.\noverride tile: u32;\noverride total_steps: u32;\noverride ir_steps: u32;\noverride n_slots: u32;\n@group(0) @binding(0) var<storage, read> slots: array<f32>;\n@group(0) @binding(1) var<storage, read_write> state: array<f32>;\n@group(0) @binding(2) var<storage, read> kernels: array<f32>;\n@group(0) @binding(3) var<storage, read_write> partial: array<f32>;\n@group(0) @binding(4) var<storage, read> tick: array<u32>;\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x + gid.y * 4194240u;\n if (i >= n_slots) { return; }\n let cRe = slots[i * 6u];\n let cIm = slots[i * 6u + 1u];\n let zRe = slots[i * 6u + 2u];\n let zIm = slots[i * 6u + 3u];\n let row = u32(slots[i * 6u + 4u]);\n let start = u32(slots[i * 6u + 5u]);\n var wRe = state[i * 2u];\n var wIm = state[i * 2u + 1u];\n let t0 = tick[0] * tile;\n for (var s = 0u; s < tile; s++) {\n let t = t0 + s;\n if (t >= total_steps) { break; }\n var hv = 0.0;\n if (t >= start && t - start < ir_steps) {\n hv = kernels[row * ir_steps + (t - start)];\n }\n let nRe = zRe * wRe - zIm * wIm + hv;\n let nIm = zRe * wIm + zIm * wRe;\n wRe = nRe;\n wIm = nIm;\n partial[i * tile + s] = cRe * wRe - cIm * wIm;\n }\n state[i * 2u] = wRe;\n state[i * 2u + 1u] = wIm;\n}\n",
355 "constants": [
356 "tile",
357 "total_steps",
358 "ir_steps",
359 "n_slots"
360 ]
361 },
362 "reduce": {
363 "code": "// One thread per sample of the tile: sum every slot's contribution.\noverride tile: u32;\noverride total_steps: u32;\noverride n_slots: u32;\n@group(0) @binding(0) var<storage, read> partial: array<f32>;\n@group(0) @binding(1) var<storage, read_write> trace: array<f32>;\n@group(0) @binding(2) var<storage, read> tick: array<u32>;\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x + gid.y * 4194240u;\n if (i >= tile) { return; }\n let t = tick[0] * tile + i;\n if (t >= total_steps) { return; }\n var s = 0.0;\n for (var q = 0u; q < n_slots; q++) {\n s += partial[q * tile + i];\n }\n trace[t] = s;\n}\n",
364 "constants": [
365 "tile",
366 "total_steps",
367 "n_slots"
368 ]
369 },
370 "bplane": {
371 "code": "// Contract the field against the bridge patch profile over z:\n// one thread per air column.\noverride nx: u32;\noverride ny: u32;\noverride nz: u32;\n@group(0) @binding(0) var<storage, read_write> bplane: array<f32>;\n@group(0) @binding(1) var<storage, read> p: array<f32>;\n@group(0) @binding(2) var<storage, read> bprof: array<f32>;\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x + gid.y * 4194240u;\n if (i >= nx * ny) { return; }\n var s = 0.0;\n for (var iz = 0u; iz < nz; iz++) {\n let k = i + nx * ny * iz;\n s += bprof[k] * p[k];\n }\n bplane[i] = s;\n}\n",
372 "constants": [
373 "nx",
374 "ny",
375 "nz"
376 ]
377 },
378 "bridge_proj": {
379 "code": "// The bridge patch is a single fixed pattern: sum its plane and write the\n// sample into the last kernel row (row mx). One thread.\noverride nx: u32;\noverride ny: u32;\noverride mx: u32;\noverride ir_steps: u32;\n@group(0) @binding(0) var<storage, read_write> kernels: array<f32>;\n@group(0) @binding(1) var<storage, read> bplane: array<f32>;\n@group(0) @binding(2) var<storage, read> tick: array<u32>;\n@compute @workgroup_size(1)\nfn main() {\n var s = 0.0;\n for (var k = 0u; k < nx * ny; k++) {\n s += bplane[k];\n }\n let t = tick[0];\n if (t < ir_steps) {\n kernels[mx * ir_steps + t] = s;\n }\n}\n",
380 "constants": [
381 "nx",
382 "ny",
383 "mx",
384 "ir_steps"
385 ]
386 }
387 },
388 "precompute": {
389 "buffers": [
390 {
391 "name": "wall",
392 "count": "npts"
393 },
394 {
395 "name": "sig",
396 "count": "npts"
397 },
398 {
399 "name": "prof",
400 "count": "npts"
401 },
402 {
403 "name": "bprof",
404 "count": "npts"
405 },
406 {
407 "name": "sx",
408 "count": "mx * nx"
409 },
410 {
411 "name": "p",
412 "count": "npts"
413 },
414 {
415 "name": "pm",
416 "count": "npts"
417 },
418 {
419 "name": "pn",
420 "count": "npts"
421 },
422 {
423 "name": "plane",
424 "count": "nx * ny"
425 },
426 {
427 "name": "bplane",
428 "count": "nx * ny"
429 },
430 {
431 "name": "tmp",
432 "count": "mx * ny"
433 },
434 {
435 "name": "kernels",
436 "count": "n_rows * ir_steps"
437 }
438 ],
439 "setup": [
440 {
441 "shader": "medium",
442 "buffers": [
443 "wall",
444 "sig",
445 "prof",
446 "bprof"
447 ],
448 "dispatch": "npts"
449 },
450 {
451 "shader": "sines_x",
452 "buffers": [
453 "sx"
454 ],
455 "dispatch": "mx * nx"
456 },
457 {
458 "shader": "impulse",
459 "buffers": [
460 "p",
461 "wall",
462 "sig"
463 ],
464 "dispatch": "1"
465 }
466 ],
467 "loop": {
468 "count": "ir_steps",
469 "batch": 512,
470 "rotate": [
471 [
472 "p",
473 "pm",
474 "pn"
475 ]
476 ],
477 "body": [
478 {
479 "shader": "plane",
480 "buffers": [
481 "plane",
482 "p",
483 "prof"
484 ],
485 "dispatch": "nx * ny"
486 },
487 {
488 "shader": "xc",
489 "buffers": [
490 "tmp",
491 "plane",
492 "sx"
493 ],
494 "dispatch": "mx * ny"
495 },
496 {
497 "shader": "proj",
498 "buffers": [
499 "kernels",
500 "tmp",
501 "__tick"
502 ],
503 "dispatch": "mx"
504 },
505 {
506 "shader": "bplane",
507 "buffers": [
508 "bplane",
509 "p",
510 "bprof"
511 ],
512 "dispatch": "nx * ny"
513 },
514 {
515 "shader": "bridge_proj",
516 "buffers": [
517 "kernels",
518 "bplane",
519 "__tick"
520 ],
521 "dispatch": "1"
522 },
523 {
524 "shader": "step",
525 "buffers": [
526 "p",
527 "pm",
528 "pn",
529 "wall",
530 "sig"
531 ],
532 "dispatch": "npts"
533 }
534 ]
535 },
536 "outputs": [
537 "kernels"
538 ]
539 },
540 "render": {
541 "buffers": [
542 {
543 "name": "kernels",
544 "count": "n_rows * ir_steps"
545 },
546 {
547 "name": "bx",
548 "count": "n_events * mx"
549 },
550 {
551 "name": "slots",
552 "count": "n_slots * 6"
553 },
554 {
555 "name": "state",
556 "count": "n_slots * 2"
557 },
558 {
559 "name": "partial",
560 "count": "n_slots * tile"
561 },
562 {
563 "name": "trace",
564 "count": "total_steps"
565 }
566 ],
567 "inputs": [
568 "kernels"
569 ],
570 "eventParams": [
571 "f0",
572 "B",
573 "t60",
574 "sig1",
575 "pluckpos",
576 "amp",
577 "gair",
578 "gbridge"
579 ],
580 "derived": {
581 "ev_stride": "9",
582 "n_slots": "n_events * mx * 4"
583 },
584 "setup": [
585 {
586 "shader": "excite_x",
587 "buffers": [
588 "bx",
589 "__events"
590 ],
591 "dispatch": "n_events * mx"
592 },
593 {
594 "shader": "slots",
595 "buffers": [
596 "slots",
597 "__events",
598 "bx"
599 ],
600 "dispatch": "n_events * mx"
601 }
602 ],
603 "loop": {
604 "count": "n_tiles",
605 "batch": 8,
606 "body": [
607 {
608 "shader": "synth",
609 "buffers": [
610 "slots",
611 "state",
612 "kernels",
613 "partial",
614 "__tick"
615 ],
616 "dispatch": "n_slots"
617 },
618 {
619 "shader": "reduce",
620 "buffers": [
621 "partial",
622 "trace",
623 "__tick"
624 ],
625 "dispatch": "tile"
626 }
627 ]
628 },
629 "trace": "trace"
630 },
631 "visual": {
632 "dims": [
633 "nx",
634 "ny",
635 "nz"
636 ],
637 "extents": [
638 "Lx",
639 "Ly",
640 "Lz"
641 ],
642 "buffers": [
643 {
644 "name": "wall",
645 "count": "npts"
646 },
647 {
648 "name": "sig",
649 "count": "npts"
650 },
651 {
652 "name": "prof",
653 "count": "npts"
654 },
655 {
656 "name": "bprof",
657 "count": "npts"
658 }
659 ],
660 "setup": [
661 {
662 "shader": "medium",
663 "buffers": [
664 "wall",
665 "sig",
666 "prof",
667 "bprof"
668 ],
669 "dispatch": "npts"
670 }
671 ],
672 "volumes": [
673 {
674 "buffer": "wall",
675 "style": "solid",
676 "invert": true,
677 "color": [
678 0.62,
679 0.48,
680 0.34
681 ]
682 },
683 {
684 "buffer": "prof",
685 "style": "cloud",
686 "color": [
687 1,
688 0.45,
689 0.2
690 ]
691 },
692 {
693 "buffer": "bprof",
694 "style": "cloud",
695 "color": [
696 0.35,
697 0.65,
698 1
699 ]
700 }
701 ],
702 "markers": [
703 {
704 "kind": "line",
705 "from": [
706 "-L1/2",
707 "oy",
708 "oz"
709 ],
710 "to": [
711 "L1/2",
712 "oy",
713 "oz"
714 ],
715 "color": [
716 1,
717 0.85,
718 0.3
719 ],
720 "label": "string"
721 },
722 {
723 "kind": "point",
724 "at": [
725 "mic_x",
726 "mic_y",
727 "mic_z"
728 ],
729 "color": [
730 0.3,
731 0.85,
732 1
733 ],
734 "label": "mic"
735 }
736 ],
737 "partials": {
738 "buffer": "slots",
739 "stride": 6,
740 "zRe": 2,
741 "zIm": 3
742 }
743 }
744}