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
NRPNChangeEventper 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 NRPNReducerconsume(msg: ControlChangeMessage): NRPNChangeEvent | undefined— feed a CC; returns the consolidated event when the dance completes (CC 38, CC 96, CC 97),undefinedotherwise.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.