Funky MIDI with libremidi

libremidi is an all-in-one cross-platform C++20 MIDI library for both file and real-time output. Real-time I/O supports MIDI 2 on macOS (11+) and Linux (Kernel 6.5+), and soon on Windows (the OS does not support it yet).

It is a fork / rewrite originally based on two libraries, but has since then almost been entirely rewritten:

Compared to its origins, it features a lot of changes and improvements:

  • libremidi::observer provides hotplug support.
  • Ports are identified not with a number but with a handle which enables more stability when unplugging / replugging.
  • Memory allocations and virtual function calls are greatly reduced when compared to the RtMidi base-line.
    • Ability to enforce fixed message sizes with boost::static_vector for hard real-time operation
    • Ability to use boost::small_vector to cover most cases.
  • Integer timestamps everywhere, by default in nanoseconds.
  • Ability to choose different timestamping methods (e.g. relative, absolute monotonic clock, sample-based or custom timestamping...).
  • Integration of modern C++20 types (for instance std::span instead of std::vector, std::function for callbacks, etc.)
  • Standard C++ threading primitives (std::thread, std::jthread) are now used, as well as modern Linux facilities for polling control (eventfd, timerfd).
  • Most of the code has been refactored in multiple files for clarity.

It also features some new & improved backends:

  • ALSA RawMidi API.
  • PipeWire.
  • Windows UWP.
  • WebMIDI in Emscripten.
  • JACK support on all platforms where it is available.
  • Computer keyboard input.

Compiling the library

libremidi uses CMake as build system. The easiest way to compile the library is to take inspiration from the CI scripts as they will install the required dependencies.

Some backends require an up-to-date C++20 compiler: JACK and PipeWire, as we leverage the C++20 semaphore support.

Compiling the library and examples is as simple as:

$ cmake -Wno-dev \
  -S path/to/libremidi \
  -B build_folder \
  -DLIBREMIDI_EXAMPLES=1 

$ cmake --build build_folder

libremidi is also available on vcpkg and Nixpkgs.

On Linux & BSD

Note that the ALSA and PipeWire back-end rely on timerfd and eventfd which may not be available on very, very, very old Linux kernels (< 3.x) or some BSD kernels.

Note that the ALSA Raw back-end also needs udev access to scan the USB peripherals.

  • Debian & Ubuntu packages for all the back-ends:
libasound-dev
libjack-jackd2-dev
libudev-dev
libpipewire-0.3-dev
  • ArchLinux packages for all the back-ends:
alsa-lib
jack2
libpipewire
systemd-libs

On Windows

Note that when targetting Windows Store, WinMM APIs are not available, only UWP ones.

Both MSVC and MSYS2 are supported (on all MSYS2 toolchains).

On macOS & iOS

Note that for MIDI 2 support, the application needs to target at least macOS 11.0. e.g. -DCMAKE_OSX_DEPLOYMENT_TARGET=11 or more recent needs to be passed to CMake.

Advanced features

  • For MIDI 1: ability to set a fixed message size upper-bound for zero-allocation scenarios, with -DLIBREMIDI_SLIM_MESSAGE=<NBytes> (in CMake or directly to the compiler)

Using libremidi in header-only mode

The library can be used header-only, with minimal modifications to your build system if you aren't using CMake:

  • Define LIBREMIDI_HEADER_ONLY=1

  • Define macros for the APIs you wish to build. The possible macros are as follows:

    • macOS: LIBREMIDI_COREMIDI=1 and link against -framework CoreMIDI -framework CoreAudio -framework CoreFoundation.
    • Linux: LIBREMIDI_ALSA=1 and link against -lasound -phtread.
    • Windows (WinMM): LIBREMIDI_WINMM=1 and link against winmm.
    • Windows (UWP): LIBREMIDI_WINUWP=1 ; note that there is complex linking logic detailed in the CMakeLists.txt when using UWP.
    • emscripten: LIBREMIDI_EMSCRIPTEN=1.
    • Any platform with JACK: LIBREMIDI_JACK=1.
  • Add the include folder to your include path.

  • #include <libremidi/libremidi.hpp> in your source code.

