# dispatch/cc14 ## Purpose `CC14Reducer` accumulates paired MIDI 1.0 control-change messages into 14-bit values. The wire convention is: CCs 0..31 carry the most-significant byte of a controller, CCs 32..63 carry the least-significant byte of the *same* logical controller (`lsbController = msbController + 32`). Sending MSB then LSB lets the receiver follow value changes smoothly across a 14-bit range — far more useful than the 7-bit single-CC form for parameters like volume, expression, and pan. The reducer maintains per-channel, per-controller state and emits one `CC14ChangeEvent` on every LSB arrival, regardless of whether the MSB has been seen yet (controllers that send LSB-only updates simply use the last seen MSB, or zero on the first message). ## Features - Per-channel, per-MSB-controller partial state. - Emits a consolidated `CC14ChangeEvent` on every CC 32..63 arrival. - Ignores controllers outside 0..63 — they belong to NRPN/RPN dispatch or are 7-bit-only. - `reset()` to clear state on port reset. ## Usage: Basic ```ts import { CC14Reducer } from "@suaveplan/midi"; const r = new CC14Reducer(); r.consume({ type: "controlChange", channel: 0, controller: 7, value: 0x40 }); // MSB const evt = r.consume({ type: "controlChange", channel: 0, controller: 39, value: 0x05 }); // LSB // evt = { type: "cc14Change", channel: 0, controller: 7, value: (0x40 << 7) | 0x05 } ``` ## Usage: Advanced ```ts import { CC14Reducer } from "@suaveplan/midi"; const reducer = new CC14Reducer(); // Combine with the running-status decoder to extract smooth volume curves. decoder.onMessage((m) => { if (m.type !== "controlChange") return; const evt = reducer.consume(m); if (evt && evt.controller === 7) { mixer.setVolume(evt.channel, evt.value / 16383); } }); ``` ## API Reference - `class CC14Reducer` - `consume(msg: ControlChangeMessage): CC14ChangeEvent | undefined` - `reset(): void` - `CC14ChangeEvent { type: "cc14Change"; channel; controller; value }` — `controller` is the MSB CC number (0..31). ## Implementation Notes State is keyed by a composite `${channel}:${msbController}` string so a single `Map` covers all 16 channels × 32 controllers without nested maps. The reducer treats an LSB without a preceding MSB as `msb = 0`, matching the behaviour of every consumer-grade synth tested against the spec. Out-of-range controllers (≥ 64) are silently ignored rather than throwing — they have legitimate uses (sustain pedal, sostenuto, NRPN parameter MSB) that this reducer should not interfere with.