Search guides and the APICtrl K
  1. API reference
  2. MIDI and OSC

midi_collector.h

MidiMessageCollector: MIDI from other threads, into the next audio block, at the sample position its arrival time implies.

#include <sonore/midi_collector.h>
On this page

Design notes

The problem

A MIDI driver delivers on its own thread, an on-screen keyboard on the UI thread, a network bridge on a socket thread. The audio thread renders in blocks and wants each event at a FRAME inside the block. midiin:: MessageQueue answers the first half -- a lock-free hand-over -- and puts everything at frame 0, which for a single USB keyboard is honest: its own jitter is already larger than a block.

It is not honest for everything. At a 2048-frame block (43 ms at 48 kHz, which hosts do use when they are rendering a heavy session) every note in a fast run lands on the same frame, and a roll becomes a chord. Sources whose timing is better than the block -- a sequencer on another thread, a network clock, a driver's own timestamps -- lose it all at frame 0.

What this does

Each event is stamped with a time in seconds when it is queued (now(), a steady clock every thread shares, unless the source has a better stamp of its own). Each block, the audio thread takes the interval since the previous block and maps it onto the block's frames:

  • an interval as long as the block maps one-to-one, so an event's offset into the interval IS its frame;
  • a SHORTER interval (a callback that came early) is laid against the END of the block, so the delay from arrival to sound stays one block however the host schedules its callbacks -- that constant block of latency is the price of removing the jitter, and the reference pays it the same way;
  • a LONGER interval (a late callback, or a stall) is squeezed into the block, keeping the events' order and relative spacing. Past 32 blocks only the most recent 32 blocks' worth keeps its spacing; anything older lands on frame 0. The reference DROPS those older events; here they are kept, because one of them may be a note-off and a lost note-off is a note that never stops.

The mapping is a pure function, samplePosition(), so the arithmetic is tested with numbers rather than with a thread and a stopwatch.

Threads

Any number of producers; one consumer, the audio thread. The queue is a bounded ring with a sequence number per slot (Vyukov's): a producer claims a slot with one compare-and-swap and never waits -- a full queue refuses, and counts it -- and the consumer never waits at all. Nothing allocates after construction. The reference's collector takes a lock in the audio callback; this one cannot, and the test suite counts allocations around the drain to keep it that way.

An event the block has no room for is LEFT in the queue for the next block rather than taken out and lost: it arrives a block late, at frame 0, which is better than not at all when it is a note-off.

Types 1

MIDI queued from any thread, handed to the audio thread a block at a time at the frames its arrival times imply. Mirrors the reference's MidiMessageCollector, without its lock in the audio callback and without dropping the events of a long gap; the file comment has the mapping and why.

Any number of producers call addMessageToQueue() and addSysexToQueue(), on any thread, without waiting. ONE consumer -- the audio thread -- calls removeNextBlockOfMessages(), and reset() belongs to that same side: call it from prepare() with the sample rate before the first block, or from the audio thread, never while a drain is running elsewhere. Every stamp is in now()'s seconds.

Fixed capacity, allocated inline: about 80 KB of ring, so it belongs in a member, not on a stack. Not copyable.

↑↓ to move↵ to openEvery public name in the headers, and every guide