# 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 ```ts 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 ```ts 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.