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

3.5 KiB

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

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

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.