For instance, to build the midiprobe example on Linux with only ALSA support, one would run:

$ g++ ~/libremidi/tests/midiprobe.cpp \
      -std=c++20 \
      -DLIBREMIDI_ALSA=1 \
      -DLIBREMIDI_HEADER_ONLY=1 \
      -I ~/libremidi/include \
      -lasound -pthread

To build it on macOS, one would run:

$ clang++ ~/libremidi/tests/midiprobe.cpp \
      -std=c++20 \
      -DLIBREMIDI_COREMIDI=1 \
      -DLIBREMIDI_HEADER_ONLY=1 \
      -I ~/libremidi/include \
      -framework CoreMIDI -framework CoreAudio -framework CoreFoundation

Adding libremidi to your project

Through CMake

Consider the following existing CMake project for you application:

project(my_app)

add_executable(my_app src/main.cpp)

Then you can add libremidi either directly for instance through a git submodule:

project(my_app)

# example of folder structure
add_subdirectory(3rdparty/libremidi)

add_executable(my_app src/main.cpp)

target_link_libraries(my_app PRIVATE libremidi)

Or through FetchContent:

project(my_app)

FetchContent_Declare(
    libremidi
    GIT_REPOSITORY https://github.com/celtera/libremidi
    GIT_TAG        main
)

FetchContent_MakeAvailable(libremidi)

add_executable(my_app src/main.cpp)

target_link_libraries(my_app PRIVATE libremidi)

Through a custom build-system

If using a custom build-system, the main thing to be aware of that CMake does automatically for you is passing the relevant flags which will enable each backend and linking with the correct libraries.

For instance on Linux with ALSA:

$ g++ \
  main.cpp \
  -std=c++20 \
  -I3rdparty/libremidi/include \
  -DLIBREMIDI_ALSA=1 \
  -DLIBREMIDI_HAS_UDEV=1
  -ldl

or on macOS with CoreMIDI:

$ clang++ \
  main.cpp \
  -std=c++20 \
  -I3rdparty/libremidi/include \
  -DLIBREMIDI_COREMIDI=1 \
  -framework CoreFoundation \
  -framework CoreAudio \
  -framework CoreMIDI

or on Win32 with WinMM:

> cl.exe ^
  main.cpp ^
  /std:c++latest ^
  /I c:\libs\3rdparty\libremidi\include ^
  /DLIBREMIDI_WINMM=1 ^
  /DUNICODE=1 ^
  /D_UNICODE=1 ^
  path/to/winmm.lib

Enumerating ports

The required header is #include <libremidi/libremidi.hpp>.

Inputs:

libremidi::observer obs;
for(const libremidi::input_port& port : obs.get_input_ports()) {
  std::cout << port.port_name << "\n";
}

Outputs:

libremidi::observer obs;
for(const libremidi::output_port& port : obs.get_output_ports()) {
  std::cout << port.port_name << "\n";
}

See midiprobe.cpp for a simple example.

Reading MIDI 1 messages from a device through callbacks

// Set the configuration of our MIDI port
// Note that the callback will be invoked from a separate thread,
// it is up to you to protect your data structures afterwards.
// For instance if you are using a GUI toolkit, don't do GUI actions
// in that callback !
auto my_callback = [](const libremidi::message& message) {
  // how many bytes
  message.size();
  // access to the individual bytes
  message[i];
  // access to the timestamp
  message.timestamp;
};

// Create the midi object
libremidi::midi_in midi{ 
  libremidi::input_configuration{ .on_message = my_callback } 
};

// Open a given midi port. 
// The argument is a libremidi::input_port gotten from a libremidi::observer. 
midi.open_port(/* a port */);
// Alternatively, to get the default port for the system: 
midi.open_port(libremidi::midi1::in_default_port());

// Note that only one port can be open at a given time on a midi_in or midi_out object.

Sending MIDI 1 messages to a device

// Create the midi object
libremidi::midi_out midi;

// Open a given midi port. Same as for input:
midi.open_port(libremidi::midi2::out_default_port());

// Option A: send fixed amount of bytes for most basic cases
midi.send_message(144, 110, 40); // Overloads exist for 1, 2, 3 bytes

