first commit
This commit is contained in:
57
docs/media/audio/midi/src/message/decode.md
Normal file
57
docs/media/audio/midi/src/message/decode.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# message/decode
|
||||
|
||||
## Purpose
|
||||
|
||||
`decodeMessage` parses one MIDI 1.0 message starting at a given offset in a `Uint8Array` and returns both the typed message and the next-offset to feed back into the decoder. It is the byte-level inverse of `encodeMessage`, designed to compose with the streaming `RunningStatusDecoder` for serial-MIDI inputs where status bytes are sometimes omitted.
|
||||
|
||||
The decoder rejects malformed input loudly: a data byte arriving without a prior status byte (and no `runningStatus` argument) throws `MidiError("MIDI.INVALID_MESSAGE")`; a SysEx frame missing its `F7` terminator throws `MidiError("MIDI.INVALID_SYSEX")`; a buffer that ends mid-message throws `MidiError("MIDI.BUFFER_UNDERRUN")`. This is deliberate — silent skipping is how stuck notes appear in production.
|
||||
|
||||
## Features
|
||||
|
||||
- Parses every one of the 18 MIDI 1.0 message variants into the discriminated `MidiMessage` union.
|
||||
- Optional `runningStatus` parameter for parsers that already know the status byte was omitted.
|
||||
- Strict data-byte high-bit checks — surfacing transport-level corruption rather than silently producing wrong notes.
|
||||
- Returns the `next` offset so consumers can drive a single buffer through repeated calls.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { decodeMessage } from "@suaveplan/midi";
|
||||
|
||||
const { msg, next } = decodeMessage(new Uint8Array([0x91, 64, 100]), 0);
|
||||
// msg = { type: "noteOn", channel: 1, note: 64, velocity: 100 }, next = 3
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { decodeMessage } from "@suaveplan/midi";
|
||||
|
||||
function decodeAll(bytes: Uint8Array) {
|
||||
const messages = [];
|
||||
let offset = 0;
|
||||
let runningStatus: number | undefined;
|
||||
while (offset < bytes.length) {
|
||||
const first = bytes[offset]!;
|
||||
const { msg, next } = decodeMessage(bytes, offset, runningStatus);
|
||||
messages.push(msg);
|
||||
if (first >= 0x80 && first < 0xf0) runningStatus = first;
|
||||
if (first >= 0xf0 && first < 0xf8) runningStatus = undefined;
|
||||
offset = next;
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `decodeMessage(bytes: Uint8Array, offset: number, runningStatus?: number): DecodeResult`
|
||||
- `bytes` — buffer containing the serial MIDI stream.
|
||||
- `offset` — index of the first byte to consume.
|
||||
- `runningStatus` — optional last-seen status byte, used when `bytes[offset] < 0x80`.
|
||||
- Returns `{ msg: MidiMessage; next: number }` — `next` is the index immediately past the consumed bytes (for SysEx, this is the position after `F7`).
|
||||
- `DecodeResult` — the return type, exported for callers building their own dispatchers.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The decoder splits into three internal helpers: `decodeChannelVoice` handles statuses `0x8X..0xEX`, `decodeSystem` handles `0xF0..0xFF`, and `decodeSysEx` walks the variable-length F0..F7 payload. `read7` enforces the high-bit-clear invariant on every data byte read so we surface `INVALID_MESSAGE` immediately rather than producing a corrupted value. SysEx returns a slice (not a view) of the payload to insulate callers from later buffer reuse.
|
||||
61
docs/media/audio/midi/src/message/encode.md
Normal file
61
docs/media/audio/midi/src/message/encode.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# message/encode
|
||||
|
||||
## Purpose
|
||||
|
||||
`encodeMessage` converts a strongly-typed `MidiMessage` value into the byte sequence a MIDI 1.0 wire format expects. It is the canonical serialisation function for the package and is shared by the streaming dispatcher, the UMP bridge (for status-byte derivation), and any consumer writing to a `MidiPortLike`.
|
||||
|
||||
The encoder validates every payload field at the type-boundary: channels must be 0..15, 7-bit data must be 0..127, 14-bit values (pitch bend, song position) must be 0..16383. Out-of-range values raise `MidiError("MIDI.INVALID_MESSAGE")` rather than silently masking bits, because silent masking is a leading cause of "the synth jumped an octave when I sent the wrong velocity" bugs.
|
||||
|
||||
## Features
|
||||
|
||||
- Pure function returning a freshly allocated `Uint8Array`; no shared buffers, safe to call concurrently.
|
||||
- Validates channel, 7-bit, and 14-bit ranges; rejects non-integer values.
|
||||
- Exposes the `STATUS` byte-constant table for the decoder and UMP bridge.
|
||||
- SysEx framing automatically inserts the `F0` prefix and `F7` terminator.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { encodeMessage } from "@suaveplan/midi";
|
||||
|
||||
const bytes = encodeMessage({
|
||||
type: "noteOn",
|
||||
channel: 0,
|
||||
note: 60,
|
||||
velocity: 100,
|
||||
});
|
||||
// Uint8Array(3) [0x90, 60, 100]
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { encodeMessage, STATUS } from "@suaveplan/midi";
|
||||
|
||||
// Compose a series of messages into a single buffer for batched send.
|
||||
const messages = [
|
||||
{ type: "controlChange" as const, channel: 0, controller: 7, value: 100 },
|
||||
{ type: "pitchBend" as const, channel: 0, value: 8192 },
|
||||
{ type: "noteOn" as const, channel: 0, note: 64, velocity: 127 },
|
||||
];
|
||||
const chunks = messages.map(encodeMessage);
|
||||
const total = chunks.reduce((n, c) => n + c.length, 0);
|
||||
const out = new Uint8Array(total);
|
||||
let offset = 0;
|
||||
for (const c of chunks) {
|
||||
out.set(c, offset);
|
||||
offset += c.length;
|
||||
}
|
||||
|
||||
port.send(out);
|
||||
expect(STATUS.NOTE_ON).toBe(0x90);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `encodeMessage(msg: MidiMessage): Uint8Array` — returns the bytes for any of the 18 message variants. Throws `MidiError("MIDI.INVALID_MESSAGE")` on out-of-range fields.
|
||||
- `STATUS` — frozen object exposing every status-byte constant the encoder writes (`NOTE_OFF`, `NOTE_ON`, `POLY_PRESSURE`, ..., `SYSEX_START`, `SYSEX_END`, `CLOCK`, etc.).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The encoder uses `Uint8Array.of(...)` rather than `new Uint8Array(n)` + index assignments because `of` accepts the elements directly and lets V8/JSCore use the inline cache for small arrays. Field assertions are inlined helpers (`assertChannel`, `assert7`, `assert14`) to keep the branch density low — every variant pays at most three boundary checks plus the `Uint8Array.of` call. SysEx allocates `payload.length + 2` to leave room for the F0/F7 endpoints.
|
||||
Reference in New Issue
Block a user