Files
skills/docs/media/audio/midi/src/dispatch/nrpn.md
2026-05-29 08:20:56 +01:00

2.9 KiB

dispatch/nrpn

Purpose

NRPNReducer accumulates the four-CC "dance" that MIDI 1.0 uses to express Non-Registered Parameter Number changes into a single, consolidated NRPNChangeEvent. The dance is: send CC 99 (parameter MSB), CC 98 (parameter LSB), CC 6 (data MSB), then CC 38 (data LSB). The reducer absorbs the first three and emits exactly one event on the fourth — much easier for downstream synth-parameter routers than reacting to each CC individually.

It also handles the data-increment (CC 96) and data-decrement (CC 97) shortcuts, which apply deltas to the currently addressed parameter without re-sending the parameter MSB/LSB. The reducer maintains per-channel state so 16 simultaneous parameter conversations interleave correctly on a single port.

Features

  • Per-channel partial state — 16 conversations multiplex without interference.
  • Emits exactly one NRPNChangeEvent per complete CC 6 + CC 38 pair.
  • Honours CC 96 (increment) / CC 97 (decrement) shortcuts with 14-bit value clamping.
  • Throws MidiError("MIDI.NRPN_INCOMPLETE") when data arrives before a parameter pair.

Usage: Basic

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

const r = new NRPNReducer();
r.consume({ type: "controlChange", channel: 0, controller: 99, value: 0x20 });
r.consume({ type: "controlChange", channel: 0, controller: 98, value: 0x10 });
r.consume({ type: "controlChange", channel: 0, controller: 6, value: 0x40 });
const evt = r.consume({ type: "controlChange", channel: 0, controller: 38, value: 0x00 });
// evt = { type: "nrpnChange", channel: 0, paramHigh: 0x20, paramLow: 0x10, value: 0x2000 }

Usage: Advanced

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

const r = new NRPNReducer();

decoder.onMessage((m) => {
  if (m.type !== "controlChange") return;
  let evt: NRPNChangeEvent | undefined;
  try {
    evt = r.consume(m);
  } catch (err) {
    logger.warn("nrpn dance broken", { cause: err });
    r.reset();
    return;
  }
  if (evt) parameterStore.set(`${evt.paramHigh}/${evt.paramLow}`, evt.value);
});

API Reference

  • class NRPNReducer
    • consume(msg: ControlChangeMessage): NRPNChangeEvent | undefined — feed a CC; returns the consolidated event when the dance completes (CC 38, CC 96, CC 97), undefined otherwise.
    • reset(): void — clear all per-channel state.
  • NRPNChangeEvent { type: "nrpnChange"; channel; paramHigh; paramLow; value } — the emitted event.

Implementation Notes

Parameter state is keyed by channel number in a Map, lazily populated by slot(channel). Increment/decrement use saturation arithmetic against the 14-bit range [0, 16383] — overflow past 16383 stays at 16383, underflow past 0 stays at 0. Data-MSB without an LSB writes value = msb << 7; this matches the convention that most synths sample value on every CC 38 LSB but commit the MSB independently for chunky sweeps.