// Option B: send a raw byte array
unsigned char bytes[3] = { 144, 110, 40 };
midi.send_message(bytes, sizeof(bytes));

// Option C: std::span<unsigned char>
// This allows to pass std::vector, std::array and the likes
midi.send_message(std::span<unsigned char>{ ... your span-compatible data-structure ... });

// Option D: helpers with the libremidi::channel_events and libremidi::meta_events structs
// See libremidi/message.hpp for the full list
midi.send_message(libremidi::channel_events::note_on(channel, note, velocity));
midi.send_message(libremidi::channel_events::control_change(channel, control, value));
midi.send_message(libremidi::channel_events::pitch_bend(channel, value));
midi.send_message(libremidi::message{ /* a message */ });

Reading MIDI 2 messages from a device through callbacks

Note that MIDI 2 support is still experimental and subject to change. Note also that the MIDI 1 and MIDI 2 send functions (not yet receive) are useable no matter the kind of backend used (e.g. one can send UMPs to MIDI 1 backends and MIDI 1 messages to MIDI 2 backends). This conversion is done in a best-effort way.

// Set the configuration of our MIDI port, same warnings apply than for MIDI 1.
// Note that an UMP message is always at most 4 * 32 bits = 16 bytes.
// Added to the 64-bit timestamp this is 24 bytes for a libremidi::ump 
// which is definitely small enough to be passed by value.
// Note that libremidi::ump is entirely constexpr.
auto my_callback = [](libremidi::ump message) {
  // how many bytes
  message.size();
  // access to the individual bytes
  message[i];
  // access to the timestamp
  message.timestamp;
};

// Create the midi object
libremidi::midi_in midi{ 
  libremidi::ump_input_configuration{ .on_message = my_callback }
};


// Open a given midi port. 
// The argument is a libremidi::input_port gotten from a libremidi::observer. 
midi.open_port(/* a port */);
// Alternatively, to get the default port for the system: 
midi.open_port(libremidi::midi2::in_default_port());

Sending MIDI 2 messages to a device

// Create the midi object
libremidi::midi_out midi;

// Open a given midi port. Same as for input:
midi.open_port(libremidi::midi2::out_default_port());

// Option A: send fixed amount of bytes for most basic cases
midi.send_ump(A, B, C, D); // Overloads exist for 1, 2, 3, 4 uint32_t

// Option B: send a raw byte array.
// This can contain a sequence of UMP messages.
uint32_t bytes[2] = { ... };
midi.send_ump(bytes, sizeof(bytes));

// Option C: std::span<uint32_t>
// This allows to pass std::vector, std::array and the likes
// This can contain a sequence of UMP messages.
midi.send_ump(std::span<uint32_t>{ ... your span-compatible data-structure ... });

// Option D: helpers with the libremidi::ump class
// The helpers haven't been implemented yet :(
midi.send_ump(libremidi::ump{ /* a message */ });

MIDI file I/O usage

Reading a .mid file

See midifile_dump.cpp for a more complete example.

// Read raw from a MIDI file
std::ifstream file{"path/to/a.mid", std::ios::binary};

std::vector<uint8_t> bytes;
bytes.assign(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());

// Initialize our reader object
libremidi::reader r;

// Parse
libremidi::reader::parse_result result = r.parse(bytes);

// If parsing succeeded, use the parsed data
if(result != libremidi::reader::invalid) {
  for(auto& track : r.tracks) {
    for(auto& event : t.events) {
      std::cout << (int) event.m.bytes[0] << '\n';
    }
  }
}

Writing a .mid file

// Initialize a writer object
libremidi::writer writer;

// Create tracks and events declaratively by changing the track vector directly:
writer.tracks.push_back(
  libremidi::midi_track{
    libremidi::track_event{0, 0, libremidi::channel_events::note_on(1, 45, 35)},
    libremidi::track_event{140, 0, libremidi::channel_events::note_off(1, 45, 0)},
  }
);

// Or through a builder API:
{
  int tick = 500;
  int track = 3;
  libremidi::message msg = libremidi::channel_events::note_on(1, 45, 35);

  // Tracks will be added as needed within safe limits
  writer.add_event(tick, track, msg);
}

