MIDI hardware is optional for notes, drums, sustain, waveform choice, and every sound parameter. Power still requires one real user gesture because browsers gate AudioContext startup (src/App.svelte:27-35,154-171).
Computer keyboard
The note row is chromatic from C4 at startup:
a w s e d f t g y h u j k o l p ;
C C# D D# E F F# G G# A A# B C C# D D# E| Keys | Action |
|---|---|
a w s e d f t g y h u j k o l p ; | Seventeen chromatic notes from the current typing base |
z | Shift the typing base down one octave |
x | Shift the typing base up one octave |
1 through 8 | Trigger kick, rim, snare, clap, tom, closed hat, open hat, and cowbell |
Space | Sustain on while held; sustain off on release |
The implementation stores each held character with the exact note chosen at keydown. Changing octave while a key is held therefore cannot change the later note-off target (src/App.svelte:21-24,60-66,85-105). The base is bounded from MIDI note 24 through 96. Repeats and modified shortcuts are ignored.
Window blur drains every held note and releases sustain, preventing an alt-tab or lost keyup from leaving a drone (src/App.svelte:107-116,127-132). Space does not hijack a focused button or slider, so standard keyboard activation remains intact (src/App.svelte:43-58,92-100). Text inputs, selects, textareas, and editable content are excluded from the performance handler.
Typing notes use velocity 100. Number-key drums use velocity 112 and receive a synthetic note-off after 100 ms; drums themselves are one-shot voices (src/App.svelte:68-90).
On-screen piano
The piano exposes 25 notes from 48 through 72, C3 through C5. White keys use a flex row and black keys are positioned above them; hardware or typing notes outside that range still sound but have no matching on-screen key (src/components/Keys.svelte:6-18,115-175). Each key is a real button with a note-name accessible label and pressed state.
Pointer velocity comes from vertical strike position:
velocity = clamp(round(60 + 67 * (clientY - top) / height), 1, 127)A strike near the top is about 60; a strike near the bottom approaches 127 (src/components/Keys.svelte:45-63). The same formula is recalculated when a glissando enters a new key (src/components/Keys.svelte:65-78). Keyboard activation on a focused on-screen key uses velocity 100.
Multi-touch and glissando
pointerNotes maps each pointer id to its current note. It does not treat the first touch as the only instrument pointer. Two fingers can hold two different notes, and lifting one releases only its note (src/components/Keys.svelte:20-43).
On pointer down, the key releases implicit pointer capture when present. Without that release, a touch would remain owned by the first key and sibling pointerenter handlers could never produce a glissando. Entering a new key releases the pointer’s prior note, registers the new one, and dispatches its note-on (src/components/Keys.svelte:45-78). Window pointer-up, pointer-cancel, and blur all drain ownership (src/components/Keys.svelte:80-82,109-113).
When two input mechanisms hold the same pitch, a pointer or keyboard release checks the remaining owners before sending note-off. This prevents one finger or focused key from silencing another owner of the same note (src/components/Keys.svelte:31-43,84-102).
Drum pads
The eight square pads use the same top-to-bottom 60–127 velocity formula (src/components/Pads.svelte:6-26). They emit note 36 plus the pad index on the currently selected drum channel. Pointer input accepts every button-0 pointer rather than only the first touch, so pads can layer with held piano notes. Enter or Space activation on a focused button uses velocity 110 through the synthetic-click path (src/components/Pads.svelte:28-54).
The pad animation key is a monotonic hit sequence. Two strikes in the same millisecond still remount the flash instead of collapsing behind an identical timestamp (src/lib/midi/manager.svelte.ts:29-37,119-123).
Mobile piano strip
Below the sm breakpoint, the piano is a sticky bottom-0 strip with height 6 rem, a token border and shadow, and env(safe-area-inset-bottom) padding. At sm and above it returns to normal document flow at height 10 rem (src/components/Keys.svelte:115-119).
This layout was chosen only after measured geometry showed that moving a taller stage above the keys pushed the piano below a 390×844 viewport. The sticky strip keeps both the stage and playable keys visible at page top, remains playable at page bottom, and avoids a duplicate spacer. The app root clips horizontal overflow, while verification measures actual page geometry during particles rather than trusting the class alone (src/App.svelte:134-150).
Sound shaping without hardware
- Drag a dial upward or downward. Hold Shift for quarter-speed fine adjustment.
- Focus a dial and use arrows for 2% steps, Page Up or Page Down for 10%, and Home or End for the range edges (
src/components/Knobs.svelte:137-217). - Choose waveform, drum channel, volume, delay mix, and reverb mix in sound settings (
src/components/Settings.svelte:24-107). - Record typing notes, on-screen piano, pads, and sustain into the free-time phrase slot, then replay them through the normal visual and monitor path. Incoming MIDI CC events can also be recorded. UI dial changes call the sanctioned parameter setter directly, so they are not phrase events.
Record and replay a phrase
The three top-bar controls are Record phrase, Play phrase, and Clear phrase. Recording starts a new slot immediately; it does not overdub the previous one. Press Stop recording when finished, or let the 60-second or 2,000-event cap stop it automatically (src/components/TopBar.svelte:109-163; src/lib/state/phrase.svelte.ts:182-236).
The stored length is the last event offset plus a 300 ms release tail, with a 500 ms minimum. Play repeats that free-time interval without quantization. It is available only after the audio engine is running and the page is visible (src/lib/state/phrase.svelte.ts:136-139,196-217).
At stop and every wrap, replay emits sustain-off and note-off for loop-owned state that the recording did not close. A live-held note at the same pitch wins over the loop, so replay cannot retrigger it or silence it on loop note-off (src/lib/state/phrase.svelte.ts:286-350; src/lib/midi/manager.svelte.ts:119-195). Panic stops recording or playback and clears all ownership; Clear phrase stops timers, empties the slot, and persists the empty state.
MIDI-learn can be armed without hardware, but completing it needs an incoming CC. Pitch bend, modulation or pressure, and program change likewise have no end-user mouse control; the developer injector can exercise them for verification. See midi-reference for bytes and private page (sign-in required) for the supported automation surface.