# midi-router _(no submodules)_ ## Purpose Routes `MidiMessage` events from a `MidiPortLike` (or a direct `handle()` call) into the upstream `SpessaSynthProcessor`, with note-on/note-off mediated through the `VoiceManager` so the polyphony cap and per-channel voice tracking stay coherent. NRPN/RPN parameter writes are reassembled in software (per the MIDI 1.0 spec — MSB/LSB selects + data MSB/LSB or inc/dec data entry) and surfaced to consumers via an `onParameterChange` callback in addition to being forwarded as raw CCs. The router is lifecycle-detached at construction and `attach()`-ed inside `SpessaSynthPlugin.setup()`. Messages received before attach are dropped (the plugin's setup guarantees the call ordering, and the alternative — buffering — risks confusing the host with late timing). ## Features - All MIDI 1.0 channel-voice messages: noteOn (velocity-0 demoted to noteOff), noteOff, controlChange, programChange, pitchBend, channelPressure, polyPressure. - System Exclusive forwarded via `processor.systemExclusive`. - NRPN/RPN reassembly with state per channel: CC#99/98 (NRPN MSB/LSB), CC#101/100 (RPN MSB/LSB), CC#6/38 (data MSB/LSB), CC#96/97 (data inc/dec). - `onParameterChange(event)` callback fires on each data-write phase so consumers can observe high-resolution parameter writes (per-note tuning, master fine-tune, modulation depth range, etc.). - Multiple `MidiPortLike` connections; each `connectPort` call returns an unsubscribe. - Both port-driven (`connectPort`) and direct-call (`handle`) entry points. ## Usage: Basic ```ts import { MidiRouter } from "@suaveplan/audio-fx-plugin-spessasynth/midi-router"; const router = new MidiRouter(); router.attach(processor, voiceManager); router.handle({ type: "noteOn", channel: 0, note: 60, velocity: 100 }); ``` ## Usage: Advanced ```ts import { MidiRouter } from "@suaveplan/audio-fx-plugin-spessasynth/midi-router"; const router = new MidiRouter({ onParameterChange(event) { if ( event.kind === "nrpn" && event.paramHigh === 0x65 && event.paramLow === 0x4c ) { logger.debug("per-note tuning update", event); } }, }); router.attach(processor, voiceManager); const disconnect = router.connectPort(midiInputPort); // later disconnect(); ``` ## API Reference - `class MidiRouter` - `constructor(options?: MidiRouterOptions)` - `attach(processor, voiceManager): void` - `detach(): void` - `connectPort(port: MidiPortLike): () => void` - `handle(message: MidiMessage): void` - `interface MidiRouterOptions { onParameterChange?(event): void }` - `interface ParameterChangeEvent { kind: "nrpn" | "rpn", channel, paramHigh, paramLow, dataHigh, dataLow }` ## Implementation Notes The NRPN/RPN reassembly intentionally fires `onParameterChange` on each of CC#6, CC#38, CC#96, CC#97 — not just on a "complete" pair — because the MIDI spec doesn't define what "complete" means: some controllers send only MSB, some send LSB-then-MSB, some send inc/dec sequences. Surfacing every data-write phase lets observers do whatever they want, including debouncing. The raw CC is also forwarded to the synth on every controller change so the upstream library's own NRPN handler still runs — software reassembly is an additional read-side hook, not a replacement. Messages arriving before `attach()` are silently dropped rather than buffered; the plugin's setup() ordering makes this unreachable in normal use, and silent drop is safer than late playback if a consumer mis-wires the lifecycle.