// Read raw from a MIDI file
std::ofstream output{"output.mid", std::ios::binary};
writer.write(output);

MIDI 2 integrations

libremidi integrates with the existing MIDI 2 ecosystem.

cmidi2

We ship Atsushi Eno's cmidi2 header-only MIDI 2 implementation as part of the library. This allows to quickly match incoming messages with MIDI 2 types. A lot of useful utility functions are provided.

See the midi2_echo.cpp example: it uses the following printing function:

std::ostream& operator<<(std::ostream& s, const libremidi::ump& message)
{
  // Automatic conversion from libremidi::ump& to cmidi2_ump*
  const cmidi2_ump* b = message;
  
  // Read MIDI 2 information
  int bytes = cmidi2_ump_get_num_bytes(message.data[0]);
  int group = cmidi2_ump_get_group(b);
  int status = cmidi2_ump_get_status_code(b);
  int channel = cmidi2_ump_get_channel(b);
  s << "[ " << bytes << " | " << group;

  switch ((libremidi::message_type)status)
  {
    case libremidi::message_type::NOTE_ON:
      s << " | note on: " << channel << (int)cmidi2_ump_get_midi2_note_note(b) << " | "
        << cmidi2_ump_get_midi2_note_velocity(b);
      break;
    case libremidi::message_type::NOTE_OFF:
      s << " | note off: " << channel << (int)cmidi2_ump_get_midi2_note_note(b) << " | "
        << cmidi2_ump_get_midi2_note_velocity(b);
      break;
    case libremidi::message_type::CONTROL_CHANGE:
      s << " | cc: " << channel << (int)cmidi2_ump_get_midi2_cc_index(b) << " | "
        << cmidi2_ump_get_midi2_cc_data(b);
      break;

    default:
      break;
  }
  s << " ]";
  return s;
}

ni-midi2

