import type { CC14Change, Channel, ControlChangeMessage, NRPNChange, RPNChange, } from "./midi-message.types.js"; const CC_NRPN_HIGH = 99; const CC_NRPN_LOW = 98; const CC_RPN_HIGH = 101; const CC_RPN_LOW = 100; const CC_DATA_HIGH = 6; const CC_DATA_LOW = 38; interface PartialState { paramHigh: number | null; paramLow: number | null; valueHigh: number | null; valueLow: number | null; } function emptyState(): PartialState { return { paramHigh: null, paramLow: null, valueHigh: null, valueLow: null, }; } /** * NRPN four-CC reducer. Feed channel-voice CC messages into `feed`; * when CC 99/98/6/38 have all arrived on the same channel, the * reducer emits a single `NRPNChange` event and resets that channel's * partial state. Incomplete dances (e.g. missing LSB) remain pending * until completion or are dropped when a fresh paramHigh arrives. */ export class NRPNReducer { private readonly partials = new Map(); feed(msg: ControlChangeMessage): NRPNChange | null { const state = this.partials.get(msg.channel) ?? emptyState(); switch (msg.controller) { case CC_NRPN_HIGH: state.paramHigh = msg.value; state.paramLow = null; state.valueHigh = null; state.valueLow = null; break; case CC_NRPN_LOW: state.paramLow = msg.value; break; case CC_DATA_HIGH: state.valueHigh = msg.value; break; case CC_DATA_LOW: state.valueLow = msg.value; break; default: this.partials.set(msg.channel, state); return null; } this.partials.set(msg.channel, state); if ( state.paramHigh !== null && state.paramLow !== null && state.valueHigh !== null && state.valueLow !== null ) { const out: NRPNChange = { type: "nrpnChange", channel: msg.channel, paramHigh: state.paramHigh, paramLow: state.paramLow, value: (state.valueHigh << 7) | state.valueLow, }; this.partials.set(msg.channel, emptyState()); return out; } return null; } } /** * RPN four-CC reducer. Identical control flow as `NRPNReducer` but * uses CC 101/100 for the parameter MSB/LSB selectors. */ export class RPNReducer { private readonly partials = new Map(); feed(msg: ControlChangeMessage): RPNChange | null { const state = this.partials.get(msg.channel) ?? emptyState(); switch (msg.controller) { case CC_RPN_HIGH: state.paramHigh = msg.value; state.paramLow = null; state.valueHigh = null; state.valueLow = null; break; case CC_RPN_LOW: state.paramLow = msg.value; break; case CC_DATA_HIGH: state.valueHigh = msg.value; break; case CC_DATA_LOW: state.valueLow = msg.value; break; default: this.partials.set(msg.channel, state); return null; } this.partials.set(msg.channel, state); if ( state.paramHigh !== null && state.paramLow !== null && state.valueHigh !== null && state.valueLow !== null ) { const out: RPNChange = { type: "rpnChange", channel: msg.channel, paramHigh: state.paramHigh, paramLow: state.paramLow, value: (state.valueHigh << 7) | state.valueLow, }; this.partials.set(msg.channel, emptyState()); return out; } return null; } } /** * 14-bit CC reducer. Pairs CCs 0-31 with CCs 32-63 (MSB+LSB pair on * the same channel). On reception of either half, the reducer * remembers the partial value and, once both halves are observed, * emits a single `CC14Change` event with the combined 14-bit value. */ export class CC14Reducer { private readonly msbs = new Map(); private readonly lsbs = new Map(); feed(msg: ControlChangeMessage): CC14Change | null { const isMsb = msg.controller < 32; const isLsb = msg.controller >= 32 && msg.controller < 64; if (!isMsb && !isLsb) { return null; } const baseController = isMsb ? msg.controller : msg.controller - 32; const key = `${msg.channel}:${baseController}`; if (isMsb) { this.msbs.set(key, msg.value); } else { this.lsbs.set(key, msg.value); } const msb = this.msbs.get(key); const lsb = this.lsbs.get(key); if (msb !== undefined && lsb !== undefined) { const out: CC14Change = { type: "cc14Change", channel: msg.channel, controller: baseController, value: (msb << 7) | lsb, }; this.msbs.delete(key); this.lsbs.delete(key); return out; } return null; } }