Initial Commit
This commit is contained in:
173
packages/ui/react/react-midi/src/midi-contract/reducers.ts
Normal file
173
packages/ui/react/react-midi/src/midi-contract/reducers.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
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<Channel, PartialState>();
|
||||
|
||||
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<Channel, PartialState>();
|
||||
|
||||
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<string, number>();
|
||||
private readonly lsbs = new Map<string, number>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user