It is possible to enable compatibility with [ni-midi2](https://github.com/midi2-dev/ni-midi2, the MIDI Association-backed MIDI 2 implementation. This is done with the LIBREMIDI_NI_MIDI2 CMake switch: -DLIBREMIDI_NI_MIDI2=ON.

An example of initialisation of a MIDI 2-compliant communication can be found in the midi2_interop.cpp example.

Note that due to the complexity of MIDI 2, we believe end-users will want to build their own, custom-tailored stack and API on top of these base layers.

Current interoperability features are:

  • libremidi::ump can be converted to midi::universal_packet and conversely.
  • libremidi::midi_out::send_ump can be called with midi::universal_packet, midi::sysex7_packet and midi::sysex8_packet.

This ensures that most MIDI-CI messages can be sent directly. Example:

libremidi::midi_out midiout;

...

midiout.send_ump(midi::ci::make_discovery_inquiry(my_muid, id, 0x02, 512));

Device connection / disconnection notification

// The callbacks will be called when the relevant event happens.
// Note that they may be called from other threads than the main thread.

libremidi::observer_configuration conf{
    .input_added = [&] (const libremidi::input_port& id) {
      std::cout << "Input connected: " << id.port_name << std::endl;
    },
    .input_removed = [&] (const libremidi::input_port& id) {
      std::cout << "Input removed: " << id.port_name << std::endl;
    },
    .output_added = [&] (const libremidi::output_port& id) {
      std::cout << "Output connected: " << id.port_name << std::endl;
    },
    .output_removed = [&] (const libremidi::output_port& id) {
      std::cout << "Output removed: " << id.port_name << std::endl;
}};

libremidi::observer obs{std::move(conf)};

See midiobserve.cpp or emscripten_midiin.cpp for an example.

Error handling

The default error handling is done with exceptions. If exceptions are undesirable, it is also possible to set a callback function which will be invoked upon error, for the midi_in and midi_out classes.

(Some classes may still throw, such as when creating invalid MIDI messages with the libremidi::message helpers, or the observer classes).

// Create the configuration
libremidi::input_configuration conf{
    .on_message = /* usual message callback */
  , .on_error = [] (libremidi::midi_error code, std::string_view info) {
      // ... log error however you want
    }
  , .on_warning = [] (libremidi::midi_error code, std::string_view info) {
      // ... log warning however you want
    }
};

// Create the midi object
libremidi::midi_in midi{conf};

Ditto for midi_out and midi_observer.

Advanced configuration

The midi_in, midi_out and midi_observer objects are configured through a input_configuration (resp. output_, etc.) object passed in argument to the constructor.

Example:

#include <libremidi/configurations.hpp>

...

libremidi::midi_in in{
    libremidi::input_configuration{
      .on_message = ...
    , .ignore_sysex = false
    , .ignore_sensing = true
    }
};

Custom back-end configuration

Additionnally, each back-end supports back-end specific configuration options, to enable users to tap into advanced features of a given API while retaining the general C++ abstraction.

For instance, this enables to set output buffer sizes, chunking parameters, etc. for back-ends which support the feature.

#include <libremidi/backends.hpp>

...

libremidi::midi_in in{
    libremidi::input_configuration{
      .on_message = ...
    },
    libremidi::pipewire_input_configuration{
      .client_name = "My app"
    }
};

Context sharing

This allows to share a single context across multiple MIDI objects (such as a jack_client_t with JACK, a PipeWire main loop & filter, or a MIDIClientRef on macOS with CoreMIDI). If no context is passed, each object will create one as they used to.

Example:

#include <libremidi/configurations.hpp>

...

libremidi::midi_in in{
    libremidi::input_configuration{.on_message = ...}
  , libremidi::alsa_seq::input_configuration{
      .client_name = "my client"
  } 
};

If one simply wants to share a context across libremidi objects (for instance, a single context shared across an observer, midi_ins and midi_outs), the following methods will create appropriate configurations from an observer's configuration:

// Create an observer with a fixed back-end
libremidi::observer obs{
    libremidi::observer_configuration{}
  , libremidi::observer_configuration_for(libremidi::API::JACK_MIDI)};

// The in and out will share the JACK client of the observer.
// Note that the observer has to outlive them.
libremidi::midi_in in{
    libremidi::input_configuration{.on_message = ...}
  , libremidi::midi_in_configuration_for(obs) 
};

libremidi::midi_out out{
    libremidi::output_configuration{...}
  , libremidi::midi_out_configuration_for(obs) 
};

In that case, note that the obs has ownership of for instance the JACK context object: it must outlive in and out.

The relevant examples are:

  • coremidi_share.cpp for a complete example for CoreMIDI.
  • jack_share.cpp for a complete example for JACK.
  • pipewire_share.cpp for a complete example for PipeWire.

Custom polling

Traditionnally, RtMidi (and most other MIDI libraries) opened a dedicated MIDI thread from which messages are read, and then transferred to the rest of the app.

If your app is based on an event loop that can poll file descriptors, such as poll(), the relevant back-ends will allow to instead control polling manually by providing you with the file descriptors, which can then be injected into your app's main loop.

Thus, this enables complete control over threading (and can also remove the need for synchronisation as this allows to make a callback-based yet single-threaded app, for simple applications which do not wish to reimplement MIDI filtering & parsing for back-ends such as ALSA Sequencer or RawMidi).

Since this feature is pretty complicated to implement, we recommend checking the examples:

See:

  • poll_share.cpp for a complete example for ALSA RawMidi (recommended).
  • alsa_share.cpp for a complete example for ALSA Seq.

Timestamping

libremidi provides many useful timestamping options for its input callbacks:

libremidi::input_configuration conf;

// No timestamping at all, all timestamps are zero
conf.timestamps = libremidi::NoTimestamp;

// In nanoseconds, timestamp is the time since the previous event (or zero)
conf.timestamps = libremidi::Relative;

// In nanoseconds, as per an arbitrary reference which may be provided by the host API,
// e.g. since the JACK cycle start, ALSA sequencer queue creation, through AudioHostTime on macOS.
// It offers the most precise ordering between events as it's the closest to the real timestamp of
// the event as provided by the host API.
// If the API does not provide any timing, it will be mapped to SystemMonotonic instead.
conf.timestamps = libremidi::Absolute;

// In nanoseconds, as per std::steady_clock::now() or equivalent (raw if possible).
// May be less precise than Absolute as timestamping is done within the library,
// but is more useful for system-wide synchronization.
// Note: depending on the backend, Absolute and SystemMonotonic may be the same.
conf.timestamps = libremidi::SystemMonotonic;

// For APIs which are based on audio process cycles such as JACK, timestamps will be in frames since
// the beginning of the current cycle's audio buffer
conf.timestamps = libremidi::AudioFrame;

// Will call the custom timestamping function provided by the user in the input configuration.
// We try to make sure that the input timestamp is as precise as possible ; it is given in the Absolute mode.
conf.get_timestamp = [] (libremidi::timestamp absolute) -> libremidi::timestamp { 
  // Map absolute timestamp to the desired time domain
  return absolute / 1e6;
};
conf.timestamps = libremidi::Custom;

For the absolute timestamps, the origin of the timestamp can be obtained with midi_in::absolute_timestamp(). For instance, it will return the time at which the timestamping queue was created in the ALSA back-end.

Queue input

The old queued input mechanism present in RtMidi and previous versions of the library has been moved out of the code as it can be built entirely on the callback mechanism and integrated with the user application's event processing queue instead.

A basic example is provided in qmidiin.cpp.

We recommend if possible to instead use an asynchronous runtime in order to keep an imperative behaviour while benefiting from the non-blocking properties of async programming.

An example based on C++20 coroutines with Boost.Cobalt is provided in coroutines.cpp.

Computer keyboard input

This backend allwos to use the computer keys to play MIDI. The backend does not directly read the key events as this would require making the library much more complex. Instead, it provides a callback that you can plug into your favourite GUI toolkit to process scan codes.

The mapping is customizable. By default:

 ,---,---,---,---,---,---,---,---,---,---,---,---,---,-------,
 | V0| V1| V2| V3| V4| V5| V6| V7| V8| V9|V10|V11|V12| <-    |
 |---'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-----|
 | ->| |   | C#| D#|   | F#| G#| A#|   | C#| D#|   | F#|     |
 |-----',--',--',--',--',--',--',--',--',--',--',--',--'|    |
 | Caps | C | D | E | F | G | A | B | C | D | E | F | G |    |
 |----,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'-,-'---'----|
 | -^ |   | O-| O+| V-| V+|   |   |   |   |   |   |   ----^  |
 |----'-,-',--'--,'---'---'---'---'---'---'-,-'---',--,------|
 | ctrl |  | alt |                          |altgr |  | ctrl |
 '------'  '-----'--------------------------'------'  '------'

Where V0 to V12 set the velocity between 0 and 127 in steps of ~10, O- / O+ increase or decrease the octave and V- / V+ increase or decrease the velocity by 10.

Example:

#include <libremidi/backends/keyboard/config.hpp>

libremidi::kbd_input_configuration api_conf;
api_conf.set_input_scancode_callbacks(
[] )

Feature matrix

This table shows which feature is supported by which backend so far, for advanced features.

It may be because the backend does not provide the ability at all (N/A), or because it has not been implemented yet.

Linux & BSD

ALSA RawALSA SeqPipeWire
MIDI 1YesYesYes
MIDI 2YesYesN/A
Virtual portsN/AYesYes
ObserverYesYesYes
SchedulingNoNoNo

Special features

  • The ALSA Raw back-end allows to perform chunked sending of MIDI messages, which can be useful to upload firmwares.

  • libasound and libpipewire are always loaded through dlopen. JACK can also be, optionally.

This allows libremidi to be built on a system with e.g. PipeWire support without preventing application loading if the end user does not use it.

Windows

WinMMUWPWinMIDI
MIDI 1YesYesNo
MIDI 2N/AN/AYes
Virtual portsN/ANoNo
ObserverYesYesYes
SchedulingNoNoNo

Mac & iOS

CoreMIDI
MIDI 1Yes
MIDI 2Yes
Virtual portsYes
ObserverYes
SchedulingNo

Web

Emscripten WebMIDI
MIDI 1Yes
MIDI 2N/A
Virtual portsN/A
ObserverYes
SchedulingNo

Shared backends

JACK
MIDI 1Yes
MIDI 2N/A
Virtual portsYes
ObserverYes
SchedulingNo