The Web Audio API shipped in Chrome in 2013. Thirteen years later, most browser-based pianos still sound like they're running through a tin can connected to a dial-up modem. There's a reason for that, and it's not the API's fault.
Why browser pianos sound bad
The naive approach to a browser piano is to generate a sine wave at the correct frequency. A4 is 440 Hz. Each semitone up multiplies the frequency by 2^(1/12), approximately 1.05946. So you create an oscillator, set its frequency, connect it to the audio context's destination, and you're done.
The result sounds nothing like a piano. It sounds like a hearing test.
A real piano string doesn't produce a single frequency. It produces a fundamental frequency plus a series of overtones (harmonics) at integer multiples of the fundamental. The second harmonic is twice the fundamental, the third is three times, and so on. The relative amplitudes of these harmonics give each instrument its characteristic timbre.
A piano's timbre is further shaped by the hammer strike (a sharp attack), the felt dampers (which create the release), the soundboard resonance, and sympathetic vibration from other strings. Recreating all of this with oscillators alone requires layering multiple harmonics with carefully tuned envelopes.
The Web Audio API approach
Here's a basic but decent-sounding piano note using the Web Audio API:
function playNote(frequency) {
const ctx = new AudioContext();
const harmonics = [1, 2, 3, 4, 5, 6];
const gains = [1.0, 0.5, 0.25, 0.125, 0.06, 0.03];
harmonics.forEach((h, i) => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.frequency.value = frequency * h;
gain.gain.setValueAtTime(gains[i] * 0.3, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 2);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start();
osc.stop(ctx.currentTime + 2);
});
}
This layers six harmonics with decreasing amplitudes and applies an exponential decay envelope. It's not a Steinway, but it's recognizably a piano-like sound rather than a pure tone.
The sample-based approach
Professional virtual instruments use recorded samples. A Steinway Model D is recorded at multiple velocities (how hard you press the key) across every note, sometimes with sustain pedal up and down variants. A full sample library can exceed 100 GB.
For a browser tool, you need a middle ground. A set of 12 to 24 high-quality samples per octave, at 2-3 velocity layers, compressed as OGG or MP3, totals around 5 to 15 MB. The Web Audio API's decodeAudioData loads these into memory, and you pitch-shift adjacent notes from the nearest sample using playbackRate.
The tradeoff is loading time versus sound quality. I've found that one sample per three semitones with playback rate adjustment is the sweet spot for a web tool. The pitch shifting artifacts are inaudible within that range.
Keyboard mapping and latency
Mapping a QWERTY keyboard to piano keys seems simple until you realize that keyboards have key repeat delay, which creates an unintentional re-trigger if someone holds a key. You need to track keydown and keyup separately, and ignore repeated keydown events for already-held keys.
const activeKeys = new Set();
document.addEventListener('keydown', (e) => {
if (activeKeys.has(e.code)) return;
activeKeys.add(e.code);
playNote(keyToFrequency(e.code));
});
document.addEventListener('keyup', (e) => {
activeKeys.delete(e.code);
releaseNote(keyToFrequency(e.code));
});
Latency is the other challenge. The Web Audio API's default buffer size can introduce 20-50ms of latency. For musical performance, anything above 10ms is perceptible. Setting the AudioContext's latencyHint to "interactive" helps, and reducing the buffer size via createScriptProcessor (deprecated but still functional) or AudioWorklet can get you under 10ms on modern hardware.
Touch support and velocity
On mobile, touch events need to map to piano keys based on the touch coordinates hitting the visual key elements. Multi-touch is essential for chords. And if you want velocity sensitivity, you can use the touch force API (where supported) or the speed of the touch event as a proxy.
I built a browser piano at zovo.one/free-tools/piano-keyboard that handles all of this: multi-octave keyboard with computer key mapping, touch support, and decent sound. It won't replace a MIDI controller, but for quick experimentation and learning, it gets the job done without installing anything.
I'm Michael Lip. I build free developer tools at zovo.one. 500+ tools, all private, all free.
Top comments (0)