first commit
This commit is contained in:
59
docs/media/audio/midi/src/dispatch/nrpn.md
Normal file
59
docs/media/audio/midi/src/dispatch/nrpn.md
Normal 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.
|
||||
Reference in New Issue
Block a user