The life of
a piano note.
No recording hides inside the key. We compute the instrument’s motion: felt meets steel, strings move wood, and a note becomes a roomful of sound.
Playable sound: a reduced browser demonstration of our physical engine.Sound starts on your first strike. One note, no samples.
Motion slowed and amplitudes enlarged. Diagrams show the model’s mechanisms, not a full solver. Visual speed and pause do not change audio.
A soft hammer.
A nonlinear encounter.
Change the gesture.
Hear the difference.
Every instrument control changes the sound and its explanation.
Live output, after volume & mute. Real audio time; independent of the slowed diagrams.
Inside the model
A physical engine, note by note
src/lib.rs keeps state for all 88 keys, A0–C8 (MIDI 21–108). A restrike adds hammer force to strings still ringing; there is no voice stealing. This page focuses on the physical piano, separate from the learned electric engine.
The bridge bus feeds the shared board and the sympathetic bank. Direct string radiation and board output sum in parallel. Mechanical attack noise, phantom longitudinal modes and duplex ring add detail.
Felt is an exciter, not a volume knob
src/hammer.rs integrates a hammer in f64 with symplectic Euler, four substeps per sample. A local string impedance and smoothed, delayed agraffe reflection can cause repeated contact.
u = x_h − y_s
F ≈ K · max(u, 0)^pThe full contact includes felt lock-up, loading/unloading, a finite force clamp and deterministic roughness. Bottom-octave voicing is different: the bottom two octaves approach p ≈ 1.05 with little lock-up, so loudness need not imply the same brightening as in treble.
Motion as a sum of modes
src/modal.rs uses damped complex rotators, not a finite-difference grid or delay-line string model.
z[k+1] = r · exp(iθ) · z[k] + g_in · F[k]
r = exp(−σ/f_s) < 1 · θ = 2πf/f_s
f_n = n · f₀ · √(1 + Bn²)Force enters the real axis and output primarily reads the imaginary axis, giving a sine impulse response; coupled modes have complex output residues. Decaying poles make bounded-input resonators stable. In src/keys.rs, f₀ is a stretched tuning reference, slightly different from the first partial when B > 0. Strike position weights modes near sin(nπx₀/L); higher partials generally damp faster. The default bass cap reaches 240 partials per oscillator, limited by the audio band.
More than independent strings
There is one physical string for A0–E1, two for F1–E2 and three for F2–C8. A one-string note has two polarisations; unisons use three reduced normal-mode families: VERT, HORZ and ANTI. Coupled coefficients encode prompt sound, aftersound and beating without a full bridge feedback solver. The strings drawn above are physical strings, not a one-to-one picture of oscillator banks.
src/sympathetic.rs uses one-directional bridge drive into other strings and then the board. Held keys, lifted dampers and the permanently undamped keys above MIDI 88 can ring; damped strings can still weakly couple. A pedal value ≥ 0.75 gives full lift; half-pedalling leaves partial contact. The pedal adds no new hammer energy.
A body, a room, two ears
src/soundboard.rs is a shared modal radiating board with four bridge regions and two listening positions. Small time and level differences make stereo. Its resonances bloom beneath the immediate direct attack; this is not convolution with a recorded impulse response.
In src/lib.rs, EQ precedes parallel dry sound, early reflections and FDN reverb. Tone and DC blocking follow, with optional limiting and soft saturation. Events land at exact sample offsets; a 64-sample control grid makes decisions consistent across host blocks. The audio callback has preallocated state and performs no allocation, locking or I/O.
What you are hearing here
This HTML uses one long-lived AudioWorklet: 24 band-limited partials in each of three simplified detuned families, distinct decays, a velocity-shaped contact pulse, eight sympathetic resonators and eight body modes. Damper loss follows key hold and continuous pedal position. A small stereo delay and different board weights produce two listening channels. Arrays are allocated once; rendering never creates buffers. Messages arrive asynchronously, not at the Rust engine’s exact event offsets.
The browser demonstration omits the full hammer/contact solver, exact coupled normal-mode residues, calibrated voicing, the complete 88-key resonance bank, mechanical/longitudinal/duplex detail and the room/EQ chain. It is not identical Rust engine output. Here f₀ uses equal-tempered references for comparison. The graphs are illustrative, except the computed partial frequencies and the actual analyser waveform.
The stock Rust engine’s src/calibration.rs and src/calibration_data.rs adjust force response and per-partial decay against acoustic reference targets. Those tables are numerical calibration, not playback samples. The browser sound is uncalibrated.