Files
skills/docs/media/audio/midi/src/dispatch/running-status.md
2026-05-29 08:20:56 +01:00

2.9 KiB

dispatch/running-status

Purpose

RunningStatusDecoder is the streaming MIDI 1.0 decoder that honours running status — the wire convention where successive same-status messages omit redundant status bytes. The MIDI 1.0 spec mandates that consumers retain the last channel-voice status byte and apply it implicitly to following data-only frames; serial-MIDI hardware almost universally exploits this to save bandwidth.

This module also handles the orthogonal real-time interleaving rule: any byte in the range 0xF8..0xFF is a one-byte real-time message that can appear at any moment, even mid-frame, without disturbing the running status state. Implementing both rules correctly is the difference between a stable port adapter and one that drops one note in twenty.

Features

  • Stateful streaming decoder — feed bytes incrementally via push, drain complete messages via drain.
  • Honours running status across multiple push calls; partial data accumulates until a complete frame arrives.
  • Splits real-time messages out without affecting the running status pointer.
  • Clears running status after every system-common message, matching the MIDI 1.0 spec.
  • decodeStream convenience wrapper for one-shot decoding of a finished buffer.

Usage: Basic

import { decodeStream } from "@suaveplan/midi";

const messages = decodeStream([0x91, 0x40, 0x64, 0x42, 0x55]);
// two noteOn events on channel 1: notes 64+100 and 66+85

Usage: Advanced

import { RunningStatusDecoder } from "@suaveplan/midi";

const decoder = new RunningStatusDecoder();
inputPort.onRawBytes((chunk) => {
  decoder.push(chunk);
  for (const msg of decoder.drain()) {
    handleMessage(msg);
  }
});

// When the port disconnects and reconnects, reset the implicit state.
inputPort.onDisconnect(() => decoder.reset());

API Reference

  • class RunningStatusDecoder
    • push(bytes: Uint8Array | readonly number[]): void — append bytes to the internal queue.
    • drain(): MidiMessage[] — return every complete message available; partial frames remain queued for the next call.
    • reset(): void — clear the buffer and running-status pointer (use on port reconnect).
  • decodeStream(bytes: Uint8Array | readonly number[]): MidiMessage[] — convenience for one-shot decoding when the entire stream is known up-front.

Implementation Notes

The decoder uses a number[] buffer rather than a Uint8Array because it needs shift / splice semantics that Uint8Array does not provide; the alternative is a ring buffer with explicit head/tail pointers, but for typical port chunk sizes (≤ 64 bytes) the array form is faster and far less code. SysEx accumulates until the F7 terminator is found; if the terminator never arrives the messages stay queued and drain returns the messages decoded so far, with the partial SysEx still pending — callers can reset to drop it on a port reset.