Files
suaveplan/packages/media/audio/midi/src/dispatch/rpn.md
2026-05-27 23:47:53 +01:00

2.5 KiB

dispatch/rpn

Purpose

RPNReducer is the sister of NRPNReducer for Registered Parameter Numbers — the parameter MSB and LSB CC numbers move from 99/98 to 101/100, but the rest of the four-CC dance is identical. RPN is the MIDI Manufacturers Association's way of standardising a handful of common parameters (pitch-bend range, fine/coarse tuning, modulation-depth range, MPE configuration) that every spec-compliant synth interprets identically.

Separating RPN from NRPN avoids cross-contamination: an RPNReducer will never accidentally interpret CC 99 as a parameter MSB, and vice versa. Consumers typically run both reducers in parallel on the same control-change stream.

Features

  • Per-channel partial state, exactly mirroring NRPNReducer.
  • Emits one RPNChangeEvent on CC 38 (data LSB) once the parameter pair is set.
  • Throws MidiError("MIDI.NRPN_INCOMPLETE") when data arrives before the parameter MSB/LSB.
  • Ignores unrelated CCs cleanly — composes with other CC-aware reducers without interference.

Usage: Basic

import { RPNReducer } from "@suaveplan/midi";

const r = new RPNReducer();
r.consume({ type: "controlChange", channel: 0, controller: 101, value: 0 });
r.consume({ type: "controlChange", channel: 0, controller: 100, value: 0 });
r.consume({ type: "controlChange", channel: 0, controller: 6, value: 2 });
const evt = r.consume({ type: "controlChange", channel: 0, controller: 38, value: 0 });
// evt.value = 0x100 (pitch-bend range = 2 semitones, coarse)

Usage: Advanced

import { NRPNReducer, RPNReducer } from "@suaveplan/midi";

const nrpn = new NRPNReducer();
const rpn = new RPNReducer();

decoder.onMessage((m) => {
  if (m.type !== "controlChange") return;
  const rpnEvt = rpn.consume(m);
  if (rpnEvt) {
    handleRpn(rpnEvt);
    return; // RPN takes priority
  }
  const nrpnEvt = nrpn.consume(m);
  if (nrpnEvt) handleNrpn(nrpnEvt);
});

API Reference

  • class RPNReducer
    • consume(msg: ControlChangeMessage): RPNChangeEvent | undefined
    • reset(): void
  • RPNChangeEvent { type: "rpnChange"; channel; paramHigh; paramLow; value }.

Implementation Notes

Because RPN does not specify increment/decrement shortcuts in the same canonical form as NRPN (the MMA's recommendation is that vendors generally avoid them for RPN), this reducer omits CC 96/97 handling — adding them would invite confusion when both reducers run on the same stream. Vendors that do support RPN inc/dec can layer their own behaviour on top by inspecting CC 96/97 outside this class.