/** * The bodies an instrument can sound over, as data plus a MATLAB fragment. * * A body is a rigid shell drawn as a *mask* into the air grid — 0 inside the * solid, 1 in air — which the solver's masked Laplacian turns into the * sound-hard (Neumann) condition at no timestep cost (see src/mgpu/ops.ts). * What a rigid mask cannot do is absorb, so each body paints a thin layer of * ordinary volume absorption over its surfaces (the smoothed mask's * transition skin, 4·shell·(1-shell), which peaks exactly at the surface); * that parameter is what tames a cavity's ring the way real wood does. * * Each template contributes the MATLAB lines that compute `shell` (0..1, * 1 inside the solid) from the grid coordinates and its own parameters; the * code generator (src/studio/codegen.ts) wraps them into the scene's * `medium` function. Masks are drawn with a sharp profile (about half a * cell) on purpose: these shapes are grid-aligned or round, so sharpness * costs no staircase artefacts, and a plate only two cells thick has to * reach mask ≈ 0 at its centre or it leaks. */ import type { BodyType, ParamSpec } from './types.ts'; export interface BodyTemplate { type: BodyType; label: string; blurb: string; paramSpecs: ParamSpec[]; /** MATLAB lines assigning `shell` (npts x 1, 0..1). May use x, y, z, h, * npts and this body's parameters. */ matlab: string[]; } const noneBody: BodyTemplate = { type: 'none', label: 'Open air', blurb: 'No body at all: the objects sound in free air.', paramSpecs: [], matlab: ['shell = zeros(npts, 1);'], }; const boxBody: BodyTemplate = { type: 'box', label: 'Box', blurb: 'A rigid box under the objects, with a round sound hole in its top plate — a dulcimer or guitar body, minus the wood’s own modes.', paramSpecs: [ { key: 'boxl', label: 'length (m)', value: 0.66, min: 0.3, max: 0.9, step: 0.01, hint: 'Along x. The default just outreaches the default string.' }, { key: 'boxw', label: 'width (m)', value: 0.16, min: 0.08, max: 0.4, step: 0.005 }, { key: 'boxd', label: 'depth (m)', value: 0.08, min: 0.03, max: 0.25, step: 0.005, musical: true, hint: 'How far the box hangs below its top plate (which sits at z = 0). Deeper means a bigger cavity and a lower air resonance.' }, { key: 'thick', label: 'wall thickness (m)', value: 0.016, min: 0.008, max: 0.05, step: 0.002, hint: 'Plates thinner than about two grid cells leak; the app warns rather than stops you.' }, { key: 'holer', label: 'sound hole radius (m)', value: 0.035, min: 0, max: 0.12, step: 0.005, musical: true, hint: '0 seals the box. The hole is what lets the cavity speak.' }, { key: 'holex', label: 'sound hole x (m)', value: -0.05, min: -0.3, max: 0.3, step: 0.01 }, { key: 'absorb', label: 'surface absorption (1/s)', value: 800, min: 0, max: 4000, step: 50, hint: 'A lossy skin over the shell’s surfaces — the stand-in for wood’s own damping. 0 lets the cavity ring hard.' }, ], matlab: [ '% The box: the outer solid minus its interior cavity, top plate at z = 0.', 'sw = 0.45 * h;', 'box_ox = 0.5 * (1 - tanh((abs(x) - boxl/2) / sw));', 'box_oy = 0.5 * (1 - tanh((abs(y) - boxw/2) / sw));', 'box_oz = 0.5 * (1 - tanh((abs(z + boxd/2) - boxd/2) / sw));', 'box_ix = 0.5 * (1 - tanh((abs(x) - (boxl/2 - thick)) / sw));', 'box_iy = 0.5 * (1 - tanh((abs(y) - (boxw/2 - thick)) / sw));', 'box_iz = 0.5 * (1 - tanh((abs(z + boxd/2) - (boxd/2 - thick)) / sw));', 'shell = max(0, box_ox .* box_oy .* box_oz - box_ix .* box_iy .* box_iz);', '% The sound hole: a cylinder cut out of the top plate only.', 'hole_r = sqrt((x - holex).^2 + y.^2);', 'hole = 0.5 * (1 - tanh((hole_r - holer) / sw));', 'top = 0.5 * (1 - tanh((abs(z + thick/2) - thick/2) / sw));', 'shell = max(0, shell - hole .* top);', ], }; const shellBody: BodyTemplate = { type: 'shell', label: 'Drum shell', blurb: 'A rigid cylinder hanging below z = 0, open at the top — put a membrane over it and it is a drum.', paramSpecs: [ { key: 'shellr', label: 'radius (m)', value: 0.17, min: 0.08, max: 0.3, step: 0.005, musical: true, hint: 'A hair larger than the head that sits over it.' }, { key: 'shelld', label: 'depth (m)', value: 0.3, min: 0.05, max: 0.4, step: 0.01, musical: true, hint: 'The cavity under the head. Deeper means a lower air resonance.' }, { key: 'thick', label: 'wall thickness (m)', value: 0.016, min: 0.008, max: 0.05, step: 0.002 }, { key: 'closed', label: 'closed bottom (0/1)', value: 1, min: 0, max: 1, step: 1, hint: '1 puts a plate across the bottom (a closed tom); 0 leaves the cylinder open like a concert tom.' }, { key: 'absorb', label: 'surface absorption (1/s)', value: 800, min: 0, max: 4000, step: 50, hint: 'A lossy skin over the shell’s surfaces. 0 lets the cavity ring hard.' }, ], matlab: [ '% The shell: a cylindrical ring from z = -shelld to 0, optional bottom.', 'sw = 0.45 * h;', 'shell_r = sqrt(x.^2 + y.^2);', 'cyl_o = 0.5 * (1 - tanh((shell_r - shellr) / sw));', 'cyl_i = 0.5 * (1 - tanh((shell_r - (shellr - thick)) / sw));', 'cyl_z = 0.5 * (1 - tanh((abs(z + shelld/2) - shelld/2) / sw));', 'bot = 0.5 * (1 - tanh((abs(z + shelld - thick/2) - thick/2) / sw));', 'shell = min(1, max(0, cyl_o - cyl_i) .* cyl_z + closed * (cyl_i .* bot));', ], }; export const BODY_TEMPLATES: Record = { none: noneBody, box: boxBody, shell: shellBody, };