{ "instrawave": 1, "name": "dulcimer", "description": "One plucked string over a rigid box with a sound hole, radiating along its line and driving a bridge patch above the plate.", "params": [ { "key": "nx", "label": "air cells along x", "unit": "1", "value": 96, "min": 32, "max": 256, "class": "accuracy", "hint": "The air grid; ny = nz = nx/2. The simulation is honest to about c/(8h). Must be even.", "step": 2 }, { "key": "Lx", "label": "domain length", "unit": "m", "value": 1, "min": 0.6, "max": 2, "class": "geometry", "hint": "The air box around the instrument, twice as long in x as in y and z." }, { "key": "ir_seconds", "label": "room response length", "unit": "s", "value": 0.5, "min": 0.1, "max": 2, "class": "accuracy", "hint": "How long the recorded room responses run; a room still ringing past this is truncated." }, { "key": "L1", "label": "speaking length", "unit": "m", "value": 0.6, "min": 0.3, "max": 0.9, "class": "geometry" }, { "key": "oy", "label": "across", "unit": "m", "value": 0, "min": -0.15, "max": 0.15, "class": "geometry" }, { "key": "oz", "label": "height", "unit": "m", "value": 0.02, "min": 0.005, "max": 0.15, "class": "geometry", "hint": "Height of the string above z = 0." }, { "key": "boxl", "label": "box length", "unit": "m", "value": 0.66, "min": 0.3, "max": 0.9, "class": "geometry" }, { "key": "boxw", "label": "box width", "unit": "m", "value": 0.16, "min": 0.08, "max": 0.4, "class": "geometry" }, { "key": "boxd", "label": "box depth", "unit": "m", "value": 0.08, "min": 0.03, "max": 0.25, "class": "geometry", "hint": "How far the box hangs below its top plate at z = 0." }, { "key": "thick", "label": "wall thickness", "unit": "m", "value": 0.016, "min": 0.008, "max": 0.05, "class": "geometry" }, { "key": "holer", "label": "sound hole radius", "unit": "m", "value": 0.035, "min": 0, "max": 0.12, "class": "geometry", "hint": "0 seals the box; the hole is what lets the cavity speak." }, { "key": "holex", "label": "sound hole x", "unit": "m", "value": -0.05, "min": -0.3, "max": 0.3, "class": "geometry" }, { "key": "absorb", "label": "surface absorption", "unit": "1/s", "value": 800, "min": 0, "max": 4000, "class": "geometry" }, { "key": "mic_x", "label": "microphone x", "unit": "m", "value": 0.12, "min": -0.45, "max": 0.45, "class": "geometry" }, { "key": "mic_y", "label": "microphone y", "unit": "m", "value": 0.08, "min": -0.2, "max": 0.2, "class": "geometry" }, { "key": "mic_z", "label": "microphone z", "unit": "m", "value": 0.1, "min": -0.2, "max": 0.2, "class": "geometry" }, { "key": "f0", "label": "pitch", "unit": "Hz", "value": 294, "min": 80, "max": 880, "class": "tuning", "hint": "The fundamental; the wave speed follows, cs = 2 L f0." }, { "key": "B", "label": "inharmonicity", "unit": "1", "value": 0.0001, "min": 0, "max": 0.002, "class": "tuning", "hint": "Bending stiffness as the inharmonicity coefficient." }, { "key": "t60", "label": "decay t60", "unit": "s", "value": 4, "min": 0.2, "max": 8, "class": "tuning" }, { "key": "sig1", "label": "brightness decay", "unit": "m^2/s", "value": 0.005, "min": 0, "max": 0.05, "class": "tuning" }, { "key": "pluckpos", "label": "pluck position", "unit": "1", "value": 0.22, "min": 0.08, "max": 0.92, "class": "tuning" }, { "key": "amp", "label": "pluck height", "unit": "m", "value": 0.002, "min": 0.0002, "max": 0.005, "class": "tuning" }, { "key": "gair", "label": "line radiation gain", "unit": "1", "value": 0.3, "min": 0, "max": 2, "class": "tuning" }, { "key": "gbridge", "label": "bridge drive gain", "unit": "1", "value": 1, "min": 0, "max": 2, "class": "tuning", "hint": "The pull at the far end driving a patch above the plate." } ], "derived": { "c0": "343", "cfl": "0.5", "ny": "nx / 2", "nz": "nx / 2", "npts": "nx * ny * nz", "h": "Lx / nx", "Ly": "ny * h", "Lz": "nz * h", "dt": "cfl * h / (sqrt(3) * c0)", "ir_steps": "max(1, round(ir_seconds / dt))", "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))", "cs_worst": "2 * L1 * f0__max", "kap2_worst": "B__max * (cs_worst * cs_worst * L1 * L1) / (pi * pi)", "a_worst": "cs_worst*cs_worst*dt*dt + 4*sig1__max*dt", "h_min": "sqrt((a_worst + sqrt(a_worst*a_worst + 16*kap2_worst*dt*dt)) / 2) / 0.95", "n1": "max(5, min(1024, floor(L1 / h_min) + 1))", "h1": "L1 / (n1 - 1)", "mx": "n1 - 2", "n_rows": "mx + 1" }, "shaders": { "medium": { "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 wall: array;\n@group(0) @binding(1) var sig: array;\n@group(0) @binding(2) var prof: array;\n@group(0) @binding(3) var bprof: array;\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3) {\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", "constants": [ "npts", "nx", "ny", "h", "Lx", "Ly", "Lz", "boxl", "boxw", "boxd", "thick", "holer", "holex", "absorb", "oy", "oz", "L1" ] }, "sines_x": { "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 table: array;\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3) {\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", "constants": [ "n1", "mx", "nx", "L1", "h1", "Lx", "h" ] }, "impulse": { "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 p: array;\n@group(0) @binding(1) var wall: array;\n@group(0) @binding(2) var sig: array;\n@compute @workgroup_size(1)\nfn main() {\n p[mic_index] = dt * dt * wall[mic_index] / (1.0 + sig[mic_index] * dt);\n}\n", "constants": [ "mic_index", "dt" ] }, "step": { "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 p: array;\n@group(0) @binding(1) var pm: array;\n@group(0) @binding(2) var pn: array;\n@group(0) @binding(3) var wall: array;\n@group(0) @binding(4) var sig: array;\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) {\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", "constants": [ "npts", "nx", "ny", "nz", "h", "dt", "c0" ] }, "plane": { "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 plane: array;\n@group(0) @binding(1) var p: array;\n@group(0) @binding(2) var prof: array;\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3) {\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", "constants": [ "nx", "ny", "nz" ] }, "xc": { "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 tmp: array;\n@group(0) @binding(1) var plane: array;\n@group(0) @binding(2) var sx: array;\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3) {\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", "constants": [ "nx", "ny", "mx" ] }, "proj": { "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 kernels: array;\n@group(0) @binding(1) var tmp: array;\n@group(0) @binding(2) var tick: array;\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3) {\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", "constants": [ "ny", "mx", "ir_steps" ] }, "excite_x": { "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 bout: array;\n@group(0) @binding(1) var ev: array;\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3) {\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", "constants": [ "n1", "mx", "ev_stride", "h1", "L1" ] }, "slots": { "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 slots: array;\n@group(0) @binding(1) var ev: array;\n@group(0) @binding(2) var bx: array;\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) {\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", "constants": [ "mx", "n1", "ev_stride", "h1", "L1", "dt" ] }, "synth": { "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 slots: array;\n@group(0) @binding(1) var state: array;\n@group(0) @binding(2) var kernels: array;\n@group(0) @binding(3) var partial: array;\n@group(0) @binding(4) var tick: array;\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3) {\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", "constants": [ "tile", "total_steps", "ir_steps", "n_slots" ] }, "reduce": { "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 partial: array;\n@group(0) @binding(1) var trace: array;\n@group(0) @binding(2) var tick: array;\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3) {\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", "constants": [ "tile", "total_steps", "n_slots" ] }, "bplane": { "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 bplane: array;\n@group(0) @binding(1) var p: array;\n@group(0) @binding(2) var bprof: array;\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid: vec3) {\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", "constants": [ "nx", "ny", "nz" ] }, "bridge_proj": { "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 kernels: array;\n@group(0) @binding(1) var bplane: array;\n@group(0) @binding(2) var tick: array;\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", "constants": [ "nx", "ny", "mx", "ir_steps" ] } }, "precompute": { "buffers": [ { "name": "wall", "count": "npts" }, { "name": "sig", "count": "npts" }, { "name": "prof", "count": "npts" }, { "name": "bprof", "count": "npts" }, { "name": "sx", "count": "mx * nx" }, { "name": "p", "count": "npts" }, { "name": "pm", "count": "npts" }, { "name": "pn", "count": "npts" }, { "name": "plane", "count": "nx * ny" }, { "name": "bplane", "count": "nx * ny" }, { "name": "tmp", "count": "mx * ny" }, { "name": "kernels", "count": "n_rows * ir_steps" } ], "setup": [ { "shader": "medium", "buffers": [ "wall", "sig", "prof", "bprof" ], "dispatch": "npts" }, { "shader": "sines_x", "buffers": [ "sx" ], "dispatch": "mx * nx" }, { "shader": "impulse", "buffers": [ "p", "wall", "sig" ], "dispatch": "1" } ], "loop": { "count": "ir_steps", "batch": 512, "rotate": [ [ "p", "pm", "pn" ] ], "body": [ { "shader": "plane", "buffers": [ "plane", "p", "prof" ], "dispatch": "nx * ny" }, { "shader": "xc", "buffers": [ "tmp", "plane", "sx" ], "dispatch": "mx * ny" }, { "shader": "proj", "buffers": [ "kernels", "tmp", "__tick" ], "dispatch": "mx" }, { "shader": "bplane", "buffers": [ "bplane", "p", "bprof" ], "dispatch": "nx * ny" }, { "shader": "bridge_proj", "buffers": [ "kernels", "bplane", "__tick" ], "dispatch": "1" }, { "shader": "step", "buffers": [ "p", "pm", "pn", "wall", "sig" ], "dispatch": "npts" } ] }, "outputs": [ "kernels" ] }, "render": { "buffers": [ { "name": "kernels", "count": "n_rows * ir_steps" }, { "name": "bx", "count": "n_events * mx" }, { "name": "slots", "count": "n_slots * 6" }, { "name": "state", "count": "n_slots * 2" }, { "name": "partial", "count": "n_slots * tile" }, { "name": "trace", "count": "total_steps" } ], "inputs": [ "kernels" ], "eventParams": [ "f0", "B", "t60", "sig1", "pluckpos", "amp", "gair", "gbridge" ], "derived": { "ev_stride": "9", "n_slots": "n_events * mx * 4" }, "setup": [ { "shader": "excite_x", "buffers": [ "bx", "__events" ], "dispatch": "n_events * mx" }, { "shader": "slots", "buffers": [ "slots", "__events", "bx" ], "dispatch": "n_events * mx" } ], "loop": { "count": "n_tiles", "batch": 8, "body": [ { "shader": "synth", "buffers": [ "slots", "state", "kernels", "partial", "__tick" ], "dispatch": "n_slots" }, { "shader": "reduce", "buffers": [ "partial", "trace", "__tick" ], "dispatch": "tile" } ] }, "trace": "trace" }, "visual": { "dims": [ "nx", "ny", "nz" ], "extents": [ "Lx", "Ly", "Lz" ], "buffers": [ { "name": "wall", "count": "npts" }, { "name": "sig", "count": "npts" }, { "name": "prof", "count": "npts" }, { "name": "bprof", "count": "npts" } ], "setup": [ { "shader": "medium", "buffers": [ "wall", "sig", "prof", "bprof" ], "dispatch": "npts" } ], "volumes": [ { "buffer": "wall", "style": "solid", "invert": true, "color": [ 0.62, 0.48, 0.34 ] }, { "buffer": "prof", "style": "cloud", "color": [ 1, 0.45, 0.2 ] }, { "buffer": "bprof", "style": "cloud", "color": [ 0.35, 0.65, 1 ] } ], "markers": [ { "kind": "line", "from": [ "-L1/2", "oy", "oz" ], "to": [ "L1/2", "oy", "oz" ], "color": [ 1, 0.85, 0.3 ], "label": "string" }, { "kind": "point", "at": [ "mic_x", "mic_y", "mic_z" ], "color": [ 0.3, 0.85, 1 ], "label": "mic" } ], "partials": { "buffer": "slots", "stride": 6, "zRe": 2, "zIm": 3 } } }