Parser contract
parseMidi(data) is a pure boundary. It reads the status high nibble as message kind, masks the low nibble into a zero-based wire channel, and returns one member of the closed MidiEvent union. Anything the app does not act on returns null (src/lib/midi/parse.ts:3-30; src/lib/midi/types.ts:1-7).
| Status family | Returned event | Dispatch behavior |
|---|---|---|
0x8n | noteoff | Releases a non-drum synth voice; drum-channel note-off is ignored because drums are one-shots |
0x9n | noteon, unless velocity is zero | Routes to synth or drums according to the configured drum channel |
0xBn | cc | CC64 sustain, CC1 vibrato, otherwise the learned or factory parameter map |
0xCn | programchange | Selects one of four waveforms with program % 4 |
0xDn | pressure | Maps channel pressure from 0–127 to vibrato depth |
0xEn | pitchbend | Applies normalized bend to every active synth voice |
A note-on with velocity 0 becomes noteoff, preserving its note and channel (src/lib/midi/parse.ts:10-17). Explicit note-off accepts a missing release velocity as zero. Program change and channel pressure are two-byte messages; the parser requires at least a status byte and one data byte.
The deliberate ignore set includes polyphonic key pressure (0xAn), system common and system real-time (0xFn, including clock, SysEx status, active sensing, and reset), and every other unsupported status family. MIDI access is requested with sysex: false (src/lib/midi/manager.svelte.ts:79-95). Unsupported messages never enter dispatch, the monitor, the phrase recorder, or audio.
Pitch bend assembly
MIDI pitch bend carries 14 bits as least-significant seven bits first, then most-significant seven bits:
const raw = data[1] | ((data[2] ?? 0) << 7)
const value = (raw - 8192) / 8192That makes raw 0 equal -1, raw 8192 equal 0, and raw 16383 equal 8191 / 8192, just below +1 (src/lib/midi/parse.ts:20-23). The synth maps the normalized value to ±200 cents and smooths changes over 10 ms on every oscillator (src/lib/audio/synth.ts:58,68,206-218). Panic resets both the visible bend and the synth closure to zero (src/lib/audio/engine.svelte.ts:209-214).
Channel convention
There is no drum-channel off-by-one.
| Layer | Convention | Evidence |
|---|---|---|
MIDI wire and MidiEvent.ch | 0 through 15 | status & 0x0f in src/lib/midi/parse.ts:6-9 |
| Settings selector | 1 through 16 | channel options in src/components/Settings.svelte:7,43-57 |
| Drum comparison | convert UI once with drumChannel - 1 | src/lib/midi/manager.svelte.ts:114-123 |
| UI pad and typing-drum events | emit settings.current.drumChannel - 1 | src/components/Pads.svelte:6-15; src/App.svelte:68-82 |
| Monitor display | convert wire once with entry.ev.ch + 1 | src/components/Monitor.svelte:81-87 |
The default UI drum channel is 10, which is wire channel 9 and status 0x99 for note-on (src/lib/state/settings.svelte.ts:26-34). Every other channel reaches the polysynth. Changing the selector changes routing immediately; it does not rewrite stored or already-sounding notes.
Drum notes fold by modulo eight. The intended banks are 36–43 and 44–51, both mapping to kick, rim, snare, clap, tom, closed hat, open hat, and cowbell (src/lib/audio/drums.ts:6-15,79-83; src/components/CheatSheet.svelte:137-153).
Continuous controllers
Two CCs have fixed performance meaning before learned mapping is consulted:
| CC | Meaning | Threshold or range |
|---|---|---|
1 | Mod wheel to vibrato depth | value / 127, then up to 60 cents |
64 | Sustain pedal | values 64–127 on; 0–63 off |
The factory parameter bindings are:
| CC | Parameter | Internal range |
|---|---|---|
70 | Detune | 0–50 cents |
71 | Resonance | normalized 0–1, rendered as Q 0.1–20 |
72 | Release | 0.02–3 seconds |
73 | Attack | 0.001–1.5 seconds |
74 | Cutoff | normalized 0–1, exponentially rendered as 80 Hz–12 kHz |
75 | Delay mix | 0–1 |
76 | Reverb mix | 0–1 |
77 | Volume | 0–1 |
mapping.apply() clamps incoming values to 0–127 and linearly normalizes them into each parameter range (src/lib/state/mapping.ts:22-43,65-68). Cutoff’s normalized value is converted exponentially in the synth; resonance is expanded to Q (src/lib/audio/synth.ts:41,130-133). Changes to cutoff, resonance, detune, and bend are smoothed on active voices rather than applied as discrete jumps.
MIDI-learn
Each dial has a Zap control. Activating it calls midi.startLearn(param) and leaves that parameter armed until one CC arrives, the same control is toggled, or Escape cancels (src/components/Knobs.svelte:219-223,280-292; src/App.svelte:43-49).
On the next non-special CC:
- Existing bindings for that parameter are removed.
- The incoming controller number is assigned to it.
- The binding is persisted under
midi.mapping.v1. - Learning clears.
- That same CC value is normalized and applied immediately.
The new controller can replace a CC that previously pointed elsewhere. Invalid persisted parameter names and corrupt JSON are ignored so startup keeps the factory bindings (src/lib/state/mapping.ts:70-90,102-125). CC1 and CC64 never reach learn because dispatch reserves them first.
Program change and pressure
Program change currently chooses sawtooth, square, triangle, or sine by modulo four and applies the waveform to active and future voices (src/lib/midi/manager.svelte.ts:203-205; src/lib/audio/synth.ts:251-253). A named patch bank is aspirational; see roadmap.
Channel pressure and CC1 share the vibrato path. The manager normalizes either to 0–1, and the engine converts that to up to 60 cents of per-voice LFO depth (src/lib/midi/manager.svelte.ts:177-205; src/lib/audio/engine.svelte.ts:181-183). Polyphonic pressure is not parsed.
Sources in the monitor
Accepted events carry a source string: midi, ui, kbd, or loop. The monitor renders that value without translation alongside the event value and one-based display channel (src/lib/midi/types.ts:1-9; src/components/Monitor.svelte:57-90). Replay is therefore visible and distinguishable. Dispatch logs first and calls phrase.capture() last; capture rejects loop, so replay can never recursively append itself (src/lib/midi/manager.svelte.ts:208-218; src/lib/state/phrase.svelte.ts:226-236).
For non-MIDI input paths, continue with playing-without-hardware. For byte-level runtime checks, use private page (sign-in required).