Skip to content
Haiku OSDeep Dive Published Updated 5 min readViews unavailable

Haiku MIDI Kit: Roster-Based Endpoints, Connections, and Timestamped Events

How Haiku's MIDI Kit registers producers and consumers, connects local and remote endpoints, schedules events, and manages latency and lifetime.

A MIDI application needs more than a function that writes three bytes to a device. Hardware inputs, software sequencers, synthesizers, and monitors must discover one another, form connections, preserve event timing, and disappear without leaving stale pointers. Haiku’s MIDI Kit models those participants as producers and consumers coordinated by a system-wide roster.

The current endpoint API is often called the MIDI 2 Kit in Haiku’s class documentation, but it carries familiar MIDI messages rather than referring to the later MIDI 2.0 wire protocol. BMidiProducer emits events, BMidiConsumer receives them, and BMidiRoster publishes and discovers endpoints across applications.

Endpoints have local objects and remote proxies

An application creates a BMidiLocalProducer or subclasses BMidiLocalConsumer for code it owns. Once registered, other applications can discover that endpoint. In their processes, the roster returns endpoint objects that can be remote proxies rather than the original C++ object.

BMidiEndpoint exposes an integer ID, name, properties, validity, and checks for producer, consumer, local, or remote status. An ID identifies a current roster endpoint, not an eternal hardware identity. Devices and applications can disconnect and later return under different objects.

Call Acquire() and Release() according to the documented reference lifecycle. Also check IsValid() because a remote endpoint can disappear while a UI still displays it.

The roster is the discovery authority

BMidiRoster::MidiRoster() returns the roster instance. Iteration methods enumerate producers, consumers, or all endpoints. Find methods resolve an endpoint by ID, with an option to limit lookup to local objects.

Build selection UIs from a snapshot, but subscribe to roster change notifications through the API’s watching mechanism so devices appear and disappear without a full application restart. Reconcile by ID and endpoint role, and remove a selection when the endpoint becomes invalid.

Names are labels, not keys. Two USB devices can report the same name. Show useful properties and let the user choose, then persist a best-effort hardware signature or application-specific preference that can be rematched safely.

Registration makes a local endpoint visible

Create the local endpoint, give it a meaningful name and properties, register it with the roster, then connect it. If registration fails, keep it local or report that it cannot participate system-wide. Do not advertise success merely because the C++ object exists.

class Monitor : public BMidiLocalConsumer {
public:
    Monitor() : BMidiLocalConsumer("Event Monitor") {}

    void NoteOn(uchar channel, uchar note,
        uchar velocity, bigtime_t when) override
    {
        QueueNote(channel, note, velocity, when);
    }
};

Consumer hooks should enqueue compact events and return. Rendering a window, writing a large log, or waiting on a network service in the callback can add jitter and delay later MIDI events.

Producers connect explicitly to consumers

BMidiProducer::Connect() establishes a route to a consumer, and Disconnect() removes it. A producer may have multiple consumers, so one keyboard can feed a synthesizer and monitor at once. The producer’s connection list is changing shared state and should not be cached indefinitely.

Check every connection result and handle the consumer becoming invalid. On teardown, disconnect routes before unregistering and releasing local endpoints. A connection is not ownership of the other application’s lifetime.

Feedback loops are possible. A processor that republishes everything it consumes can be connected back to itself through other endpoints. Include a routing model that detects or deliberately bounds loops instead of relying on users never to create one.

Events carry a performance time

Local producers use SprayNoteOn(), SprayControlChange(), SpraySystemExclusive(), and related methods to send events to connected consumers. Each event accepts a bigtime_t time. Consumers receive that time in their virtual callback.

Use the time domain documented by the Kit and preserve the original timestamp through transformations. Setting every event to zero may request immediate behavior, but it discards sequencing information and makes downstream jitter correction impossible.

A sequencer should schedule ahead by a bounded interval rather than sleeping until each note and then publishing late. A live input bridge should add only the buffering needed for stable delivery. Measure timestamp error at the consumer, not merely time spent in the producer function.

Consumer latency informs scheduling

BMidiConsumer exposes a latency value, and a local consumer can publish its latency. A producer or routing application can account for that delay when scheduling events intended to be heard at a particular performance time.

Latency is not a promise that every callback completes within that time. CPU overload, driver behavior, and downstream audio buffering still introduce variation. Update the declared value when the processing path changes, and expose underrun or late-event counters.

Avoid compensating twice. If a sequencer subtracts consumer latency and an intermediate router also shifts timestamps without declaring that behavior, notes arrive early. Define which component owns end-to-end compensation.

System Exclusive data needs strict bounds

System Exclusive messages can be much larger than channel messages and may contain device-specific commands. A local consumer receives a pointer and length for the callback. Validate length, copy only when data must outlive the call, and impose a maximum suitable for the application.

Do not send unknown SysEx to hardware automatically. It may alter firmware, memory, or global device state. Require an explicit route and device profile. Logs should show size and a bounded prefix, not dump private patch data without consent.

Raw Data() events also declare whether a block is atomic. Preserve message boundaries when forwarding and do not split atomic content merely to fit an internal queue without defining reassembly.

Lifecycle tests matter more than a single note

Test hot-plug, producer crash, consumer crash, repeated register and unregister, duplicate names, connection refusal, rapid reconnect, late timestamps, very large SysEx, and an intentional feedback loop. Run under CPU and audio load to expose queue growth and timestamp drift.

Persist user routing as intent, not as bare endpoint IDs. On startup, discover current endpoints, match conservatively, ask when ambiguous, and connect only after both roles are valid. Make a disconnected route visible rather than silently sending into nowhere.

Haiku’s MIDI Kit turns musical data into a roster-managed graph. The graph remains reliable when applications respect remote endpoint lifetime, keep callbacks short, preserve timestamp meaning, account for declared latency, and treat every connection as a relationship that can vanish while the music is playing.

Related:

Sources:

Comments