# @suaveplan/midi — universal MIDI primitives - **Target path**: `packages/media/audio/midi/` - **Tier**: universal (zero browser/Node coupling) - **Date**: 2026-05-23 - **Status**: PROPOSED ## Why The audio family ships codecs, plugins, analysers, an engine, and a React surface — but no MIDI primitives. Every downstream surface that wants to drive plugin parameters via control surfaces (`@suaveplan/react-control-surface`), accept input from hardware controllers (Maschine, Traktor, KORG nanoKONTROL), expose virtual instruments (SoundFont synth, drum machines), or replicate parameter state across a network session needs a shared MIDI vocabulary. Without this package every consumer hand-rolls byte parsers and types, which inevitably diverges (status nibble vs. channel masking, running status, SysEx framing edge cases, NRPN vs. RPN dispatch). This change introduces a universal-tier, zero-runtime-dependency package owning the canonical MIDI types, byte parsers, message constructors, CC/NRPN/RPN/Pitch/SysEx dispatchers, GM/GS/XG patch tables, MIDI 1.0 + MIDI 2.0 UMP differentiation, and a structural `MidiPortLike` contract that browser/Node adapters implement. No Web MIDI calls live here — that lives in `@suaveplan/react-midi` (browser) and a future `@suaveplan/midi-node` (server). ## What changes 1. **NEW package** `@suaveplan/midi` at `packages/media/audio/midi/`. 2. **Message types** — `MidiMessage` discriminated union: `NoteOn`, `NoteOff`, `PolyPressure`, `ControlChange`, `ProgramChange`, `ChannelPressure`, `PitchBend`, `SystemExclusive`, `TimeCode`, `SongPosition`, `SongSelect`, `TuneRequest`, `Clock`, `Start`, `Continue`, `Stop`, `ActiveSensing`, `Reset`. Each carries the parsed fields (channel 1-16 or `null` for system messages, plus type-specific payload). 3. **Encoders / decoders** — `encodeMessage(msg): Uint8Array`, `decodeMessage(bytes, offset): { msg, next }`. Round-trip property tests via fast-check. Handles running-status decode (when a stream omits the status byte and reuses the previous one). 4. **High-level dispatch** — `NRPNState` and `RPNState` reducers that accumulate the four-CC dance into a single `NRPNChange` / `RPNChange` event. Same for 14-bit CCs (MSB + LSB on pairs 0-31 / 32-63). 5. **SysEx framing** — `SysExFrame` type + universal manufacturer-id registry (Yamaha, Roland, Korg, NI, Ableton, generic non-commercial). `parseSysEx(bytes)`, `buildSysEx(manufacturer, deviceId, payload)`. 6. **Port contract** — `MidiPortLike { send(bytes): void; onMessage(handler): Unsubscribe; name: string; id: string; manufacturer?: string; state: "connected" | "disconnected" }`. Adapters in `@suaveplan/react-midi` (Web MIDI) and future `@suaveplan/midi-node` (node-midi) implement this. 7. **MIDI Clock helpers** — `clockToBpm(intervalsMs[]): number`, `bpmToClock(bpm): clockMs`. 24 PPQN per the spec. 8. **General MIDI tables** — GM Level 1 (128 patches + 128 percussion notes), GM2 extensions, GS/XG dialects as separate exported tables. 9. **MIDI 2.0 UMP** — Universal MIDI Packet framing types (`UMP32`, `UMP64`, `UMP96`, `UMP128`), high-resolution velocity/CC, per-note controllers. Encode/decode + bridge helpers (`midi1ToUmp`, `umpToMidi1` where lossless). 10. **Error codes** — `MIDI.{INVALID_MESSAGE, INVALID_SYSEX, BUFFER_UNDERRUN, UNSUPPORTED_UMP_VARIANT, NRPN_INCOMPLETE}` via `defineCodes`. 11. **OTel instrumentation** — counter `midi_messages_decoded_total{type}`, histogram `midi_decode_duration_ns{type}`. ## Impact - New package; nothing else changes today. - Unblocks `@suaveplan/react-midi` (browser Web MIDI adapter), `@suaveplan/audio-fx-plugin-spessasynth` (consumes MIDI for SF2 playback), `@suaveplan/control-surface-schema` (bindings can reference MIDI CC/note ids), `@suaveplan/network-midi` (transport over WebRTC/WebSocket). - Zero external runtime deps beyond `@suaveplan/error` + `@suaveplan/types` + `tslib` + `zod`. ## Out of scope (later changes) - Web MIDI API access — `@suaveplan/react-midi`. - Node MIDI access — `@suaveplan/midi-node` (Wave 3+). - Wire transport — `@suaveplan/network-midi`. - SoundFont loading — `@suaveplan/audio-fx-plugin-spessasynth`.