first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,59 @@
# 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.