A floor tom whose sound is sampled live from the 2D wave equation that draws its head.
The head is the 2D wave equation with broadband and frequency-dependent loss, stepped explicitly 24,000 times a second on a 61-cell disc, and the sound is the head's velocity at a pickup point, gathered into short AudioBuffers queued on the audio clock, which also paces the simulation. Strikes are millisecond half-sine force pulses, so a hit near the center mostly excites the (0,1) mode while a hit near the rim rings the asymmetric ones, and the inset compares the measured spectrum with the Bessel zeros (1, 1.59, 2.14, 2.30 times the fundamental) that make a drum inharmonic. Big hits stretch the head and briefly raise its pitch, the downward glide you hear on real toms. Every strike is filmed by a virtual 3,000 frame-per-second camera and replayed with a speed ramp, and between strikes a stroboscope locked to the fundamental shows the head breathing.
Try it. Click the head to strike it (the center thuds, the rim rings), keep the pointer down to rest a finger on the head and mute it, and drag the tension slider or use the up and down arrows to retune. Keys 1 to 5 strike from center to rim, Space strikes at random, S toggles the slow-motion replays, M mutes; sound starts on the first click.
Paste this into Claude Code, Codex or any coding agent to get a simple version running, then take it wherever you like.
Build a playable drum with JavaScript and the HTML canvas element, where the sound comes from the same wave simulation that draws the drumhead. Put everything in a single index.html file with no libraries or build step, so I can open it directly in a browser.
Start simple:
- Make a canvas that fills the window, stays sharp on high-DPI screens (scale by devicePixelRatio), and resizes with the window.
- Simulate a circular membrane on a 64 x 64 grid with the 2D wave equation: keep three Float32Arrays (previous, current, next) and update each cell inside the circle with next = 2 * current - previous + c2 * (sum of the four neighbors - 4 * current), with c2 about 0.3 for stability. Cells outside the circle stay at zero, which clamps the rim. Multiply by 0.9995 each step for a little damping.
- Draw the grid as a small image, coloring each cell by its height (warm for up, cool for down), and scale it up to fill a circle on the screen.
- When I click the head, add a small round bump of displacement at that point and watch the ripples spread and reflect.
Once that works, make it sound:
- On the first click, create an AudioContext. Run the simulation at 22,050 steps per second, about 368 steps per frame, and record the velocity (current minus previous) at one pickup cell off center into an array.
- Each frame, copy those samples into an AudioBuffer created at 22,050 Hz and schedule it with an AudioBufferSourceNode to start exactly when the previous chunk ends, keeping about 80 ms queued.
- Notice that hitting the center sounds dull and hitting near the rim sounds brighter: you are hearing different Bessel modes.
Explain the key ideas in short code comments. When you're done, tell me how to open it and suggest three directions I could take it next, such as shading the head as a lit 3D height field, adding a tension slider that bends the pitch, or plotting the spectrum against the Bessel zeros 1, 1.59, 2.14 and 2.30.