Files
2026-05-27 23:29:53 +01:00

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

  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.