/ proof-of-concept / soundbox
Sign in
proof-of-concept / soundbox
soundbox / src / studio / bodies.ts
107 lines · 5.4 KBCodeBlameHistory
2 * The bodies an instrument can sound over, as data plus a MATLAB fragment.
3 *
4 * A body is a rigid shell drawn as a *mask* into the air grid — 0 inside the
5 * solid, 1 in air — which the solver's masked Laplacian turns into the
6 * sound-hard (Neumann) condition at no timestep cost (see src/mgpu/ops.ts).
7 * What a rigid mask cannot do is absorb, so each body paints a thin layer of
8 * ordinary volume absorption over its surfaces (the smoothed mask's
9 * transition skin, 4·shell·(1-shell), which peaks exactly at the surface);
10 * that parameter is what tames a cavity's ring the way real wood does.
11 *
12 * Each template contributes the MATLAB lines that compute `shell` (0..1,
13 * 1 inside the solid) from the grid coordinates and its own parameters; the
14 * code generator (src/studio/codegen.ts) wraps them into the scene's
15 * `medium` function. Masks are drawn with a sharp profile (about half a
16 * cell) on purpose: these shapes are grid-aligned or round, so sharpness
17 * costs no staircase artefacts, and a plate only two cells thick has to
18 * reach mask ≈ 0 at its centre or it leaks.
19 */
20import type { BodyType, ParamSpec } from './types.ts';
22export interface BodyTemplate {
23 type: BodyType;
24 label: string;
25 blurb: string;
26 paramSpecs: ParamSpec[];
27 /** MATLAB lines assigning `shell` (npts x 1, 0..1). May use x, y, z, h,
28 * npts and this body's parameters. */
29 matlab: string[];
32const noneBody: BodyTemplate = {
33 type: 'none',
34 label: 'Open air',
35 blurb: 'No body at all: the objects sound in free air.',
36 paramSpecs: [],
37 matlab: ['shell = zeros(npts, 1);'],
38};
40const boxBody: BodyTemplate = {
41 type: 'box',
42 label: 'Box',
43 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.',
44 paramSpecs: [
45 { key: 'boxl', label: 'length (m)', value: 0.66, min: 0.3, max: 0.9, step: 0.01,
46 hint: 'Along x. The default just outreaches the default string.' },
47 { key: 'boxw', label: 'width (m)', value: 0.16, min: 0.08, max: 0.4, step: 0.005 },
a97bec5Render any scene from precomputation, and split the Play knobs by costJeremy Magland 48 { key: 'boxd', label: 'depth (m)', value: 0.08, min: 0.03, max: 0.25, step: 0.005, musical: true,
761af16soundbox: a scene-composition studio over the dulcimer engineJeremy Magland 49 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.' },
50 { key: 'thick', label: 'wall thickness (m)', value: 0.016, min: 0.008, max: 0.05, step: 0.002,
51 hint: 'Plates thinner than about two grid cells leak; the app warns rather than stops you.' },
a97bec5Render any scene from precomputation, and split the Play knobs by costJeremy Magland 52 { key: 'holer', label: 'sound hole radius (m)', value: 0.035, min: 0, max: 0.12, step: 0.005, musical: true,
761af16soundbox: a scene-composition studio over the dulcimer engineJeremy Magland 53 hint: '0 seals the box. The hole is what lets the cavity speak.' },
54 { key: 'holex', label: 'sound hole x (m)', value: -0.05, min: -0.3, max: 0.3, step: 0.01 },
55 { key: 'absorb', label: 'surface absorption (1/s)', value: 800, min: 0, max: 4000, step: 50,
56 hint: 'A lossy skin over the shell’s surfaces — the stand-in for wood’s own damping. 0 lets the cavity ring hard.' },
57 ],
58 matlab: [
59 '% The box: the outer solid minus its interior cavity, top plate at z = 0.',
60 'sw = 0.45 * h;',
61 'box_ox = 0.5 * (1 - tanh((abs(x) - boxl/2) / sw));',
62 'box_oy = 0.5 * (1 - tanh((abs(y) - boxw/2) / sw));',
63 'box_oz = 0.5 * (1 - tanh((abs(z + boxd/2) - boxd/2) / sw));',
64 'box_ix = 0.5 * (1 - tanh((abs(x) - (boxl/2 - thick)) / sw));',
65 'box_iy = 0.5 * (1 - tanh((abs(y) - (boxw/2 - thick)) / sw));',
66 'box_iz = 0.5 * (1 - tanh((abs(z + boxd/2) - (boxd/2 - thick)) / sw));',
67 'shell = max(0, box_ox .* box_oy .* box_oz - box_ix .* box_iy .* box_iz);',
68 '% The sound hole: a cylinder cut out of the top plate only.',
69 'hole_r = sqrt((x - holex).^2 + y.^2);',
70 'hole = 0.5 * (1 - tanh((hole_r - holer) / sw));',
71 'top = 0.5 * (1 - tanh((abs(z + thick/2) - thick/2) / sw));',
72 'shell = max(0, shell - hole .* top);',
73 ],
74};
76const shellBody: BodyTemplate = {
77 type: 'shell',
78 label: 'Drum shell',
79 blurb: 'A rigid cylinder hanging below z = 0, open at the top — put a membrane over it and it is a drum.',
80 paramSpecs: [
a97bec5Render any scene from precomputation, and split the Play knobs by costJeremy Magland 81 { key: 'shellr', label: 'radius (m)', value: 0.17, min: 0.08, max: 0.3, step: 0.005, musical: true,
761af16soundbox: a scene-composition studio over the dulcimer engineJeremy Magland 82 hint: 'A hair larger than the head that sits over it.' },
a97bec5Render any scene from precomputation, and split the Play knobs by costJeremy Magland 83 { key: 'shelld', label: 'depth (m)', value: 0.3, min: 0.05, max: 0.4, step: 0.01, musical: true,
761af16soundbox: a scene-composition studio over the dulcimer engineJeremy Magland 84 hint: 'The cavity under the head. Deeper means a lower air resonance.' },
85 { key: 'thick', label: 'wall thickness (m)', value: 0.016, min: 0.008, max: 0.05, step: 0.002 },
86 { key: 'closed', label: 'closed bottom (0/1)', value: 1, min: 0, max: 1, step: 1,
87 hint: '1 puts a plate across the bottom (a closed tom); 0 leaves the cylinder open like a concert tom.' },
88 { key: 'absorb', label: 'surface absorption (1/s)', value: 800, min: 0, max: 4000, step: 50,
89 hint: 'A lossy skin over the shell’s surfaces. 0 lets the cavity ring hard.' },
90 ],
91 matlab: [
92 '% The shell: a cylindrical ring from z = -shelld to 0, optional bottom.',
93 'sw = 0.45 * h;',
94 'shell_r = sqrt(x.^2 + y.^2);',
95 'cyl_o = 0.5 * (1 - tanh((shell_r - shellr) / sw));',
96 'cyl_i = 0.5 * (1 - tanh((shell_r - (shellr - thick)) / sw));',
97 'cyl_z = 0.5 * (1 - tanh((abs(z + shelld/2) - shelld/2) / sw));',
98 'bot = 0.5 * (1 - tanh((abs(z + shelld - thick/2) - thick/2) / sw));',
99 'shell = min(1, max(0, cyl_o - cyl_i) .* cyl_z + closed * (cyl_i .* bot));',
100 ],
101};
103export const BODY_TEMPLATES: Record<BodyType, BodyTemplate> = {
104 none: noneBody,
105 box: boxBody,
106 shell: shellBody,
107};
↑↓ move↵openescclose