Most pianos work the same way: press a key, hear a note. Reverse Piano inverts that. Pressing a key produces nothing; the instrument records the press and waits. Sound is synthesized at the moment of release, shaped by three measurements taken during the hold — how long the key was held, the velocity it was struck with, and how many other keys were held at the same time. A short hold produces a fast, percussive tone; a hold of several seconds produces a slow attack and long sustain.
how to play
Two ways in — your laptop keyboard, or a MIDI controller.
black: w e t y u o p ]
white: a s d f g h j k l ; ' \
- keyboard — hold a key, then release it. Velocity is faked (random, 70–120) since browsers don't report real key pressure. Simultaneous notes are capped by your keyboard's hardware rollover (often 6 keys) — use MIDI for full polyphony.
- MIDI — plug a controller in over USB; it's auto-detected. Real velocity replaces the faked value.
- Local Control — turn it off on the controller, or you'll hear its own sound layered with the engine's (which is also fun).
- browser — keyboard play works anywhere. MIDI needs Chrome, Edge, or Firefox — Safari doesn't support Web MIDI.
how it works
Nothing is scheduled while a key is held. On keydown (or MIDI note-on), the note goes into a single map — that's the entire cost of a press:
held: Map<note, {startTime, velocity}>
on release:
duration = now - startTime // seconds of silence
velocity = midiVelocity / 127 // 0..1
density = heldKeys.size + 1 // simultaneous notes
held.delete(note)
Those three scalars are the only inputs to synthesis — each drives a different part of the sound:
duration → attack 0.005s → 0.125s
sustain 0.3s → 4.3s
release 0.2s → 3.2s
velocity → gain 0.08 + v · 0.25
cutoff 400 + v · 4500 Hz
density → reverb send, clamped at 0.8
Duration is normalized against a 3-second window before it interpolates the envelope. Density does one thing beyond the reverb send: past a richness threshold, a third partial appears in the tone. And the UI names each release by where its hold duration lands — gasp / breath / sigh / exhale.
Each release builds a throwaway Web Audio graph: a triangle fundamental, sine partials at 2·f₀ and 3·f₀, and a 100ms noise burst — boosted on very short holds to produce the percussive gasp transient — routed through a shared low-pass, an envelope gain, and a convolution reverb with a procedurally generated impulse response. No samples, no voice pool; polyphony is bounded only by how many keys are actually held.
filter direction
The low-pass cutoff sweeps downward as each note sounds — from its velocity-derived value to 0.15× over the note's lifetime — so the tone softens and rounds as it decays, matching the pressure curve of an exhalation: hold as inhale, release as exhale.
what I learned
The hardest part wasn't the code — it was learning how to play it. The first time I sat down with the finished version, I had to unlearn my instinct to press and sustain; this instrument only responds to release.
I think about this often when writing code. The best architectures are usually the ones where you notice what isn't there — the coupling that was never added, the abstraction layer that wasn't needed, the state that was never introduced. Systems, like this instrument, are as much about what you leave out as what you build.