/ proof-of-concept / toy-instruments
Sign in
proof-of-concept / toy-instruments
toy-instruments / src / instruments / bead-drum / page.ts
156 lines · 7.2 KBCodeBlameHistory
2cd36faBead drum: hex-lattice round head, felt mallet contact, top view and cross-section; shared instrument pageJeremy Magland 3import { MembraneView } from '../../ui/membraneview';
4import { mountInstrumentPage, type Built } from '../../ui/instrumentPage';
5import {
6 buildDrum,
7 buildDrumExcitation,
8 buildDrumPickup,
9 deriveDrum,
10 excitationPoint,
11 nearestBead,
12 type DrumParams,
13} from './model';
14import { DRUM_PRESETS } from './presets';
16const isExc = (t: DrumParams['excitation']) => (p: DrumParams) => p.excitation === t;
18const SECTIONS: Section<DrumParams>[] = [
19 {
20 title: 'Geometry',
21 params: [
22 { key: 'diameter', label: 'Diameter', kind: 'number', unit: 'cm', scale: 100 },
23 { key: 'rings', label: 'Rings of beads', kind: 'int', min: 1, help: 'Beads from the centre to the rim. The lattice spacing is radius / (rings + 1); bead count grows as about 3 × rings².' },
24 ],
25 },
26 {
27 title: 'Membrane',
28 params: [
29 { key: 'density', label: 'Surface density', kind: 'number', unit: 'kg/m²', help: 'Mass per area. Mylar drumheads: about 0.26 kg/m².' },
30 { key: 'tension', label: 'Tension', kind: 'number', unit: 'N/m', help: 'Force per unit length in the head (same in every direction). Sets the pitch.' },
31 { key: 'inPlane', label: 'In-plane stiffness E·t', kind: 'number', unit: 'N/m', help: "Young's modulus times thickness. Mylar 0.19 mm: about 8.6e5 N/m. Lower values run faster but exaggerate nonlinearity." },
32 ],
33 },
34 {
35 title: 'Damping (0 = off)',
36 params: [
37 { key: 't60Low', label: 'T60 low', kind: 'number', unit: 's' },
38 { key: 'fLow', label: 'at', kind: 'number', unit: 'Hz' },
39 { key: 't60High', label: 'T60 high', kind: 'number', unit: 's' },
40 { key: 'fHigh', label: 'at', kind: 'number', unit: 'Hz' },
41 ],
42 },
43 {
44 title: 'Excitation',
45 params: [
46 {
47 key: 'excitation',
48 label: 'Type',
49 kind: 'select',
50 options: [
51 ['mallet', 'Mallet (felt contact)'],
52 ['strike', 'Strike (initial velocity)'],
53 ['force', 'Force pulse'],
54 ['press', 'Press (initial dent, released)'],
55 ],
56 },
57 { key: 'excR', label: 'Position', kind: 'number', unit: '× radius', help: '0 = centre, 1 = rim.' },
58 { key: 'excAngle', label: 'Angle', kind: 'number', unit: '°' },
59 { key: 'excWidth', label: 'Patch width', kind: 'number', unit: 'mm', scale: 1000, help: 'Gaussian width of the contact patch; 0 = a single bead.' },
60 { key: 'malletMass', label: 'Mallet mass', kind: 'number', unit: 'g', scale: 1000, show: isExc('mallet') },
61 { key: 'malletSpeed', label: 'Mallet speed', kind: 'number', unit: 'm/s', show: isExc('mallet') },
62 { key: 'feltK', label: 'Felt stiffness K', kind: 'number', unit: 'N/mᵖ', show: isExc('mallet'), help: 'Contact force K·δ^p, δ = felt compression. Harder felt = larger K: shorter contact, brighter sound.' },
63 { key: 'feltP', label: 'Felt exponent p', kind: 'number', show: isExc('mallet'), help: 'Typical felt: 2 to 3.5.' },
64 { key: 'strikeVel', label: 'Velocity', kind: 'number', unit: 'm/s', show: isExc('strike') },
65 { key: 'forcePeak', label: 'Peak force', kind: 'number', unit: 'N', show: isExc('force') },
66 { key: 'forceMs', label: 'Duration', kind: 'number', unit: 'ms', show: isExc('force') },
67 { key: 'pressAmp', label: 'Depth', kind: 'number', unit: 'mm', scale: 1000, show: isExc('press') },
68 ],
69 },
70 {
71 title: 'Pickup (acceleration)',
72 params: [
73 { key: 'pickupMode', label: 'Pickup', kind: 'select', options: [['sum', 'Sum over all beads'], ['bead', 'One bead']], help: 'The sum of accelerations is the monopole term of the radiated sound.' },
74 { key: 'pickupR', label: 'Position', kind: 'number', unit: '× radius', show: (p) => p.pickupMode === 'bead' },
75 { key: 'pickupAngle', label: 'Angle', kind: 'number', unit: '°', show: (p) => p.pickupMode === 'bead' },
76 { key: 'pickupAxis', label: 'Component', kind: 'select', options: [['y', 'y (transverse)'], ['x', 'x (in-plane)'], ['z', 'z (in-plane)']] },
77 ],
78 },
79 {
80 title: 'Simulation',
81 params: [
82 { key: 'duration', label: 'Duration', kind: 'number', unit: 's' },
83 { key: 'fs', label: 'Sample rate', kind: 'select', options: [['44100', '44.1 kHz'], ['48000', '48 kHz'], ['96000', '96 kHz']] },
84 { key: 'cfl', label: 'Step safety', kind: 'number', help: 'Fraction of the leapfrog stability limit dt = 2/ω_max.' },
85 { key: 'frameRate', label: 'Replay frames', kind: 'number', unit: 'per s', help: 'How often displacements of all beads are saved for replay.' },
86 ],
87 },
88];
90export function mountBeadDrum(root: HTMLElement) {
91 const build = (p: DrumParams): Built | null => {
92 const d = deriveDrum(p);
93 if (!(d.restLength > 0) || d.n < 1) return null;
94 const sys = buildDrum(d);
95 const exc = buildDrumExcitation(p, d);
96 const pickup = buildDrumPickup(p, d);
97 return {
98 sys,
99 exc,
100 pickup,
101 scene: {
102 sys,
103 exc,
104 excKind: p.excitation,
105 pickup,
106 pickupLabel: `${p.pickupMode === 'sum' ? 'sum over all beads' : 'one bead'}, ${p.pickupAxis} acceleration`,
107 hoop: { radius: p.diameter / 2 },
108 },
109 };
110 };
112 mountInstrumentPage<DrumParams>(root, {
113 title: 'Bead drum',
114 wavName: 'bead-drum.wav',
115 presets: DRUM_PRESETS,
116 sections: SECTIONS,
117 setup(p) {
118 const d = deriveDrum(p);
119 const rows: [string, string][] = [
120 ['Beads', `${d.n} (rim points ${d.lattice.rim.length})`],
121 ['Lattice spacing', `${fmtNum(d.lattice.a * 1000)} mm`],
122 ['Bead mass', `${fmtNum(d.beadMass * 1000)} g`],
123 ['Spring stiffness, tension', `${fmtNum(d.springK)} N/m, ${fmtNum(d.springTension)} N`],
124 ['Rest length L0', `${fmtNum(d.restLength * 1000)} mm (pre-stretch ${fmtNum(d.prestretch * 100)}%)`],
125 ['Wave speed (transverse, in-plane)', `${fmtNum(d.waveSpeedT)}, ${fmtNum(d.waveSpeedP)} m/s`],
126 ['Ideal membrane fundamental', `${fmtNum(d.idealF01)} Hz`],
127 ];
128 const warnings = [...d.warnings];
129 if (p.excR >= 0.97) warnings.push('The excitation point is at the rim, where the head barely moves.');
130 if (d.n > 3000) warnings.push(`${d.n} beads: expect a slow run.`);
131 return { rows, warnings, built: build(p) };
132 },
c696b9aCymbal: plate bending via per-triangle shape operators, felt centre support, stick and mallet with angle and friction, far-field microphoneJeremy Magland 133 summaryRows: (p, _b, r) => [['Largest spring strain', `${fmtNum(r.maxStretch * 100)}% (pre-stretch ${fmtNum(deriveDrum(p).prestretch * 100)}%)`]],
c696b9aCymbal: plate bending via per-triangle shape operators, felt centre support, stick and mallet with angle and friction, far-field microphoneJeremy Magland 135 <section class="card" data-r="out" hidden><h2>Drumhead (top view and cross-section)</h2><div data-r="membrane"></div></section>`,
137 const view = new MembraneView(q('membrane'), timeline);
138 return {
139 show(p, _built, r) {
140 const d = deriveDrum(p);
141 view.set(
142 {
143 pos: d.lattice.pos,
144 radius: p.diameter / 2,
145 spacing: d.lattice.a,
146 strike: excitationPoint(p),
147 pickup: p.pickupMode === 'bead' ? nearestBead(d, (p.diameter / 2) * p.pickupR * Math.cos((p.pickupAngle * Math.PI) / 180), (p.diameter / 2) * p.pickupR * Math.sin((p.pickupAngle * Math.PI) / 180)) : null,
148 sectionAngle: p.excR > 0.01 ? p.excAngle : 0,
149 },
150 r.frames,
151 );
152 },
153 };
154 },
155 });
↑↓ move↵openescclose