4.1 KiB
@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
- NEW package
@suaveplan/midiatpackages/media/audio/midi/. - Message types —
MidiMessagediscriminated 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 ornullfor system messages, plus type-specific payload). - 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). - High-level dispatch —
NRPNStateandRPNStatereducers that accumulate the four-CC dance into a singleNRPNChange/RPNChangeevent. Same for 14-bit CCs (MSB + LSB on pairs 0-31 / 32-63). - SysEx framing —
SysExFrametype + universal manufacturer-id registry (Yamaha, Roland, Korg, NI, Ableton, generic non-commercial).parseSysEx(bytes),buildSysEx(manufacturer, deviceId, payload). - 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. - MIDI Clock helpers —
clockToBpm(intervalsMs[]): number,bpmToClock(bpm): clockMs. 24 PPQN per the spec. - General MIDI tables — GM Level 1 (128 patches + 128 percussion notes), GM2 extensions, GS/XG dialects as separate exported tables.
- 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,umpToMidi1where lossless). - Error codes —
MIDI.{INVALID_MESSAGE, INVALID_SYSEX, BUFFER_UNDERRUN, UNSUPPORTED_UMP_VARIANT, NRPN_INCOMPLETE}viadefineCodes. - OTel instrumentation — counter
midi_messages_decoded_total{type}, histogrammidi_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.