Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

MIDI I/O

Supported bindings: ossia, vst, vst3, clap

Some media systems may have a concept of MIDI input / output. Note that currently this is only implemented for DAW-ish bindings: ossia, VST3, CLAP… Max and Pd do not support it yet (but if there is a standard for passing MIDI messages between objects there I’d love to hear about it !).

There are a few ways to specify MIDI ports.

Here is how one specifies unsafe MIDI ports:

struct
{
  static consteval auto name() { return "MIDI"; }
  struct
  {
    uint8_t bytes[3]{};
    int timestamp{}; // relative to the beginning of the tick
  }* midi_messages{};
  std::size_t size{};
} midi_port;

Or, more clearly:

// the name does not matter
struct midi_message {
  uint8_t bytes[3]{};
  int timestamp{}; // relative to the beginning of the tick
};

struct
{
  static consteval auto name() { return "MIDI"; }
  midi_message* midi_messages{};
  std::size_t size{};
} midi_port;

Here, Avendish bindings will allocate a large enough buffer to store MIDI messages ; this is mainly to enable writing dynamic-allocation-free backends where such a buffer may be allocated statically.

It is also possible to do this if you don’t expect to run your code on Arduinos:

struct
{
  // Using a non-fixed size type here will enable MIDI messages > 3 bytes, if for instance your 
  // processor expects to handle SYSEX messages.
  struct msg {
    std::vector<uint8_t> bytes;
    int64_t timestamp{};
  };

  std::vector<msg> midi_messages;
} midi_port;

Helpers

The library provides helper types which are a good compromise between these two solutions, as they are based on boost::container::small_vector: for small numbers of MIDI messages, there will be no memory allocation, but pathological cases (an host sending a thousand MIDI messages in a single tick) can still be handled without loosing messages.

The type is very simple:

halp::midi_bus<"In"> midi;

Message lifetime and sliced ossia execution

MIDI ports are scratch storage for the current invocation. Keep pending starts, active-note identity, future release deadlines, and cleanup debt in the processor, not in a port. Do not retain pointers, references, or iterators into its messages. Output timestamps are slice-relative and must lie in [0, frames); the ossia binding adds the slice offset once without changing the processor’s timestamp. A one-sample note starting on the last frame therefore releases at frame zero of the next writable invocation. A zero-frame invocation cannot publish that release.

The ossia graph initializes native ports, runs all requested token slices, then publishes the node’s aggregate output. A buffer holds several slices when a process loops inside it, or when a process stopped inside it runs its last partial slice and then an empty one. The binding keeps the MIDI of all the slices of a buffer: it stores the already-produced packets in executor-local storage and rebuilds the native output from them after each slice, even an empty one. The ossia::graph_node::begin_execution() hook resets this storage before each graph execution, independently of seeks, repeated positions, callback counters, and buffer sizes. Direct users of safe_node::run() must call begin_execution() once before each group of slices they will publish together.

The storage of each MIDI output, and the native output port, reserve the buffer size in packets. A processor that sends more can raise it, or go back to each slice replacing the output of the previous one:

halp_meta(local_midi_tick_batch_reserve, 4096) // Per-MIDI-port reserve.
halp_meta(local_midi_tick_batch, false)       // Per-slice replacement.

A reserve is a capacity hint, not a bound or an allocation-free guarantee for arbitrary host traffic.

A lifecycle callback such as stop() is not itself a writable publication window. A host must run and publish a final release-only invocation before disconnecting or destroying a processor that owns notes. In score’s current hard-stop/removal path, such an invocation is not automatically supplied; locally queued cleanup releases alone cannot guarantee delivered note-offs.