# `midi-contract/reducers` ## Purpose Three small reducers that fold the four-CC NRPN, RPN, and 14-bit CC "dances" into single consolidated events. The Web MIDI runtime delivers each CC as an isolated message; a knob that maps to a 14-bit parameter therefore arrives as two CC messages (MSB then LSB), and an NRPN edit arrives as four (CC 99/98 for parameter MSB/LSB then CC 6/38 for value MSB/LSB). The reducers track per-channel partial state and emit one `NRPNChange` / `RPNChange` / `CC14Change` per completed dance. This logic lives here so the `useNRPN` / `useRPN` / `useCC14` hooks have a deterministic, pure target to wrap with `useSyncExternalStore`. Once `@suaveplan/midi` ships its canonical reducers, these classes SHALL be replaced one-for-one without behavioural change. ## Features - `NRPNReducer` — CC 99/98 parameter MSB/LSB, CC 6/38 data MSB/LSB. - `RPNReducer` — CC 101/100 parameter MSB/LSB, CC 6/38 data MSB/LSB. - `CC14Reducer` — pairs CC 0-31 (MSB) with CC 32-63 (LSB). - Per-channel partial state — concurrent edits on different channels do not interleave. ## Usage: Basic ```ts import { NRPNReducer } from "@suaveplan/react-midi/midi-contract"; const reducer = new NRPNReducer(); const out = reducer.feed({ type: "controlChange", channel: 0, controller: 99, value: 0x20, }); // out === null (waiting for CC 98 / 6 / 38) ``` ## Usage: Advanced ```ts import { CC14Reducer } from "@suaveplan/react-midi/midi-contract"; const r = new CC14Reducer(); // 14-bit "bank select" on channel 0: const a = r.feed({ type: "controlChange", channel: 0, controller: 0, value: 1 }); const b = r.feed({ type: "controlChange", channel: 0, controller: 32, value: 64 }); // b === { type: "cc14Change", channel: 0, controller: 0, value: (1<<7)|64 } ``` ## API Reference - `class NRPNReducer { feed(msg: ControlChangeMessage): NRPNChange | null }` - `class RPNReducer { feed(msg: ControlChangeMessage): RPNChange | null }` - `class CC14Reducer { feed(msg: ControlChangeMessage): CC14Change | null }` ## Implementation Notes Reception of a fresh `paramHigh` (CC 99 / CC 101) resets all four partial slots for that channel — devices commonly start a fresh NRPN edit without an explicit "null param" sentinel, so the reducer treats a new paramHigh as the implicit "start of new dance" marker. The 14-bit CC reducer keys per `(channel, baseController)` so two simultaneous 14-bit edits on different channels do not corrupt each other's pending MSB.