diff --git a/libremidi.cabal b/libremidi.cabal
--- a/libremidi.cabal
+++ b/libremidi.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           libremidi
-version:        0.2.0
+version:        0.3.0
 synopsis:       libremidi bindings for haskell
 description:    Please see the README on GitHub at <https://github.com/ejconlon/libremidi-haskell#readme>
 homepage:       https://github.com/ejconlon/libremidi-haskell#readme
@@ -65,18 +65,19 @@
       TypeFamilies
   ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints -fno-warn-unused-top-binds
   include-dirs:
-      ./
+      vendor
   install-includes:
-      libremidi/libremidi-c.h
+      vendor/libremidi/api-c.h
+      vendor/libremidi/libremidi-c.h
   cxx-sources:
-      libremidi/client.cpp
-      libremidi/libremidi-c.cpp
-      libremidi/libremidi.cpp
-      libremidi/midi_in.cpp
-      libremidi/midi_out.cpp
-      libremidi/observer.cpp
-      libremidi/reader.cpp
-      libremidi/writer.cpp
+      vendor/libremidi/client.cpp
+      vendor/libremidi/libremidi-c.cpp
+      vendor/libremidi/libremidi.cpp
+      vendor/libremidi/midi_in.cpp
+      vendor/libremidi/midi_out.cpp
+      vendor/libremidi/observer.cpp
+      vendor/libremidi/reader.cpp
+      vendor/libremidi/writer.cpp
   extra-libraries:
       stdc++
   build-depends:
@@ -140,7 +141,8 @@
       TypeFamilies
   ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints -fno-warn-unused-top-binds -threaded -rtsopts -with-rtsopts=-N
   install-includes:
-      libremidi/libremidi-c.h
+      vendor/libremidi/api-c.h
+      vendor/libremidi/libremidi-c.h
   build-depends:
       base >=4.12 && <5
     , containers >=0.6.7 && <0.7
@@ -192,7 +194,8 @@
       TypeFamilies
   ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints -fno-warn-unused-top-binds -threaded -rtsopts -with-rtsopts=-N
   install-includes:
-      libremidi/libremidi-c.h
+      vendor/libremidi/api-c.h
+      vendor/libremidi/libremidi-c.h
   build-depends:
       base >=4.12 && <5
     , containers >=0.6.7 && <0.7
diff --git a/libremidi/client.cpp b/libremidi/client.cpp
deleted file mode 100644
--- a/libremidi/client.cpp
+++ /dev/null
@@ -1,47 +0,0 @@
-#if !defined(LIBREMIDI_HEADER_ONLY)
-  #include "client.hpp"
-#endif
-#include <libremidi/backends.hpp>
-#include <libremidi/shared_context.hpp>
-
-#ifdef LIBREMIDI_ALSA
-  #include <libremidi/backends/alsa_seq/shared_handler.hpp>
-#endif
-#ifdef LIBREMIDI_JACK
-  #include <libremidi/backends/jack/shared_handler.hpp>
-#endif
-namespace libremidi
-{
-LIBREMIDI_INLINE
-shared_configurations
-create_shared_context(const libremidi::API api, [[maybe_unused]] std::string_view client_name)
-{
-  switch (api)
-  {
-#if __has_include(<boost/lockfree/spsc_queue.hpp>)
-  #if defined(LIBREMIDI_ALSA)
-    case libremidi::API::ALSA_SEQ:
-      return alsa_seq::shared_handler::make(client_name);
-  #endif
-
-  #if defined(LIBREMIDI_JACK)
-    case libremidi::API::JACK_MIDI:
-      return jack::shared_handler::make(client_name);
-  #endif
-#endif
-    case libremidi::API::COREMIDI:
-    case libremidi::API::COREMIDI_UMP:
-      // TODO
-
-    case libremidi::API::WINDOWS_MIDI_SERVICES:
-      // TODO
-
-    default:
-      return {
-          .context = {},
-          .observer = observer_configuration_for(api),
-          .in = midi_in_configuration_for(api),
-          .out = midi_out_configuration_for(api)};
-  }
-}
-}
diff --git a/libremidi/libremidi-c.cpp b/libremidi/libremidi-c.cpp
deleted file mode 100644
--- a/libremidi/libremidi-c.cpp
+++ /dev/null
@@ -1,507 +0,0 @@
-// clang-format off
-#include <libremidi/libremidi-c.h>
-#include <libremidi/libremidi.hpp>
-#include <libremidi/backends.hpp>
-// clang-format on
-
-#include <cerrno>
-#include <cstdlib>
-#include <cstring>
-#include <limits>
-
-struct libremidi_midi_observer_handle
-{
-  libremidi::observer self;
-};
-
-struct libremidi_midi_in_handle
-{
-  libremidi::midi_in self;
-};
-
-struct libremidi_midi_out_handle
-{
-  libremidi::midi_out self;
-};
-
-namespace libremidi
-{
-
-static void assign_error_callback(const auto& src, auto& dst)
-{
-  if (src.callback)
-  {
-    dst = [src](std::string_view errorText, const source_location& loc) {
-      src.callback(src.context, errorText.data(), errorText.size(), &loc);
-    };
-  }
-}
-}
-
-extern "C" {
-const char* libremidi_get_version(void)
-{
-  return LIBREMIDI_VERSION;
-}
-
-void libremidi_available_midi1_apis(void* ctx, void (*cb)(void* ctx, libremidi_api))
-{
-  if (!cb)
-    return;
-  libremidi::midi1::for_all_backends([=](auto b) { cb(ctx, b.API); });
-}
-
-void libremidi_available_midi2_apis(void* ctx, void (*cb)(void* ctx, libremidi_api))
-{
-  if (!cb)
-    return;
-  libremidi::midi2::for_all_backends([=](auto b) { cb(ctx, b.API); });
-}
-
-const char* libremidi_api_identifier(libremidi_api api)
-{
-  return libremidi::get_api_name(api).data();
-}
-
-const char* libremidi_api_display_name(libremidi_api api)
-{
-  return libremidi::get_api_display_name(api).data();
-}
-
-libremidi_api libremidi_get_compiled_api_by_identifier(const char* name)
-{
-  libremidi_api ret = libremidi_api::UNSPECIFIED;
-  libremidi::midi_any::for_all_backends([&](auto& b) {
-    if (name == b.name)
-      ret = b.API;
-  });
-  return ret;
-}
-
-int libremidi_midi_api_configuration_init(libremidi_api_configuration* conf)
-{
-  memset(conf, 0, sizeof(*conf));
-  return 0;
-}
-
-int libremidi_midi_observer_configuration_init(libremidi_observer_configuration* conf)
-{
-  memset(conf, 0, sizeof(*conf));
-  return 0;
-}
-
-int libremidi_midi_configuration_init(libremidi_midi_configuration* conf)
-{
-  memset(conf, 0, sizeof(*conf));
-  return 0;
-}
-
-int libremidi_midi_in_port_clone(const libremidi_midi_in_port* port, libremidi_midi_in_port** dst)
-{
-  if (!port || !dst)
-    return -EINVAL;
-
-  auto copied = new libremidi::input_port{*reinterpret_cast<const libremidi::input_port*>(port)};
-  *dst = reinterpret_cast<libremidi_midi_in_port*>(copied);
-  return 0;
-}
-
-int libremidi_midi_in_port_free(libremidi_midi_in_port* port)
-{
-  delete reinterpret_cast<libremidi::input_port*>(port);
-  return 0;
-}
-
-int libremidi_midi_in_port_name(const libremidi_midi_in_port* port, const char** name, size_t* len)
-{
-  if (!port || !name || !len)
-    return -EINVAL;
-
-  auto& p = *reinterpret_cast<const libremidi::input_port*>(port);
-  *name = p.port_name.data();
-  *len = p.port_name.size();
-  return 0;
-}
-
-int libremidi_midi_out_port_clone(
-    const libremidi_midi_out_port* port, libremidi_midi_out_port** dst)
-{
-  if (!port || !dst)
-    return -EINVAL;
-
-  auto copied = new libremidi::output_port{*reinterpret_cast<const libremidi::output_port*>(port)};
-  *dst = reinterpret_cast<libremidi_midi_out_port*>(copied);
-  return 0;
-}
-
-int libremidi_midi_out_port_free(libremidi_midi_out_port* port)
-{
-  delete reinterpret_cast<libremidi::output_port*>(port);
-  return 0;
-}
-
-int libremidi_midi_out_port_name(
-    const libremidi_midi_out_port* port, const char** name, size_t* len)
-{
-  if (!port || !name || !len)
-    return -EINVAL;
-
-  auto& p = *reinterpret_cast<const libremidi::output_port*>(port);
-  *name = p.port_name.data();
-  *len = p.port_name.size();
-  return 0;
-}
-
-int libremidi_midi_observer_new(
-    const libremidi_observer_configuration* c, libremidi_api_configuration* api,
-    libremidi_midi_observer_handle** out)
-{
-  if (!out || !c)
-    return -EINVAL;
-
-  libremidi::observer_configuration conf;
-  libremidi::assign_error_callback(c->on_error, conf.on_error);
-  libremidi::assign_error_callback(c->on_warning, conf.on_warning);
-
-  conf.track_hardware = c->track_hardware;
-  conf.track_virtual = c->track_virtual;
-  conf.track_any = c->track_any;
-  conf.notify_in_constructor = c->notify_in_constructor;
-
-  auto api_conf = libremidi::observer_configuration_for(static_cast<libremidi::API>(api->api));
-
-  if (c->input_added.callback)
-  {
-    conf.input_added = [cb = c->input_added](const auto& port) {
-      cb.callback(cb.context, reinterpret_cast<const libremidi_midi_in_port*>(&port));
-    };
-  }
-  if (c->input_removed.callback)
-  {
-    conf.input_removed = [cb = c->input_removed](const auto& port) {
-      cb.callback(cb.context, reinterpret_cast<const libremidi_midi_in_port*>(&port));
-    };
-  }
-  if (c->output_added.callback)
-  {
-    conf.output_added = [cb = c->output_added](const auto& port) {
-      cb.callback(cb.context, reinterpret_cast<const libremidi_midi_out_port*>(&port));
-    };
-  }
-  if (c->output_removed.callback)
-  {
-    conf.output_removed = [cb = c->output_removed](const auto& port) {
-      cb.callback(cb.context, reinterpret_cast<const libremidi_midi_out_port*>(&port));
-    };
-  }
-
-  try
-  {
-    auto ptr = new libremidi_midi_observer_handle{
-        libremidi::observer{std::move(conf), std::move(api_conf)}};
-    *out = ptr;
-    return 0;
-  }
-  catch (...)
-  {
-    return -EINVAL;
-  }
-}
-
-int libremidi_midi_observer_enumerate_input_ports(
-    libremidi_midi_observer_handle* ptr, void* context,
-    void (*cb)(void* ctx, const libremidi_midi_in_port*))
-{
-  if (!ptr || !cb)
-    return -EINVAL;
-
-  for (const auto& port : ptr->self.get_input_ports())
-  {
-    cb(context, reinterpret_cast<const libremidi_midi_in_port*>(&port));
-  }
-
-  return 0;
-}
-
-int libremidi_midi_observer_enumerate_output_ports(
-    libremidi_midi_observer_handle* ptr, void* context,
-    void (*cb)(void* ctx, const libremidi_midi_out_port*))
-{
-  if (!ptr || !cb)
-    return -EINVAL;
-
-  for (const auto& port : ptr->self.get_output_ports())
-  {
-    cb(context, reinterpret_cast<const libremidi_midi_out_port*>(&port));
-  }
-
-  return 0;
-}
-
-int libremidi_midi_observer_free(libremidi_midi_observer_handle* ptr)
-{
-  delete ptr;
-  return 0;
-}
-
-int libremidi_midi_in_new(
-    const libremidi_midi_configuration* c, const libremidi_api_configuration* api,
-    libremidi_midi_in_handle** out)
-{
-  if (!out || !c)
-    return -EINVAL;
-  if (!c->virtual_port && !c->in_port)
-    return -EINVAL;
-
-  *out = nullptr;
-
-  auto api_conf = libremidi::midi_in_configuration_for(static_cast<libremidi::API>(api->api));
-
-  // Create the MIDI object
-  switch (c->version)
-  {
-    case libremidi_midi_configuration::MIDI1:
-    case libremidi_midi_configuration::MIDI1_RAW: {
-      libremidi::input_configuration conf;
-      libremidi::assign_error_callback(c->on_error, conf.on_error);
-      libremidi::assign_error_callback(c->on_warning, conf.on_warning);
-      conf.ignore_sensing = c->ignore_sensing;
-      conf.ignore_sysex = c->ignore_sysex;
-      conf.ignore_timing = c->ignore_timing;
-      conf.timestamps = c->timestamps;
-
-      if (c->get_timestamp.callback)
-      {
-        conf.get_timestamp
-            = [cb = c->get_timestamp](int64_t msg) { return cb.callback(cb.context, msg); };
-      }
-
-      if (c->on_midi1_message.callback)
-        conf.on_message = [cb = c->on_midi1_message](const libremidi::message& msg) {
-          cb.callback(cb.context, msg.timestamp, msg.bytes.data(), msg.size());
-        };
-      else if (c->on_midi1_raw_data.callback)
-      {
-        conf.on_raw_data = [cb = c->on_midi1_raw_data](std::span<const uint8_t> msg, int64_t ts) {
-          cb.callback(cb.context, ts, msg.data(), msg.size());
-        };
-      }
-      else
-      {
-        return -EINVAL;
-      }
-
-      try
-      {
-        auto ptr = new libremidi_midi_in_handle{
-            libremidi::midi_in{std::move(conf), std::move(api_conf)}};
-        *out = ptr;
-      }
-      catch (...)
-      {
-        return -EINVAL;
-      }
-      break;
-    }
-    case libremidi_midi_configuration::MIDI2:
-    case libremidi_midi_configuration::MIDI2_RAW: {
-      libremidi::ump_input_configuration conf;
-      libremidi::assign_error_callback(c->on_error, conf.on_error);
-      libremidi::assign_error_callback(c->on_warning, conf.on_warning);
-      conf.ignore_sensing = c->ignore_sensing;
-      conf.ignore_sysex = c->ignore_sysex;
-      conf.ignore_timing = c->ignore_timing;
-      conf.timestamps = c->timestamps;
-
-      if (c->get_timestamp.callback)
-      {
-        conf.get_timestamp
-            = [cb = c->get_timestamp](int64_t msg) { return cb.callback(cb.context, msg); };
-      }
-
-      if (c->on_midi2_message.callback)
-        conf.on_message = [cb = c->on_midi2_message](const libremidi::ump& msg) {
-          cb.callback(cb.context, msg.timestamp, msg.data, msg.size());
-        };
-      else if (c->on_midi2_raw_data.callback)
-      {
-        conf.on_raw_data = [cb = c->on_midi2_raw_data](std::span<const uint32_t> msg, int64_t ts) {
-          cb.callback(cb.context, ts, msg.data(), msg.size());
-        };
-      }
-      else
-      {
-        return -EINVAL;
-      }
-
-      try
-      {
-        auto ptr = new libremidi_midi_in_handle{
-            libremidi::midi_in{std::move(conf), std::move(api_conf)}};
-        *out = ptr;
-      }
-      catch (...)
-      {
-        return -EINVAL;
-      }
-      break;
-    }
-    default:
-      return -EINVAL;
-  }
-
-  // Open the port
-  auto ptr = *out;
-  if (!ptr)
-    return -EINVAL;
-
-  if (c->virtual_port)
-  {
-    if (auto ret = ptr->self.open_virtual_port(c->port_name); ret != stdx::error{})
-    {
-      *out = nullptr;
-      delete ptr;
-      return -EIO;
-    }
-  }
-  else
-  {
-    auto port = reinterpret_cast<const libremidi::input_port*>(c->in_port);
-    if (auto ret = ptr->self.open_port(*port); ret != stdx::error{})
-    {
-      *out = nullptr;
-      delete ptr;
-      return -EIO;
-    }
-  }
-
-  return 0;
-}
-int libremidi_midi_in_is_connected(const libremidi_midi_in_handle* in)
-{
-  if (!in)
-    return -EINVAL;
-  return in->self.is_port_connected() ? 1 : 0;
-}
-
-libremidi_timestamp libremidi_midi_in_absolute_timestamp(libremidi_midi_in_handle* in)
-{
-  if (!in)
-    return -EINVAL;
-  return in->self.absolute_timestamp();
-}
-
-int libremidi_midi_in_free(libremidi_midi_in_handle* ptr)
-{
-  delete ptr;
-  return 0;
-}
-
-int libremidi_midi_out_new(
-    const libremidi_midi_configuration* c, const libremidi_api_configuration* api,
-    libremidi_midi_out_handle** out)
-{
-  if (!out || !c)
-    return -EINVAL;
-  if (!c->virtual_port && !c->out_port)
-    return -EINVAL;
-
-  *out = nullptr;
-
-  auto api_conf = libremidi::midi_out_configuration_for(static_cast<libremidi::API>(api->api));
-
-  // Create the MIDI object
-  libremidi::output_configuration conf;
-  libremidi::assign_error_callback(c->on_error, conf.on_error);
-  libremidi::assign_error_callback(c->on_warning, conf.on_warning);
-  conf.timestamps = c->timestamps;
-  try
-  {
-    auto ptr
-        = new libremidi_midi_out_handle{libremidi::midi_out{std::move(conf), std::move(api_conf)}};
-    *out = ptr;
-  }
-  catch (...)
-  {
-    return -EINVAL;
-  }
-
-  // Open the port
-  auto ptr = *out;
-  if (!ptr)
-    return -EINVAL;
-
-  if (c->virtual_port)
-  {
-    if (auto ret = ptr->self.open_virtual_port(c->port_name); ret != stdx::error{})
-    {
-      *out = nullptr;
-      delete ptr;
-      return -EIO;
-    }
-  }
-  else
-  {
-    auto port = reinterpret_cast<const libremidi::output_port*>(c->in_port);
-    if (auto ret = ptr->self.open_port(*port); ret != stdx::error{})
-    {
-      *out = nullptr;
-      delete ptr;
-      return -EIO;
-    }
-  }
-  return 0;
-}
-
-int libremidi_midi_out_is_connected(const libremidi_midi_out_handle* out)
-{
-  if (!out)
-    return -EINVAL;
-  return out->self.is_port_connected() ? 1 : 0;
-}
-
-int libremidi_midi_out_send_message(
-    libremidi_midi_out_handle* out, const libremidi_midi1_symbol* msg, size_t sz)
-{
-  if (!out || !msg || sz > std::numeric_limits<int32_t>::max())
-    return -EINVAL;
-
-  auto res = out->self.send_message(msg, sz);
-  return res != stdx::error{} ? -EIO : 0;
-}
-
-int libremidi_midi_out_send_ump(libremidi_midi_out_handle* out, const libremidi_midi2_symbol* msg, size_t sz)
-{
-  if (!out || !msg || sz > std::numeric_limits<int32_t>::max())
-    return -EINVAL;
-
-  auto res = out->self.send_ump(msg, sz);
-  return res != stdx::error{} ? -EIO : 0;
-}
-
-int libremidi_midi_out_schedule_message(
-    libremidi_midi_out_handle* out, int64_t ts, const libremidi_midi1_symbol* msg, size_t sz)
-{
-  if (!out || !msg || sz > std::numeric_limits<int32_t>::max())
-    return -EINVAL;
-
-  auto res = out->self.schedule_message(ts, msg, sz);
-  return res != stdx::error{} ? -EIO : 0;
-}
-
-int libremidi_midi_out_schedule_ump(
-    libremidi_midi_out_handle* out, int64_t ts, const libremidi_midi2_symbol* msg, size_t sz)
-{
-  if (!out || !msg || sz > std::numeric_limits<int32_t>::max())
-    return -EINVAL;
-
-  auto res = out->self.schedule_ump(ts, msg, sz);
-  return res != stdx::error{} ? -EIO : 0;
-}
-
-int libremidi_midi_out_free(libremidi_midi_out_handle* ptr)
-{
-  delete ptr;
-  return 0;
-}
-}
diff --git a/libremidi/libremidi-c.h b/libremidi/libremidi-c.h
deleted file mode 100644
--- a/libremidi/libremidi-c.h
+++ /dev/null
@@ -1,275 +0,0 @@
-#pragma once
-#include <libremidi/api-c.h>
-
-#include <stdbool.h>
-#include <stddef.h>
-#include <stdint.h>
-
-#if defined(LIBREMIDI_EXPORTS)
-  #if defined(_MSC_VER)
-    #define LIBREMIDI_EXPORT __declspec(dllexport)
-  #elif defined(__GNUC__) || defined(__clang__)
-    #define LIBREMIDI_EXPORT __attribute__((visibility("default")))
-  #endif
-#else
-  #define LIBREMIDI_EXPORT
-#endif
-
-#if __cplusplus
-extern "C" {
-#endif
-typedef unsigned char libremidi_midi1_symbol;
-typedef libremidi_midi1_symbol* libremidi_midi1_message;
-
-typedef uint32_t libremidi_midi2_symbol;
-typedef libremidi_midi2_symbol* libremidi_midi2_message;
-
-typedef int64_t libremidi_timestamp;
-
-typedef struct libremidi_midi_in_port libremidi_midi_in_port;
-typedef struct libremidi_midi_out_port libremidi_midi_out_port;
-typedef struct libremidi_midi_in_handle libremidi_midi_in_handle;
-typedef struct libremidi_midi_out_handle libremidi_midi_out_handle;
-typedef struct libremidi_midi_observer_handle libremidi_midi_observer_handle;
-
-typedef struct libremidi_api_configuration libremidi_api_configuration;
-
-enum libremidi_timestamp_mode
-{
-  NoTimestamp,
-
-  Relative,
-  Absolute,
-  SystemMonotonic,
-  AudioFrame,
-  Custom
-};
-
-typedef struct libremidi_api_configuration
-{
-  enum libremidi_api api;
-  enum
-  {
-    Observer,
-    Input,
-    Output
-  } configuration_type;
-
-  void* data;
-} libremidi_api_configuration;
-
-typedef struct libremidi_observer_configuration
-{
-  struct
-  {
-    void* context;
-    void (*callback)(void* ctx, const char* error, size_t error_len, const void* source_location);
-  } on_error;
-  struct
-  {
-    void* context;
-    void (*callback)(void* ctx, const char* error, size_t error_len, const void* source_location);
-  } on_warning;
-
-  struct
-  {
-    void* context;
-    void (*callback)(void* ctx, const libremidi_midi_in_port*);
-  } input_added;
-  struct
-  {
-    void* context;
-    void (*callback)(void* ctx, const libremidi_midi_in_port*);
-  } input_removed;
-  struct
-  {
-    void* context;
-    void (*callback)(void* ctx, const libremidi_midi_out_port*);
-  } output_added;
-  struct
-  {
-    void* context;
-    void (*callback)(void* ctx, const libremidi_midi_out_port*);
-  } output_removed;
-
-  bool track_hardware;
-  bool track_virtual;
-  bool track_any;
-  bool notify_in_constructor;
-} libremidi_observer_configuration;
-
-typedef struct libremidi_midi1_callback
-{
-  void* context;
-  void (*callback)(void* ctx, libremidi_timestamp, const libremidi_midi1_symbol*, size_t len);
-} libremidi_midi1_callback;
-
-typedef struct libremidi_midi2_callback
-{
-  void* context;
-  void (*callback)(void* ctx, libremidi_timestamp, const libremidi_midi2_symbol*, size_t len);
-} libremidi_midi2_callback;
-
-typedef struct libremidi_midi_configuration
-{
-  // Indicates the kind of callback requested, e.g. set MIDI1_RAW if you use the on_midi1_raw_data
-  enum
-  {
-    MIDI1 = (1 << 1),
-    MIDI1_RAW = (1 << 2),
-    MIDI2 = (1 << 3),
-    MIDI2_RAW = (1 << 4)
-  } version;
-
-  union
-  {
-    libremidi_midi_in_port* in_port;
-    libremidi_midi_out_port* out_port;
-  };
-
-  union
-  {
-    libremidi_midi1_callback on_midi1_message;
-    libremidi_midi1_callback on_midi1_raw_data;
-    libremidi_midi2_callback on_midi2_message;
-    libremidi_midi2_callback on_midi2_raw_data;
-  };
-
-  struct
-  {
-    void* context;
-    libremidi_timestamp (*callback)(void* ctx, libremidi_timestamp);
-  } get_timestamp;
-
-  struct
-  {
-    void* context;
-    void (*callback)(void* ctx, const char* error, size_t error_len, const void* source_location);
-  } on_error;
-  struct
-  {
-    void* context;
-    void (*callback)(void* ctx, const char* error, size_t error_len, const void* source_location);
-  } on_warning;
-
-  const char* port_name;
-  bool virtual_port;
-
-  bool ignore_sysex;
-  bool ignore_timing;
-  bool ignore_sensing;
-
-  enum libremidi_timestamp_mode timestamps;
-} libremidi_midi_configuration;
-
-/// API utilities
-LIBREMIDI_EXPORT
-const char* libremidi_get_version(void);
-
-LIBREMIDI_EXPORT
-void libremidi_midi1_available_apis(void* ctx, void (*)(void*, libremidi_api));
-LIBREMIDI_EXPORT
-void libremidi_midi2_available_apis(void* ctx, void (*)(void*, libremidi_api));
-LIBREMIDI_EXPORT
-const char* libremidi_api_identifier(libremidi_api);
-LIBREMIDI_EXPORT
-const char* libremidi_api_display_name(libremidi_api);
-LIBREMIDI_EXPORT
-libremidi_api libremidi_get_compiled_api_by_identifier(const char*);
-
-/// Create configurations
-LIBREMIDI_EXPORT
-int libremidi_midi_api_configuration_init(libremidi_api_configuration*);
-
-LIBREMIDI_EXPORT
-int libremidi_midi_observer_configuration_init(libremidi_observer_configuration*);
-
-LIBREMIDI_EXPORT
-int libremidi_midi_configuration_init(libremidi_midi_configuration*);
-
-/// Read information about port objects
-LIBREMIDI_EXPORT
-int libremidi_midi_in_port_clone(const libremidi_midi_in_port* port, libremidi_midi_in_port** dst);
-
-LIBREMIDI_EXPORT
-int libremidi_midi_in_port_free(libremidi_midi_in_port* port);
-
-LIBREMIDI_EXPORT
-int libremidi_midi_in_port_name(
-    const libremidi_midi_in_port* port, const char** name, size_t* len);
-
-LIBREMIDI_EXPORT
-int libremidi_midi_out_port_clone(
-    const libremidi_midi_out_port* port, libremidi_midi_out_port** dst);
-
-LIBREMIDI_EXPORT
-int libremidi_midi_out_port_free(libremidi_midi_out_port* port);
-
-LIBREMIDI_EXPORT
-int libremidi_midi_out_port_name(
-    const libremidi_midi_out_port* port, const char** name, size_t* len);
-
-/// Observer API
-LIBREMIDI_EXPORT
-int libremidi_midi_observer_new(
-    const libremidi_observer_configuration*, libremidi_api_configuration*,
-    libremidi_midi_observer_handle**);
-
-LIBREMIDI_EXPORT
-int libremidi_midi_observer_enumerate_input_ports(
-    libremidi_midi_observer_handle*, void* context,
-    void (*)(void* ctx, const libremidi_midi_in_port*));
-
-LIBREMIDI_EXPORT
-int libremidi_midi_observer_enumerate_output_ports(
-    libremidi_midi_observer_handle*, void* context,
-    void (*)(void* ctx, const libremidi_midi_out_port*));
-
-LIBREMIDI_EXPORT
-int libremidi_midi_observer_free(libremidi_midi_observer_handle*);
-
-/// MIDI input API (read MIDI messages)
-LIBREMIDI_EXPORT
-int libremidi_midi_in_new(
-    const libremidi_midi_configuration*, const libremidi_api_configuration*,
-    libremidi_midi_in_handle**);
-
-LIBREMIDI_EXPORT
-int libremidi_midi_in_is_connected(const libremidi_midi_in_handle*);
-
-LIBREMIDI_EXPORT
-libremidi_timestamp libremidi_midi_in_absolute_timestamp(libremidi_midi_in_handle*);
-
-LIBREMIDI_EXPORT
-int libremidi_midi_in_free(libremidi_midi_in_handle*);
-
-/// MIDI output API (send MIDI messages)
-LIBREMIDI_EXPORT
-int libremidi_midi_out_new(
-    const libremidi_midi_configuration*, const libremidi_api_configuration*,
-    libremidi_midi_out_handle**);
-
-LIBREMIDI_EXPORT
-int libremidi_midi_out_is_connected(const libremidi_midi_out_handle*);
-
-LIBREMIDI_EXPORT
-int libremidi_midi_out_send_message(
-    libremidi_midi_out_handle*, const libremidi_midi1_symbol*, size_t);
-
-LIBREMIDI_EXPORT
-int libremidi_midi_out_send_ump(libremidi_midi_out_handle*, const libremidi_midi2_symbol*, size_t);
-
-LIBREMIDI_EXPORT
-int libremidi_midi_out_schedule_message(
-    libremidi_midi_out_handle*, int64_t ts, const libremidi_midi1_symbol*, size_t);
-
-LIBREMIDI_EXPORT
-int libremidi_midi_out_schedule_ump(
-    libremidi_midi_out_handle*, int64_t ts, const libremidi_midi2_symbol*, size_t);
-
-LIBREMIDI_EXPORT
-int libremidi_midi_out_free(libremidi_midi_out_handle*);
-
-#if __cplusplus
-}
-#endif
diff --git a/libremidi/libremidi.cpp b/libremidi/libremidi.cpp
deleted file mode 100644
--- a/libremidi/libremidi.cpp
+++ /dev/null
@@ -1,186 +0,0 @@
-#if !defined(LIBREMIDI_HEADER_ONLY)
-  #include <libremidi/libremidi.hpp>
-#endif
-
-#include <libremidi/backends.hpp>
-#include <libremidi/detail/midi_api.hpp>
-
-#include <cmath>
-
-namespace libremidi
-{
-
-LIBREMIDI_INLINE
-std::string_view get_version() noexcept
-{
-  return LIBREMIDI_VERSION;
-}
-
-LIBREMIDI_INLINE std::string_view get_api_name(libremidi::API api)
-{
-  std::string_view ret;
-  midi_any::for_backend(api, [&](auto& b) { ret = b.name; });
-  return ret;
-}
-
-LIBREMIDI_INLINE std::string_view get_api_display_name(libremidi::API api)
-{
-  std::string_view ret;
-  midi_any::for_backend(api, [&](auto& b) { ret = b.display_name; });
-  return ret;
-}
-
-LIBREMIDI_INLINE libremidi::API get_compiled_api_by_name(std::string_view name)
-{
-  libremidi::API ret = libremidi::API::UNSPECIFIED;
-  midi_any::for_all_backends([&](auto& b) {
-    if (name == b.name)
-      ret = b.API;
-  });
-  return ret;
-}
-
-[[nodiscard]] LIBREMIDI_INLINE std::vector<libremidi::API> available_apis() noexcept
-{
-  std::vector<libremidi::API> apis;
-  midi1::for_all_backends([&](auto b) { apis.push_back(b.API); });
-  return apis;
-}
-
-[[nodiscard]] LIBREMIDI_INLINE std::vector<libremidi::API> available_ump_apis() noexcept
-{
-  std::vector<libremidi::API> apis;
-  midi2::for_all_backends([&](auto b) { apis.push_back(b.API); });
-  return apis;
-}
-LIBREMIDI_INLINE
-libremidi::API midi_api(const std::any& conf)
-{
-  libremidi::API ret = libremidi::API::UNSPECIFIED;
-  midi_any::for_all_backends([&]<typename T>(T) {
-    if (std::any_cast<typename T::midi_in_configuration>(&conf)
-        || std::any_cast<typename T::midi_out_configuration>(&conf)
-        || std::any_cast<typename T::midi_observer_configuration>(&conf))
-    {
-      ret = T::API;
-    }
-  });
-  return ret;
-}
-LIBREMIDI_INLINE
-std::any midi_in_configuration_for(libremidi::API api)
-{
-  std::any ret;
-  midi_any::for_backend(api, [&]<typename T>(T) {
-    using conf_type = typename T::midi_in_configuration;
-    ret = conf_type{};
-  });
-  return ret;
-}
-
-LIBREMIDI_INLINE
-std::any midi_out_configuration_for(libremidi::API api)
-{
-  std::any ret;
-  midi_any::for_backend(api, [&]<typename T>(T) {
-    using conf_type = typename T::midi_out_configuration;
-    ret = conf_type{};
-  });
-  return ret;
-}
-
-LIBREMIDI_INLINE
-std::any observer_configuration_for(libremidi::API api)
-{
-  std::any ret;
-  midi_any::for_backend(api, [&]<typename T>(T) {
-    using conf_type = typename T::midi_observer_configuration;
-    ret = conf_type{};
-  });
-  return ret;
-}
-
-LIBREMIDI_INLINE
-std::any midi_in_configuration_for(const libremidi::observer& obs)
-{
-  return midi_in_configuration_for(obs.get_current_api());
-}
-
-LIBREMIDI_INLINE
-std::any midi_out_configuration_for(const libremidi::observer& obs)
-{
-  // FIXME reuse context when meaningful, e.g. ALSA, JACK...
-  return midi_out_configuration_for(obs.get_current_api());
-}
-
-LIBREMIDI_INLINE
-std::optional<input_port> in_default_port(libremidi::API api) noexcept
-try
-{
-  libremidi::observer obs{{}, observer_configuration_for(api)};
-  if (auto ports = obs.get_input_ports(); !ports.empty())
-    return ports.front();
-  return std::nullopt;
-}
-catch (const std::exception& e)
-{
-  return std::nullopt;
-}
-
-LIBREMIDI_INLINE
-std::optional<output_port> out_default_port(libremidi::API api) noexcept
-try
-{
-  libremidi::observer obs{{}, observer_configuration_for(api)};
-  if (auto ports = obs.get_output_ports(); !ports.empty())
-    return ports.front();
-  return std::nullopt;
-}
-catch (const std::exception& e)
-{
-  return std::nullopt;
-}
-
-namespace midi1
-{
-LIBREMIDI_INLINE
-std::any in_default_configuration()
-{
-  return midi_in_configuration_for(default_api());
-}
-
-LIBREMIDI_INLINE
-std::any out_default_configuration()
-{
-  return midi_out_configuration_for(default_api());
-}
-
-LIBREMIDI_INLINE
-std::any observer_default_configuration()
-{
-  return observer_configuration_for(default_api());
-}
-}
-
-namespace midi2
-{
-LIBREMIDI_INLINE
-std::any in_default_configuration()
-{
-  return midi_in_configuration_for(default_api());
-}
-
-LIBREMIDI_INLINE
-std::any out_default_configuration()
-{
-  return midi_out_configuration_for(default_api());
-}
-
-LIBREMIDI_INLINE
-std::any observer_default_configuration()
-{
-  return observer_configuration_for(default_api());
-}
-}
-
-}
diff --git a/libremidi/midi_in.cpp b/libremidi/midi_in.cpp
deleted file mode 100644
--- a/libremidi/midi_in.cpp
+++ /dev/null
@@ -1,373 +0,0 @@
-#if !defined(LIBREMIDI_HEADER_ONLY)
-  #include <libremidi/libremidi.hpp>
-#endif
-
-#include <libremidi/backends.hpp>
-#include <libremidi/detail/midi_api.hpp>
-
-#include <cassert>
-
-namespace libremidi
-{
-
-static libremidi::ump_input_configuration
-convert_midi1_to_midi2_input_configuration(const input_configuration& base_conf) noexcept
-{
-  libremidi::ump_input_configuration c2;
-  c2.on_message = [cb = base_conf.on_message,
-                   converter = midi2_to_midi1{}](libremidi::ump&& msg) mutable -> void {
-    converter.convert(
-        msg.data, 1, msg.timestamp, [cb](const unsigned char* midi, std::size_t n, int64_t ts) {
-      cb(libremidi::message{{midi, midi + n}, ts});
-      return stdx::error{};
-    });
-  };
-  c2.get_timestamp = base_conf.get_timestamp;
-  c2.on_error = base_conf.on_error;
-  c2.on_warning = base_conf.on_warning;
-  c2.ignore_sysex = base_conf.ignore_sysex;
-  c2.ignore_timing = base_conf.ignore_timing;
-  c2.ignore_sensing = base_conf.ignore_sensing;
-  c2.timestamps = base_conf.timestamps;
-  return c2;
-}
-
-static libremidi::input_configuration
-convert_midi2_to_midi1_input_configuration(const ump_input_configuration& base_conf) noexcept
-{
-  libremidi::input_configuration c2;
-  c2.on_message = [cb = base_conf.on_message,
-                   converter = midi1_to_midi2{}](libremidi::message&& msg) mutable -> void {
-    converter.convert(
-        msg.bytes.data(), msg.bytes.size(), msg.timestamp,
-        [cb](const uint32_t* ump, std::size_t n, int64_t ts) {
-      if(n >= 4)
-      {
-        libremidi::ump u{ump[0],ump[1],ump[2],ump[3]};
-        u.timestamp = ts;
-        cb(std::move(u));
-      }
-      return stdx::error{};
-    });
-  };
-  c2.get_timestamp = base_conf.get_timestamp;
-  c2.on_error = base_conf.on_error;
-  c2.on_warning = base_conf.on_warning;
-  c2.ignore_sysex = base_conf.ignore_sysex;
-  c2.ignore_timing = base_conf.ignore_timing;
-  c2.ignore_sensing = base_conf.ignore_sensing;
-  c2.timestamps = base_conf.timestamps;
-  return c2;
-}
-
-static LIBREMIDI_INLINE std::unique_ptr<midi_in_api>
-make_midi_in(auto base_conf, std::any api_conf, auto backends)
-{
-  std::unique_ptr<midi_in_api> ptr;
-
-  assert(base_conf.on_message || base_conf.on_raw_data);
-
-  auto from_api = [&]<typename T>(T& /*backend*/) mutable {
-    if (auto conf = std::any_cast<typename T::midi_in_configuration>(&api_conf))
-    {
-      ptr = libremidi::make<typename T::midi_in>(std::move(base_conf), std::move(*conf));
-      return true;
-    }
-    return false;
-  };
-  std::apply([&](auto&&... b) { (from_api(b) || ...); }, backends);
-  return ptr;
-}
-
-/// MIDI 1 helpers
-static LIBREMIDI_INLINE std::unique_ptr<midi_in_api>
-make_midi1_in(const input_configuration& base_conf)
-{
-  for (const auto& api : available_apis())
-  {
-    try
-    {
-      auto impl
-          = make_midi_in(base_conf, midi_in_configuration_for(api), midi1::available_backends);
-      if (impl)
-        return impl;
-    }
-    catch (const std::exception& e)
-    {
-    }
-  }
-
-  // No MIDI 1 backend, try the MIDI 2 ones with a wrap:
-  {
-    auto c2 = convert_midi1_to_midi2_input_configuration(base_conf);
-    for (const auto& api : available_ump_apis())
-    {
-      try
-      {
-        auto impl = make_midi_in(c2, midi_in_configuration_for(api), midi2::available_backends);
-        if (impl)
-          return impl;
-      }
-      catch (const std::exception& e)
-      {
-      }
-    }
-  }
-
-  return std::make_unique<midi_in_dummy>(input_configuration{}, dummy_configuration{});
-}
-
-static LIBREMIDI_INLINE std::unique_ptr<midi_in_api>
-make_midi1_in(const input_configuration& base_conf, const std::any& api_conf, libremidi::API api)
-{
-  if (libremidi::is_midi1(api))
-  {
-    return make_midi_in(base_conf, api_conf, midi1::available_backends);
-  }
-  else if (libremidi::is_midi2(api))
-  {
-    auto c2 = convert_midi1_to_midi2_input_configuration(base_conf);
-    return make_midi_in(c2, api_conf, midi2::available_backends);
-  }
-  return {};
-}
-
-static LIBREMIDI_INLINE std::unique_ptr<midi_in_api>
-make_midi1_in(const input_configuration& base_conf, const std::any& api_conf)
-{
-  if (!api_conf.has_value())
-  {
-    return make_midi1_in(base_conf);
-  }
-  else if (auto api_p = std::any_cast<libremidi::API>(&api_conf))
-  {
-    if (*api_p == libremidi::API::UNSPECIFIED)
-    {
-      return make_midi1_in(base_conf);
-    }
-    else
-    {
-      return make_midi1_in(base_conf, midi_in_configuration_for(*api_p), *api_p);
-    }
-  }
-  else
-  {
-    if (auto api = libremidi::midi_api(api_conf); api == libremidi::API::UNSPECIFIED)
-      return {};
-    else
-      return make_midi1_in(base_conf, api_conf, libremidi::midi_api(api_conf));
-  }
-}
-
-/// MIDI 1 constructors
-LIBREMIDI_INLINE midi_in::midi_in(const input_configuration& base_conf) noexcept
-    : impl_{make_midi1_in(base_conf)}
-{
-}
-
-LIBREMIDI_INLINE
-midi_in::midi_in(input_configuration base_conf, std::any api_conf)
-    : impl_{make_midi1_in(base_conf, api_conf)}
-{
-  if (!impl_)
-  {
-    error_handler e;
-    e.libremidi_handle_error(base_conf, "Could not open midi in for the given api");
-    impl_ = std::make_unique<midi_in_dummy>(input_configuration{}, dummy_configuration{});
-  }
-}
-
-/// MIDI 2 helpers
-static LIBREMIDI_INLINE std::unique_ptr<midi_in_api>
-make_midi2_in(const ump_input_configuration& base_conf)
-{
-  for (const auto& api : available_ump_apis())
-  {
-    try
-    {
-      if (auto ret
-          = make_midi_in(base_conf, midi_in_configuration_for(api), midi2::available_backends))
-        return ret;
-    }
-    catch (const std::exception& e)
-    {
-    }
-  }
-
-  // No MIDI 2 backend, try the MIDI 1 ones with a wrap:
-  {
-    auto c2 = convert_midi2_to_midi1_input_configuration(base_conf);
-    for (const auto& api : available_apis())
-    {
-      try
-      {
-        if (auto ret = make_midi_in(c2, midi_in_configuration_for(api), midi1::available_backends))
-          return ret;
-      }
-      catch (const std::exception& e)
-      {
-      }
-    }
-  }
-
-  return {};
-}
-
-static LIBREMIDI_INLINE std::unique_ptr<midi_in_api> make_midi2_in(
-    const ump_input_configuration& base_conf, const std::any& api_conf, libremidi::API api)
-{
-  if (is_midi2(api))
-  {
-    return make_midi_in(base_conf, api_conf, midi2::available_backends);
-  }
-  else if (is_midi1(api))
-  {
-    auto c2 = convert_midi2_to_midi1_input_configuration(base_conf);
-    return make_midi_in(c2, api_conf, midi1::available_backends);
-  }
-
-  return {};
-}
-
-static LIBREMIDI_INLINE std::unique_ptr<midi_in_api>
-make_midi2_in(const ump_input_configuration& base_conf, const std::any& api_conf)
-{
-  if (!api_conf.has_value())
-  {
-    return make_midi2_in(base_conf);
-  }
-  else if (auto api_p = std::any_cast<libremidi::API>(&api_conf))
-  {
-    if (*api_p == libremidi::API::UNSPECIFIED)
-    {
-      return make_midi2_in(base_conf);
-    }
-    else
-    {
-      return make_midi2_in(base_conf, midi_in_configuration_for(*api_p), *api_p);
-    }
-  }
-  else
-  {
-    if (auto api = libremidi::midi_api(api_conf); api == libremidi::API::UNSPECIFIED)
-      return {};
-    else
-      return make_midi2_in(base_conf, api_conf, libremidi::midi_api(api_conf));
-  }
-}
-
-/// MIDI 2 constructors
-LIBREMIDI_INLINE midi_in::midi_in(ump_input_configuration base_conf) noexcept
-    : impl_{make_midi2_in(base_conf)}
-{
-}
-
-LIBREMIDI_INLINE
-midi_in::midi_in(ump_input_configuration base_conf, std::any api_conf)
-    : impl_{make_midi2_in(base_conf, api_conf)}
-{
-  if (!impl_)
-  {
-    error_handler e;
-    e.libremidi_handle_error(base_conf, "Could not open midi in for the given api");
-    impl_ = std::make_unique<midi_in_dummy>(input_configuration{}, dummy_configuration{});
-  }
-}
-
-LIBREMIDI_INLINE midi_in::~midi_in() = default;
-
-LIBREMIDI_INLINE midi_in::midi_in(midi_in&& other) noexcept
-    : impl_{std::move(other.impl_)}
-{
-  other.impl_
-      = std::make_unique<libremidi::midi_in_dummy>(input_configuration{}, dummy_configuration{});
-}
-
-LIBREMIDI_INLINE
-stdx::error midi_in::set_port_name(std::string_view portName)
-{
-  if(impl_->is_port_open())
-    return impl_->set_port_name(portName);
-
-  return std::errc::not_connected;
-}
-
-LIBREMIDI_INLINE midi_in& midi_in::operator=(midi_in&& other) noexcept
-{
-  this->impl_ = std::move(other.impl_);
-  other.impl_
-      = std::make_unique<libremidi::midi_in_dummy>(input_configuration{}, dummy_configuration{});
-  return *this;
-}
-
-LIBREMIDI_INLINE
-libremidi::API midi_in::get_current_api() const noexcept
-{
-  return impl_->get_current_api();
-}
-
-LIBREMIDI_INLINE
-stdx::error midi_in::open_port(const input_port& port, std::string_view portName)
-{
-  if (auto err = impl_->is_client_open(); err != stdx::error{})
-    return std::errc::not_connected;
-
-  if (impl_->is_port_open())
-    return std::errc::operation_not_supported;
-
-  auto ret = impl_->open_port(port, portName);
-  if (ret == stdx::error{})
-  {
-    impl_->connected_ = true;
-    impl_->port_open_ = true;
-  }
-  return ret;
-}
-
-LIBREMIDI_INLINE
-stdx::error midi_in::open_virtual_port(std::string_view portName)
-{
-  if (auto err = impl_->is_client_open(); err != stdx::error{})
-    return std::errc::not_connected;
-
-  if (impl_->is_port_open())
-    return std::errc::operation_not_supported;
-
-  auto ret = impl_->open_virtual_port(portName);
-  if (ret == stdx::error{})
-    impl_->port_open_ = true;
-  return ret;
-}
-
-LIBREMIDI_INLINE
-stdx::error midi_in::close_port()
-{
-  if (auto err = impl_->is_client_open(); err != stdx::error{})
-    return std::errc::not_connected;
-
-  auto ret = impl_->close_port();
-
-  impl_->connected_ = false;
-  impl_->port_open_ = false;
-
-  return ret;
-}
-
-LIBREMIDI_INLINE
-bool midi_in::is_port_open() const noexcept
-{
-  return impl_->is_port_open();
-}
-
-LIBREMIDI_INLINE
-bool midi_in::is_port_connected() const noexcept
-{
-  return impl_->is_port_connected();
-}
-
-LIBREMIDI_INLINE
-int64_t midi_in::absolute_timestamp() const noexcept
-{
-  return impl_->absolute_timestamp();
-}
-}
diff --git a/libremidi/midi_out.cpp b/libremidi/midi_out.cpp
deleted file mode 100644
--- a/libremidi/midi_out.cpp
+++ /dev/null
@@ -1,326 +0,0 @@
-#if !defined(LIBREMIDI_HEADER_ONLY)
-  #include <libremidi/libremidi.hpp>
-#endif
-
-#include <libremidi/backends.hpp>
-#include <libremidi/detail/midi_api.hpp>
-
-#include <array>
-#include <cassert>
-
-namespace libremidi
-{
-static LIBREMIDI_INLINE std::unique_ptr<midi_out_api>
-make_midi_out_impl(auto base_conf, std::any api_conf)
-{
-  std::unique_ptr<midi_out_api> ptr;
-  auto from_api = [&]<typename T>(T& /*backend*/) mutable {
-    if (auto conf = std::any_cast<typename T::midi_out_configuration>(&api_conf))
-    {
-      ptr = libremidi::make<typename T::midi_out>(std::move(base_conf), std::move(*conf));
-      return true;
-    }
-    return false;
-  };
-  std::apply([&](auto&&... b) { (from_api(b) || ...); }, midi1::available_backends);
-  std::apply([&](auto&&... b) { (from_api(b) || ...); }, midi2::available_backends);
-  return ptr;
-}
-
-static LIBREMIDI_INLINE std::unique_ptr<midi_out_api>
-make_midi_out(const output_configuration& base_conf)
-{
-  for (const auto& api : available_apis())
-  {
-    try
-    {
-      if (auto ret = make_midi_out_impl(base_conf, midi_out_configuration_for(api)))
-        return ret;
-    }
-    catch (const std::exception& e)
-    {
-    }
-  }
-
-  for (const auto& api : available_ump_apis())
-  {
-    try
-    {
-      if (auto ret = make_midi_out_impl(base_conf, midi_out_configuration_for(api)))
-        return ret;
-    }
-    catch (const std::exception& e)
-    {
-    }
-  }
-
-  return std::make_unique<midi_out_dummy>(output_configuration{}, dummy_configuration{});
-}
-
-static LIBREMIDI_INLINE std::unique_ptr<midi_out_api>
-make_midi_out(const output_configuration& base_conf, const std::any& api_conf)
-{
-  if (!api_conf.has_value())
-  {
-    return make_midi_out(base_conf);
-  }
-  else if (auto api_p = std::any_cast<libremidi::API>(&api_conf))
-  {
-    if (*api_p == libremidi::API::UNSPECIFIED)
-    {
-      return make_midi_out(base_conf);
-    }
-    else
-    {
-      return make_midi_out_impl(base_conf, midi_out_configuration_for(*api_p));
-    }
-  }
-  else
-  {
-    if (auto api = libremidi::midi_api(api_conf); api == libremidi::API::UNSPECIFIED)
-      return make_midi_out(base_conf);
-    else
-      return make_midi_out_impl(base_conf, api_conf);
-  }
-}
-
-LIBREMIDI_INLINE midi_out::midi_out(const output_configuration& base_conf) noexcept
-    : impl_{make_midi_out(base_conf)}
-{
-}
-
-LIBREMIDI_INLINE
-midi_out::midi_out(output_configuration base_conf, std::any api_conf)
-    : impl_{make_midi_out(base_conf, api_conf)}
-{
-  if (!impl_)
-  {
-    error_handler e;
-    e.libremidi_handle_error(base_conf, "Could not open midi out for the given api");
-    impl_ = std::make_unique<midi_out_dummy>(output_configuration{}, dummy_configuration{});
-  }
-}
-
-LIBREMIDI_INLINE midi_out::~midi_out() = default;
-
-LIBREMIDI_INLINE midi_out::midi_out(midi_out&& other) noexcept
-    : impl_{std::move(other.impl_)}
-{
-  other.impl_
-      = std::make_unique<libremidi::midi_out_dummy>(output_configuration{}, dummy_configuration{});
-}
-
-LIBREMIDI_INLINE midi_out& midi_out::operator=(midi_out&& other) noexcept
-{
-  this->impl_ = std::move(other.impl_);
-  other.impl_
-      = std::make_unique<libremidi::midi_out_dummy>(output_configuration{}, dummy_configuration{});
-  return *this;
-}
-
-LIBREMIDI_INLINE
-stdx::error midi_out::set_port_name(std::string_view portName) const
-{
-  if(impl_->is_port_open())
-    return impl_->set_port_name(portName);
-
-  return std::errc::not_connected;
-}
-
-LIBREMIDI_INLINE
-libremidi::API midi_out::get_current_api() const noexcept
-{
-  return impl_->get_current_api();
-}
-
-LIBREMIDI_INLINE
-stdx::error midi_out::open_port(const output_port& port, std::string_view portName) const
-{
-  if (auto err = impl_->is_client_open(); err != stdx::error{})
-    return std::errc::not_connected;
-
-  if (impl_->is_port_open())
-    return std::errc::operation_not_supported;
-
-  auto ret = impl_->open_port(port, portName);
-  if (ret == stdx::error{})
-  {
-    impl_->connected_ = true;
-    impl_->port_open_ = true;
-  }
-  return ret;
-}
-
-LIBREMIDI_INLINE
-stdx::error midi_out::open_virtual_port(std::string_view portName) const
-{
-  if (auto err = impl_->is_client_open(); err != stdx::error{})
-    return std::errc::not_connected;
-
-  if (impl_->is_port_open())
-    return std::errc::operation_not_supported;
-
-  auto ret = impl_->open_virtual_port(portName);
-  if (ret == stdx::error{})
-    impl_->port_open_ = true;
-  return ret;
-}
-
-LIBREMIDI_INLINE
-stdx::error midi_out::close_port() const
-{
-  if (auto err = impl_->is_client_open(); err != stdx::error{})
-    return std::errc::not_connected;
-
-  auto ret = impl_->close_port();
-  impl_->connected_ = false;
-  impl_->port_open_ = false;
-  return ret;
-}
-
-LIBREMIDI_INLINE
-bool midi_out::is_port_open() const noexcept
-{
-  return impl_->is_port_open();
-}
-
-LIBREMIDI_INLINE
-bool midi_out::is_port_connected() const noexcept
-{
-  return impl_->is_port_connected();
-}
-
-LIBREMIDI_INLINE
-stdx::error midi_out::send_message(const libremidi::message& message) const
-{
-  return send_message(message.bytes.data(), message.bytes.size());
-}
-
-LIBREMIDI_INLINE
-stdx::error midi_out::send_message(std::span<const unsigned char> message) const
-{
-  return send_message(message.data(), message.size());
-}
-
-LIBREMIDI_INLINE
-stdx::error midi_out::send_message(unsigned char b0) const
-{
-  return send_message(&b0, 1);
-}
-
-LIBREMIDI_INLINE
-stdx::error midi_out::send_message(unsigned char b0, unsigned char b1) const
-{
-  return send_message(std::to_array({b0, b1}));
-}
-
-LIBREMIDI_INLINE
-stdx::error midi_out::send_message(unsigned char b0, unsigned char b1, unsigned char b2) const
-{
-  return send_message(std::to_array({b0, b1, b2}));
-}
-
-LIBREMIDI_INLINE
-stdx::error midi_out::send_message(const unsigned char* message, size_t size) const
-{
-#if defined(LIBREMIDI_ASSERTIONS)
-  assert(size > 0);
-#endif
-
-  return impl_->send_message(message, size);
-}
-
-LIBREMIDI_INLINE
-int64_t midi_out::current_time()
-{
-  return impl_->current_time();
-}
-
-LIBREMIDI_INLINE
-stdx::error midi_out::schedule_message(int64_t ts, const unsigned char* message, size_t size) const
-{
-#if defined(LIBREMIDI_ASSERTIONS)
-  assert(size > 0);
-#endif
-
-  return impl_->schedule_message(ts, message, size);
-}
-
-LIBREMIDI_INLINE
-stdx::error midi_out::send_ump(const uint32_t* message, size_t size) const
-{
-#if defined(LIBREMIDI_ASSERTIONS)
-  assert(size > 0);
-  assert(size <= 4);
-#endif
-
-  return impl_->send_ump(message, size);
-}
-LIBREMIDI_INLINE
-stdx::error midi_out::send_ump(const libremidi::ump& message) const
-{
-  return send_ump(message.data, message.size());
-}
-
-LIBREMIDI_INLINE
-stdx::error midi_out::send_ump(std::span<const uint32_t> message) const
-{
-  return send_ump(message.data(), message.size());
-}
-
-LIBREMIDI_INLINE
-stdx::error midi_out::send_ump(uint32_t b0) const
-{
-  return send_ump(&b0, 1);
-}
-
-LIBREMIDI_INLINE
-stdx::error midi_out::send_ump(uint32_t b0, uint32_t b1) const
-{
-  return send_ump(std::to_array({b0, b1}));
-}
-
-LIBREMIDI_INLINE
-stdx::error midi_out::send_ump(uint32_t b0, uint32_t b1, uint32_t b2) const
-{
-  return send_ump(std::to_array({b0, b1, b2}));
-}
-
-LIBREMIDI_INLINE
-stdx::error midi_out::send_ump(uint32_t b0, uint32_t b1, uint32_t b2, uint32_t b3) const
-{
-  return send_ump(std::to_array({b0, b1, b2, b3}));
-}
-
-LIBREMIDI_INLINE
-stdx::error midi_out::send_ump(int32_t b0) const
-{
-  return send_ump(reinterpret_cast<uint32_t*>(&b0), 1);
-}
-
-LIBREMIDI_INLINE
-stdx::error midi_out::send_ump(int64_t b01) const
-{
-  auto ptr = reinterpret_cast<uint32_t*>(&b01);
-  uint32_t msg[2]{ptr[1], ptr[0]};
-  return send_ump(msg, 2);
-}
-
-LIBREMIDI_INLINE
-stdx::error midi_out::send_ump(uint64_t b01) const
-{
-  auto ptr = reinterpret_cast<uint32_t*>(&b01);
-  uint32_t msg[2]{ptr[1], ptr[0]};
-  return send_ump(msg, 2);
-}
-
-LIBREMIDI_INLINE
-stdx::error midi_out::schedule_ump(int64_t ts, const uint32_t* message, size_t size) const
-{
-#if defined(LIBREMIDI_ASSERTIONS)
-  assert(size > 0);
-#endif
-
-  return impl_->schedule_ump(ts, message, size);
-}
-}
diff --git a/libremidi/observer.cpp b/libremidi/observer.cpp
deleted file mode 100644
--- a/libremidi/observer.cpp
+++ /dev/null
@@ -1,133 +0,0 @@
-#if !defined(LIBREMIDI_HEADER_ONLY)
-  #include <libremidi/libremidi.hpp>
-#endif
-
-#include <libremidi/backends.hpp>
-#include <libremidi/detail/midi_api.hpp>
-
-namespace libremidi
-{
-
-static LIBREMIDI_INLINE std::unique_ptr<observer_api> make_observer(auto base_conf)
-{
-  for (const auto& api : available_apis())
-  {
-    try
-    {
-      if (auto impl_ = make_observer(base_conf, observer_configuration_for(api)))
-        return impl_;
-    }
-    catch (const std::exception& e)
-    {
-    }
-  }
-
-  for (const auto& api : available_ump_apis())
-  {
-    try
-    {
-      if (auto impl_ = make_observer(base_conf, observer_configuration_for(api)))
-        return impl_;
-    }
-    catch (const std::exception& e)
-    {
-    }
-  }
-
-  return std::make_unique<observer_dummy>(observer_configuration{}, dummy_configuration{});
-}
-
-LIBREMIDI_INLINE auto make_observer_impl(auto base_conf, std::any api_conf)
-{
-  std::unique_ptr<observer_api> ptr;
-  auto from_api = [&]<typename T>(T& /*backend*/) mutable {
-    if (auto conf = std::any_cast<typename T::midi_observer_configuration>(&api_conf))
-    {
-      ptr = libremidi::make<typename T::midi_observer>(std::move(base_conf), std::move(*conf));
-      return true;
-    }
-    return false;
-  };
-  std::apply([&](auto&&... b) { (from_api(b) || ...); }, midi1::available_backends);
-  std::apply([&](auto&&... b) { (from_api(b) || ...); }, midi2::available_backends);
-  return ptr;
-}
-
-LIBREMIDI_INLINE std::unique_ptr<observer_api> make_observer(auto base_conf, std::any api_conf)
-{
-  if (!api_conf.has_value())
-  {
-    return make_observer(base_conf);
-  }
-  else if (auto api_p = std::any_cast<libremidi::API>(&api_conf))
-  {
-    if (*api_p == libremidi::API::UNSPECIFIED)
-    {
-      return make_observer(base_conf);
-    }
-    else
-    {
-      return make_observer_impl(base_conf, observer_configuration_for(*api_p));
-    }
-  }
-  else
-  {
-    if (auto api = libremidi::midi_api(api_conf); api == libremidi::API::UNSPECIFIED)
-      return {};
-    else
-      return make_observer_impl(base_conf, api_conf);
-  }
-}
-
-LIBREMIDI_INLINE observer::observer(const observer_configuration& base_conf) noexcept
-    : impl_{make_observer(base_conf)}
-{
-}
-
-LIBREMIDI_INLINE observer::observer(observer_configuration base_conf, std::any api_conf)
-    : impl_{make_observer(base_conf, api_conf)}
-{
-  if (!impl_)
-  {
-    error_handler e;
-    e.libremidi_handle_error(base_conf, "Could not open observer for the given api");
-    impl_ = std::make_unique<observer_dummy>(observer_configuration{}, dummy_configuration{});
-  }
-}
-
-LIBREMIDI_INLINE observer::observer(observer&& other) noexcept
-    : impl_{std::move(other.impl_)}
-{
-  other.impl_ = std::make_unique<libremidi::observer_dummy>(
-      observer_configuration{}, dummy_configuration{});
-}
-
-LIBREMIDI_INLINE observer& observer::operator=(observer&& other) noexcept
-{
-  this->impl_ = std::move(other.impl_);
-  other.impl_ = std::make_unique<libremidi::observer_dummy>(
-      observer_configuration{}, dummy_configuration{});
-  return *this;
-}
-
-LIBREMIDI_INLINE
-observer::~observer() = default;
-
-LIBREMIDI_INLINE
-libremidi::API observer::get_current_api() const noexcept
-{
-  return impl_->get_current_api();
-}
-
-LIBREMIDI_INLINE
-std::vector<libremidi::input_port> observer::get_input_ports() const noexcept
-{
-  return impl_->get_input_ports();
-}
-
-LIBREMIDI_INLINE
-std::vector<libremidi::output_port> observer::get_output_ports() const noexcept
-{
-  return impl_->get_output_ports();
-}
-}
diff --git a/libremidi/reader.cpp b/libremidi/reader.cpp
deleted file mode 100644
--- a/libremidi/reader.cpp
+++ /dev/null
@@ -1,752 +0,0 @@
-/*
-Copyright (c) 2015, Dimitri Diakopoulos All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-* Redistributions of source code must retain the above copyright notice, this
-  list of conditions and the following disclaimer.
-
-* Redistributions in binary form must reproduce the above copyright notice,
-  this list of conditions and the following disclaimer in the documentation
-  and/or other materials provided with the distribution.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-*/
-
-#if !defined(LIBREMIDI_HEADER_ONLY)
-  #include <libremidi/reader.hpp>
-#endif
-#include <libremidi/message.hpp>
-
-#include <algorithm>
-#include <iostream>
-
-// File Parsing Validation Todo:
-// ==============================
-// [] Bad file name
-// [] Bad header
-// [] Unknown header type
-// [] Bad header size
-// [] Bad type
-// [] Bad tmecode
-// [] Header too short
-// [] Track too short
-// [] Event too short
-// ==============================
-
-#if defined(__LIBREMIDI_DEBUG__)
-std::ostream& operator<<(std::ostream& s, const libremidi::message& m)
-{
-  s << "[ MIDI: ";
-  for (auto b : m)
-    s << (unsigned int)b << ' ';
-  s << "]\n";
-  return s;
-}
-#endif
-
-namespace libremidi
-{
-namespace util
-{
-struct no_validator
-{
-  static inline bool validate_track([[maybe_unused]] const midi_track& track) { return true; }
-};
-
-struct validator
-{
-  static inline bool validate_track(const midi_track& track)
-  {
-    if (track.empty())
-    {
-#if defined(__LIBREMIDI_DEBUG__)
-      std::cerr << "empty track" << std::endl;
-#endif
-      return false;
-    }
-
-    // Ensure that there is a unique EOT at the end of the track
-    auto it = std::find_if(track.begin(), track.end(), [](const libremidi::track_event& msg) {
-      static const auto eot = meta_events::end_of_track();
-      return msg.m.bytes == eot.bytes;
-    });
-
-    if (it == track.end())
-    {
-#if defined(__LIBREMIDI_DEBUG__)
-      std::cerr << "track has no END OF TRACK" << std::endl;
-#endif
-      return false;
-    }
-
-    if (&it->m != &track.back().m)
-    {
-#if defined(__LIBREMIDI_DEBUG__)
-      std::cerr << std::distance(it, track.end());
-      std::cerr << "track does not end with END OF TRACK" << std::endl;
-#endif
-      return false;
-    }
-
-    return true;
-  }
-};
-
-// Used when we know that we have enough space
-struct read_unchecked
-{
-  // Read a MIDI-style variable-length integer (big-endian value in groups of 7 bits,
-  // with top bit set to signify that another byte follows).
-  static inline void ensure_size(
-      [[maybe_unused]] const uint8_t* begin, [[maybe_unused]] const uint8_t* end,
-      [[maybe_unused]] int64_t needed)
-  {
-  }
-
-  static inline uint32_t
-  read_variable_length(uint8_t const*& data, [[maybe_unused]] uint8_t const* end)
-  {
-    uint32_t result = 0;
-    while (true)
-    {
-      uint8_t b = *data++;
-      if (b & 0x80)
-      {
-        result += (b & 0x7F);
-        result <<= 7;
-      }
-      else
-      {
-        return result + b; // b is the last byte
-      }
-    }
-  }
-
-  static inline void read_bytes(
-      midi_bytes& buffer, uint8_t const*& data, [[maybe_unused]] const uint8_t* end,
-      const std::size_t num)
-  {
-    buffer.reserve(buffer.size() + num);
-    for (std::size_t i = 0; i < num; ++i)
-      buffer.push_back(*data++);
-  }
-
-  static inline void read_bytes(
-      midi_bytes& buffer, uint8_t const*& data, [[maybe_unused]] const uint8_t* end, const int num)
-  {
-    read_bytes(buffer, data, end, static_cast<std::size_t>(num));
-  }
-
-  static inline uint16_t read_uint16_be(uint8_t const*& data, [[maybe_unused]] const uint8_t* end)
-  {
-    uint16_t result = *data++ << 8u;
-    result += *data++;
-    return result;
-  }
-
-  static inline uint32_t read_uint24_be(uint8_t const*& data, [[maybe_unused]] const uint8_t* end)
-  {
-    uint32_t result = *data++ << 16u;
-    result += static_cast<uint32_t>(*data++ << 8u);
-    result += *data++;
-    return result;
-  }
-
-  static inline uint32_t read_uint32_be(uint8_t const*& data, [[maybe_unused]] const uint8_t* end)
-  {
-    uint32_t result = *data++ << 24u;
-    result += static_cast<uint32_t>(*data++ << 16u);
-    result += static_cast<uint32_t>(*data++ << 8u);
-    result += *data++;
-    return result;
-  }
-};
-
-// Used when we do not know if we have enough bytes and have to check before reading
-struct read_checked
-{
-  // Read a MIDI-style variable-length integer (big-endian value in groups of 7 bits,
-  // with top bit set to signify that another byte follows).
-  static inline void
-  ensure_size(const uint8_t* begin, const uint8_t* end, const std::size_t needed)
-  {
-    if (const auto available = static_cast<std::size_t>(end - begin); available < needed)
-      throw std::runtime_error("MIDI reader: not enough data to process");
-  }
-
-  static inline std::size_t read_variable_length(uint8_t const*& data, uint8_t const* end)
-  {
-    std::size_t result = 0;
-    while (true)
-    {
-      ensure_size(data, end, 1);
-      uint8_t b = *data++;
-      if (b & 0x80)
-      {
-        result += (b & 0x7F);
-        result <<= 7;
-      }
-      else
-      {
-        return result + b; // b is the last byte
-      }
-    }
-  }
-
-  static inline void
-  read_bytes(midi_bytes& buffer, uint8_t const*& data, uint8_t const* end, const std::size_t num)
-  {
-    ensure_size(data, end, num);
-    read_unchecked::read_bytes(buffer, data, end, num);
-  }
-
-  static inline uint16_t read_uint16_be(uint8_t const*& data, uint8_t const* end)
-  {
-    ensure_size(data, end, 2);
-    return read_unchecked::read_uint16_be(data, end);
-  }
-
-  static inline uint32_t read_uint24_be(uint8_t const*& data, uint8_t const* end)
-  {
-    ensure_size(data, end, 3);
-    return read_unchecked::read_uint24_be(data, end);
-  }
-
-  static inline uint32_t read_uint32_be(uint8_t const*& data, uint8_t const* end)
-  {
-    ensure_size(data, end, 4);
-    return read_unchecked::read_uint32_be(data, end);
-  }
-};
-}
-
-#if defined(LIBREMIDI_UNCHECKED)
-using byte_reader = util::read_unchecked;
-#else
-using byte_reader = util::read_checked;
-#endif
-
-#if defined(LIBREMIDI_UNVALIDATED)
-using validator = util::no_validator;
-#else
-using validator = util::validator;
-#endif
-
-LIBREMIDI_INLINE
-track_event parse_event(
-    int tick, int track, const uint8_t*& dataStart, const uint8_t* dataEnd,
-    message_type lastEventTypeByte)
-{
-  byte_reader::ensure_size(dataStart, dataEnd, 1);
-  auto type = static_cast<message_type>(*dataStart++);
-
-  track_event event{tick, track, message{}};
-
-  if ((static_cast<uint8_t>(type) & 0xF0) == 0xF0)
-  {
-    // Meta event
-    if (static_cast<uint8_t>(type) == 0xFF)
-    {
-      byte_reader::ensure_size(dataStart, dataEnd, 1);
-      auto subtype = static_cast<meta_event_type>(*dataStart++);
-
-      event.m.bytes.reserve(3);
-      event.m.bytes.push_back(static_cast<uint8_t>(type));
-      event.m.bytes.push_back(static_cast<uint8_t>(subtype));
-
-      uint32_t length = 0;
-      // Here we read the meta-event length manually, as this way we can also put it into
-      // event.m.bytes
-      while (true)
-      {
-        byte_reader::ensure_size(dataStart, dataEnd, 1);
-        uint8_t b = *dataStart++;
-        event.m.bytes.push_back(b);
-        if (b & 0x80)
-        {
-          const uint8_t byte = (b & 0x7F);
-
-          length += byte;
-          length <<= 7;
-        }
-        else
-        {
-          length += b; // b is the last byte
-          break;
-        }
-      }
-
-      switch (subtype)
-      {
-        case meta_event_type::SEQUENCE_NUMBER: {
-          switch (length)
-          {
-            case 0:
-              return event;
-            case 2:
-              byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, 2);
-              return event;
-            default:
-              throw std::invalid_argument("Expected length for SEQUENCE_NUMBER event is 0 or 2");
-          }
-        }
-        case meta_event_type::TEXT:
-        case meta_event_type::COPYRIGHT:
-        case meta_event_type::TRACK_NAME:
-        case meta_event_type::INSTRUMENT:
-        case meta_event_type::LYRIC:
-        case meta_event_type::MARKER:
-        case meta_event_type::CUE:
-        case meta_event_type::PATCH_NAME:
-        case meta_event_type::DEVICE_NAME: {
-          byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length);
-          return event;
-        }
-
-        case meta_event_type::END_OF_TRACK: {
-          if (length != 0)
-            throw std::invalid_argument("Expected length for END_OF_TRACK event is 0");
-          return event;
-        }
-        case meta_event_type::TEMPO_CHANGE: {
-          if (length != 3)
-            throw std::invalid_argument("Expected length for TEMPO_CHANGE event is 3");
-          // event.m.bytes[3] = read_uint24_be(dataStart); // @dimitri TOFIX
-          byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length);
-          return event;
-        }
-        case meta_event_type::SMPTE_OFFSET: {
-          if (length != 5)
-            throw std::invalid_argument("Expected length for SMPTE_OFFSET event is 5");
-          byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length);
-          auto& b = event.m.bytes;
-
-          uint8_t format = (b[3] & 0b01100000) >> 5;
-          uint8_t h = (b[3] & 0b00011111);
-
-          if (format > 3)
-            throw std::invalid_argument("SMPTE_OFFSET has unknown format");
-
-          int max = 0;
-          switch (format)
-          {
-            case 0: // 24
-              max = 24;
-              break;
-            case 1: // 25
-              max = 25;
-              break;
-            case 2: // 29
-              max = 29;
-              break;
-            case 3: // 30
-              max = 30;
-              break;
-            default:
-              break;
-          }
-
-          if (h >= 24 || b[4] >= 60 || b[5] >= 60 || b[6] >= max || b[7] >= 100)
-            throw std::invalid_argument("SMPTE_OFFSET is out-of-23:59:59:xx:99 bounds");
-          return event;
-        }
-        case meta_event_type::TIME_SIGNATURE: {
-          if (length != 4)
-            throw std::invalid_argument("Expected length for TIME_SIGNATURE event is 4");
-          byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length);
-          return event;
-        }
-        case meta_event_type::KEY_SIGNATURE: {
-          if (length != 2)
-            throw std::invalid_argument("Expected length for KEY_SIGNATURE event is 2");
-          byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length);
-          auto k = static_cast<int8_t>(event.m[3]);
-          if (k < -7 || k > 7)
-            throw std::invalid_argument("Invalid KEY_SIGNATURE");
-          if (event.m[4] > 1)
-            throw std::invalid_argument("Invalid KEY_SIGNATURE");
-          return event;
-        }
-        case meta_event_type::PROPRIETARY: {
-          byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length);
-          return event;
-        }
-        case meta_event_type::CHANNEL_PREFIX: {
-          if (length != 1)
-            throw std::invalid_argument("Expected length for CHANNEL_PREFIX event is 1");
-          byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length);
-          return event;
-        }
-        case meta_event_type::MIDI_PORT: {
-          if (length != 1)
-            throw std::invalid_argument("Expected length for MIDI_PORT event is 1");
-          byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length);
-          return event;
-        }
-        case meta_event_type::UNKNOWN:
-        default: {
-          // Unknown events?
-          byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length);
-          return event;
-        }
-      }
-    }
-
-    else if (type == message_type::SYSTEM_EXCLUSIVE)
-    {
-      const auto length = byte_reader::read_variable_length(dataStart, dataEnd);
-      event.m.bytes = {static_cast<uint8_t>(type)};
-      byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length);
-      return event;
-    }
-
-    else if (type == message_type::EOX)
-    {
-      const auto length = byte_reader::read_variable_length(dataStart, dataEnd);
-      byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length);
-      return event;
-    }
-    else
-    {
-      throw std::runtime_error("Unrecognised MIDI event type byte");
-    }
-  }
-
-  // Channel events
-  else
-  {
-    event.m.bytes.clear();
-
-    // Running status...
-    if ((static_cast<uint8_t>(type) & 0x80) == 0)
-    {
-      // Reuse lastEventTypeByte as the event type.
-      // eventTypeByte is actually the first parameter
-      event.m.bytes.push_back(static_cast<uint8_t>(lastEventTypeByte));
-      event.m.bytes.push_back(static_cast<uint8_t>(type));
-      type = lastEventTypeByte;
-    }
-    else
-    {
-      event.m.bytes.push_back(static_cast<uint8_t>(type));
-
-      byte_reader::ensure_size(dataStart, dataEnd, 1);
-      event.m.bytes.push_back(static_cast<uint8_t>(*dataStart++));
-      lastEventTypeByte = type;
-    }
-
-    static constexpr auto validate = [](midi_bytes& b) {
-      if (b[1] < 128 && b[2] < 128)
-        return true;
-      throw std::invalid_argument("MIDI message has arguments > 127");
-    };
-
-    switch (static_cast<message_type>(static_cast<uint8_t>(type) & 0xF0))
-    {
-      case message_type::NOTE_OFF:
-      case message_type::NOTE_ON:
-      case message_type::POLY_PRESSURE:
-      case message_type::CONTROL_CHANGE:
-        byte_reader::ensure_size(dataStart, dataEnd, 1);
-        event.m.bytes.push_back(*dataStart++);
-        validate(event.m.bytes);
-        return event;
-      case message_type::PROGRAM_CHANGE:
-        if (event.m.bytes[1] >= 128)
-          throw std::invalid_argument("MIDI PC has arguments > 127");
-        return event;
-      case message_type::AFTERTOUCH:
-        if (event.m.bytes[1] >= 128)
-          throw std::invalid_argument("MIDI Atertouch has arguments > 127");
-        return event;
-      case message_type::PITCH_BEND:
-        byte_reader::ensure_size(dataStart, dataEnd, 1);
-        event.m.bytes.push_back(*dataStart++);
-        validate(event.m.bytes);
-        return event;
-
-      case message_type::TIME_CODE:
-        throw std::runtime_error("Unsupported MIDI event type TIME_CODE");
-      case message_type::SONG_POS_POINTER:
-        throw std::runtime_error("Unsupported MIDI event type SONG_POS_POINTER");
-      case message_type::SONG_SELECT:
-        throw std::runtime_error("Unsupported MIDI event type SONG_SELECT");
-      case message_type::RESERVED1:
-        throw std::runtime_error("Unsupported MIDI event type RESERVED1");
-      case message_type::RESERVED2:
-        throw std::runtime_error("Unsupported MIDI event type RESERVED2");
-      case message_type::TUNE_REQUEST:
-        throw std::runtime_error("Unsupported MIDI event type TUNE_REQUEST");
-      case message_type::EOX:
-        throw std::runtime_error("Unsupported MIDI event type EOX");
-        // System Realtime Messages :
-      case message_type::TIME_CLOCK:
-        throw std::runtime_error("Unsupported MIDI event type TIME_CLOCK");
-      case message_type::RESERVED3:
-        throw std::runtime_error("Unsupported MIDI event type RESERVED3");
-      case message_type::START:
-        throw std::runtime_error("Unsupported MIDI event type START");
-      case message_type::CONTINUE:
-        throw std::runtime_error("Unsupported MIDI event type CONTINUE");
-      case message_type::STOP:
-        throw std::runtime_error("Unsupported MIDI event type STOP");
-      case message_type::RESERVED4:
-        throw std::runtime_error("Unsupported MIDI event type RESERVED4");
-      case message_type::ACTIVE_SENSING:
-        throw std::runtime_error("Unsupported MIDI event type ACTIVE_SENSING");
-      case message_type::SYSTEM_RESET:
-        throw std::runtime_error("Unsupported MIDI event type SYSTEM_RESET");
-      case message_type::INVALID:
-        throw std::runtime_error("Unsupported MIDI event type INVALID");
-      case message_type::SYSTEM_EXCLUSIVE:
-        throw std::runtime_error("Unsupported MIDI event type SYSTEM_EXCLUSIVE");
-      default:
-        throw std::runtime_error("Unsupported MIDI event type");
-    }
-  }
-}
-
-LIBREMIDI_INLINE
-reader::reader(bool useAbsolute)
-    : ticksPerBeat(480)
-    , startingTempo(120)
-    , useAbsoluteTicks(useAbsolute)
-{
-}
-
-LIBREMIDI_INLINE
-reader::~reader() { }
-
-constexpr int str_to_headerid(const char* str)
-{
-  return str[0] << 24 | str[1] << 16 | str[2] << 8 | str[3];
-}
-
-LIBREMIDI_INLINE
-auto reader::parse(const uint8_t* dataPtr, std::size_t size) noexcept -> parse_result
-try
-{
-  using namespace libremidi::util;
-
-  tracks.clear();
-
-  if (size == 0)
-  {
-#if defined(__LIBREMIDI_DEBUG__)
-    std::cerr << "empty buffer passed to parse." << std::endl;
-#endif
-    return parse_result::invalid;
-  }
-
-  const uint8_t* const dataEnd = dataPtr + size;
-
-  uint32_t headerId = read_checked::read_uint32_be(dataPtr, dataEnd);
-  uint32_t headerLength = read_checked::read_uint32_be(dataPtr, dataEnd);
-
-  if (static_cast<int>(headerId) != str_to_headerid("MThd") || headerLength != 6)
-  {
-#if defined(__LIBREMIDI_DEBUG__)
-    std::cerr << "couldn't parse header" << std::endl;
-#endif
-    return parse_result::invalid;
-  }
-
-  format = read_checked::read_uint16_be(
-      dataPtr, dataEnd); //@tofix format type -> save for later eventually
-  if (format > 2)
-  {
-#if defined(__LIBREMIDI_DEBUG__)
-    std::cerr << "unknown format" << std::endl;
-#endif
-    return parse_result::invalid;
-  }
-  int trackCount = read_checked::read_uint16_be(dataPtr, dataEnd);
-  uint16_t timeDivision = read_checked::read_uint16_be(dataPtr, dataEnd);
-
-  // CBB: deal with the SMPTE style time coding
-  // timeDivision is described here http://www.sonicspot.com/guide/midifiles.html
-  if (timeDivision & 0x8000)
-  {
-#if defined(__LIBREMIDI_DEBUG__)
-    std::cerr << "found SMPTE time frames (unsupported)" << std::endl;
-    int fps = (timeDivision >> 16) & 0x7f;
-    if (fps != -30 && fps != -29 && fps != -25 && fps != -24)
-      return parse_result::invalid;
-    int ticksPerFrame = timeDivision & 0xff;
-#endif
-    // given beats per second, timeDivision should be derivable.
-    return parse_result::invalid;
-  }
-
-  startingTempo = 120.0f;             // midi default
-  ticksPerBeat = float(timeDivision); // ticks per beat (a beat is defined as a quarter note)
-
-  parse_result result = parse_result::validated;
-
-  for (int i = 0; i < trackCount; ++i)
-  {
-    midi_track track;
-
-    headerId = read_checked::read_uint32_be(dataPtr, dataEnd);
-    headerLength = read_checked::read_uint32_be(dataPtr, dataEnd);
-
-    if (headerId != str_to_headerid("MTrk"))
-    {
-#if defined(__LIBREMIDI_DEBUG__)
-      std::cerr << "couldn't find track header" << std::endl;
-#endif
-      return parse_result::incomplete;
-    }
-
-    int64_t available = dataEnd - dataPtr;
-    if (available < headerLength)
-    {
-#if defined(__LIBREMIDI_DEBUG__)
-      std::cerr << "not enough data available" << std::endl;
-#endif
-      return parse_result::incomplete;
-    }
-
-    track.reserve(headerLength / 3);
-
-    const uint8_t* const trackEnd = dataPtr + headerLength;
-
-    auto runningEvent = message_type::INVALID;
-
-    std::size_t tickCount = 0;
-
-    while (dataPtr < trackEnd)
-    {
-      const auto tick = read_checked::read_variable_length(dataPtr, trackEnd);
-      if (useAbsoluteTicks)
-      {
-        tickCount += tick;
-      }
-      else
-      {
-        tickCount = tick;
-      }
-
-      try
-      {
-        track_event ev
-            = parse_event(static_cast<int>(tickCount), i, dataPtr, trackEnd, runningEvent);
-        if (!ev.m.empty())
-        {
-          if (!ev.m.is_meta_event())
-          {
-            runningEvent = static_cast<message_type>(ev.m.bytes[0]);
-          }
-        }
-        else
-        {
-#if defined(__LIBREMIDI_DEBUG__)
-          std::cerr << "could not read event" << std::endl;
-#endif
-          dataPtr = trackEnd;
-          result = parse_result::incomplete;
-          continue;
-        }
-
-        track.push_back(std::move(ev));
-      }
-      catch (const std::exception& e)
-      {
-#if defined(__LIBREMIDI_DEBUG__)
-        std::cerr << "" << e.what() << std::endl;
-#endif
-        dataPtr = trackEnd;
-        result = parse_result::incomplete;
-        continue;
-      }
-    }
-
-    if (result == parse_result::validated)
-    {
-      if (!validator::validate_track(track))
-      {
-        result = parse_result::complete;
-      }
-    }
-    tracks.push_back(std::move(track));
-  }
-
-  if (result == parse_result::validated)
-  {
-    if (dataPtr != dataEnd)
-    {
-#if defined(__LIBREMIDI_DEBUG__)
-      std::cerr << "midifile has junk at end: " << std::intptr_t(dataEnd - dataPtr) << std::endl;
-#endif
-      result = parse_result::complete;
-    }
-  }
-  return result;
-}
-catch (const std::exception& e)
-{
-#if defined(__LIBREMIDI_DEBUG__)
-  std::cerr << "" << e.what() << std::endl;
-#endif
-  return parse_result::invalid;
-}
-
-// In ticks
-LIBREMIDI_INLINE
-double reader::get_end_time() const noexcept
-{
-  if (useAbsoluteTicks)
-  {
-    double totalLength = 0.;
-    for (const auto& t : tracks)
-    {
-      if (!t.empty())
-      {
-        const auto& last_event = t.back();
-        if (last_event.tick > totalLength)
-          totalLength = last_event.tick;
-      }
-    }
-    return totalLength;
-  }
-  else
-  {
-    double totalLength = 0.;
-    for (const auto& t : tracks)
-    {
-      double trackLength = 0.;
-      for (const auto& e : t)
-        trackLength += e.tick;
-
-      if (trackLength > totalLength)
-        totalLength = trackLength;
-    }
-    return totalLength;
-  }
-}
-
-LIBREMIDI_INLINE
-auto reader::parse(const std::vector<uint8_t>& buffer) noexcept -> parse_result
-{
-  return parse(buffer.data(), buffer.size());
-}
-
-#if defined(LIBREMIDI_HAS_SPAN)
-LIBREMIDI_INLINE
-auto reader::parse(std::span<uint8_t> buffer) noexcept -> parse_result
-{
-  return parse(buffer.data(), buffer.size());
-}
-#endif
-}
diff --git a/libremidi/writer.cpp b/libremidi/writer.cpp
deleted file mode 100644
--- a/libremidi/writer.cpp
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
-Copyright (c) 2015, Dimitri Diakopoulos All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-* Redistributions of source code must retain the above copyright notice, this
-  list of conditions and the following disclaimer.
-
-* Redistributions in binary form must reproduce the above copyright notice,
-  this list of conditions and the following disclaimer in the documentation
-  and/or other materials provided with the distribution.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-*/
-
-#if !defined(LIBREMIDI_HEADER_ONLY)
-  #include <libremidi/writer.hpp>
-#endif
-#include <algorithm>
-#include <bit>
-#include <ostream>
-#include <string>
-
-#if !defined(__cpp_lib_bit_cast)
-namespace std
-{
-template <typename R, typename T>
-[[nodiscard]]
-constexpr R bit_cast(const T& v) noexcept
-{
-  union
-  {
-    R res;
-    T init;
-  } u{.init = v};
-  return u.res;
-}
-}
-#endif
-namespace libremidi
-{
-namespace util
-{
-template <typename T>
-static LIBREMIDI_INLINE std::ostream& write_be(std::ostream& out, T value)
-{
-  static_assert(
-      std::endian::native == std::endian::big || std::endian::native == std::endian::little);
-  if constexpr (std::endian::native == std::endian::big)
-  {
-    out << value;
-  }
-  else
-  {
-    static constexpr auto N = sizeof(value);
-    struct storage
-    {
-      uint8_t bytes[N];
-    };
-    auto data = std::bit_cast<storage>(value);
-    std::reverse(data.bytes, data.bytes + N);
-    out.write(reinterpret_cast<const char*>(data.bytes), static_cast<std::streamsize>(N));
-  }
-  return out;
-}
-
-// Write a number to the midifile
-// as a variable length value which segments a file into 7-bit
-// values.  Maximum size of aValue is 0x7fffffff
-static LIBREMIDI_INLINE void write_variable_length(uint32_t aValue, std::vector<uint8_t>& outdata)
-{
-  uint8_t bytes[5] = {0};
-
-  bytes[0] = static_cast<uint8_t>((aValue >> 28) & 0x7F); // most significant 5 bits
-  bytes[1] = static_cast<uint8_t>((aValue >> 21) & 0x7F); // next largest 7 bits
-  bytes[2] = static_cast<uint8_t>((aValue >> 14) & 0x7F);
-  bytes[3] = static_cast<uint8_t>((aValue >> 7) & 0x7F);
-  bytes[4] = static_cast<uint8_t>((aValue) & 0x7F); // least significant 7 bits
-
-  int start = 0;
-  while (start < 5 && bytes[start] == 0)
-    start++;
-
-  for (int i = start; i < 4; i++)
-  {
-    bytes[i] = bytes[i] | 0x80;
-    outdata.push_back(bytes[i]);
-  }
-  outdata.push_back(bytes[4]);
-}
-
-static LIBREMIDI_INLINE void
-add_event_track_count_check(std::vector<midi_track>& tracks, int track)
-{
-  if (track < 0)
-    throw std::out_of_range("Refusing to add an event to track " + std::to_string(track) + ".");
-  if (track > 65535)
-    throw std::out_of_range(
-        "Refusing to add an event to track " + std::to_string(track)
-        + " ; change add_event_track_count_check in libremidi writer.cpp to increase the limit.");
-
-  while (tracks.size() < static_cast<std::size_t>(track + 1)) // NOLINT(*-misplaced-widening-cast)
-    tracks.emplace_back();
-}
-}
-
-LIBREMIDI_INLINE
-void writer::add_event(const int tick, const int track, const message& m)
-{
-  util::add_event_track_count_check(tracks, track);
-
-  tracks[static_cast<std::size_t>(track)].push_back({tick, track, m});
-}
-
-LIBREMIDI_INLINE
-void writer::add_event(int track, const track_event& m)
-{
-  util::add_event_track_count_check(tracks, track);
-
-  tracks[static_cast<std::size_t>(track)].push_back(m);
-}
-
-LIBREMIDI_INLINE
-void writer::add_track()
-{
-  util::add_event_track_count_check(tracks, static_cast<int>(tracks.size() + 1));
-}
-
-LIBREMIDI_INLINE
-void writer::write(std::ostream& out) const
-{
-  // MIDI File Header
-  out.write("MThd", 4);
-  util::write_be<uint32_t>(out, 6);
-  util::write_be<uint16_t>(out, (tracks.size() == 1) ? 0 : 1);
-  util::write_be<uint16_t>(out, static_cast<uint16_t>(tracks.size()));
-  util::write_be<uint16_t>(out, ticksPerQuarterNote);
-
-  std::vector<uint8_t> trackRawData;
-  for (const auto& event_list : tracks)
-  {
-    trackRawData.clear();
-    // Rough estimation of the memory to allocate
-    trackRawData.reserve(event_list.size() * 3);
-
-    for (const auto& event : event_list)
-    {
-      const auto& msg = event.m;
-      if (msg.empty())
-        continue;
-
-      // Suppress end-of-track meta messages (one will be added
-      // automatically after all track data has been written).
-      if (msg.get_meta_event_type() == meta_event_type::END_OF_TRACK)
-        continue;
-
-      util::write_variable_length(static_cast<uint32_t>(event.tick), trackRawData);
-
-      if ((msg.get_message_type() == message_type::SYSTEM_EXCLUSIVE)
-          || (event.m.get_message_type() == message_type::EOX))
-      {
-        // 0xf0 == Complete sysex message (0xf0 is part of the raw MIDI).
-        // 0xf7 == Raw byte message (0xf7 not part of the raw MIDI).
-        // Print the first byte of the message (0xf0 or 0xf7), then
-        // print a VLV length for the rest of the bytes in the message.
-        // In other words, when creating a 0xf0 or 0xf7 MIDI message,
-        // do not insert the VLV byte length yourself, as this code will
-        // do it for you automatically.
-        trackRawData.emplace_back(msg.bytes[0]); // 0xf0 or 0xf7;
-
-        util::write_variable_length(static_cast<uint32_t>(msg.size()) - 1, trackRawData);
-
-        trackRawData.insert(
-            trackRawData.end(), msg.bytes.data() + 1, msg.bytes.data() + msg.bytes.size());
-      }
-      else
-      {
-        // Non-sysex type of message, so just output the bytes of the message:
-        trackRawData.insert(
-            trackRawData.end(), msg.bytes.data(), msg.bytes.data() + msg.bytes.size());
-      }
-    }
-
-    auto size = trackRawData.size();
-    const auto eot = meta_events::end_of_track();
-
-    if ((size < 3) || !((trackRawData[size - 3] == 0xFF) && (trackRawData[size - 2] == 0x2F)))
-    {
-      trackRawData.emplace_back(0x0); // tick
-      trackRawData.emplace_back(eot[0]);
-      trackRawData.emplace_back(eot[1]);
-      trackRawData.emplace_back(eot[2]);
-    }
-
-    // Write the track ID marker "MTrk":
-    out.write("MTrk", 4);
-    util::write_be<uint32_t>(out, static_cast<uint32_t>(trackRawData.size()));
-    out.write(
-        reinterpret_cast<char*>(trackRawData.data()),
-        static_cast<std::streamsize>(trackRawData.size()));
-  }
-}
-}
diff --git a/vendor/libremidi/api-c.h b/vendor/libremidi/api-c.h
new file mode 100644
--- /dev/null
+++ b/vendor/libremidi/api-c.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#if __cplusplus
+extern "C" {
+#endif
+
+//! MIDI API specifier arguments.
+//! To get information on which feature is supported by each back-end, check their backend file
+//! in e.g. backends/winmm.hpp, etc.
+typedef enum libremidi_api
+{
+  UNSPECIFIED = 0x0, /*!< Search for a working compiled API. */
+
+  // MIDI 1.0 APIs
+  COREMIDI = 0x1, /*!< macOS CoreMidi API. */
+  ALSA_SEQ,       /*!< Linux ALSA Sequencer API. */
+  ALSA_RAW,       /*!< Linux Raw ALSA API. */
+  JACK_MIDI,      /*!< JACK Low-Latency MIDI Server API. */
+  WINDOWS_MM,     /*!< Microsoft Multimedia MIDI API. */
+  WINDOWS_UWP,    /*!< Microsoft WinRT MIDI API. */
+  WEBMIDI,        /*!< Web MIDI API through Emscripten */
+  PIPEWIRE,       /*!< PipeWire */
+  KEYBOARD,       /*!< Computer keyboard input */
+  NETWORK,        /*!< MIDI over IP */
+
+  // MIDI 2.0 APIs
+  ALSA_RAW_UMP = 0x1000, /*!< Raw ALSA API for MIDI 2.0 */
+  ALSA_SEQ_UMP,          /*!< Linux ALSA Sequencer API for MIDI 2.0 */
+  COREMIDI_UMP,          /*!< macOS CoreMidi API for MIDI 2.0. Requires macOS 11+ */
+  WINDOWS_MIDI_SERVICES, /*!< Windows API for MIDI 2.0. Requires Windows 11 */
+  KEYBOARD_UMP,          /*!< Computer keyboard input */
+  NETWORK_UMP,           /*!< MIDI2 over IP */
+
+  DUMMY = 0xFFFF /*!< A compilable but non-functional API. */
+} libremidi_api;
+
+#if __cplusplus
+}
+#endif
diff --git a/vendor/libremidi/client.cpp b/vendor/libremidi/client.cpp
new file mode 100644
--- /dev/null
+++ b/vendor/libremidi/client.cpp
@@ -0,0 +1,47 @@
+#if !defined(LIBREMIDI_HEADER_ONLY)
+  #include "client.hpp"
+#endif
+#include <libremidi/backends.hpp>
+#include <libremidi/shared_context.hpp>
+
+#ifdef LIBREMIDI_ALSA
+  #include <libremidi/backends/alsa_seq/shared_handler.hpp>
+#endif
+#ifdef LIBREMIDI_JACK
+  #include <libremidi/backends/jack/shared_handler.hpp>
+#endif
+namespace libremidi
+{
+LIBREMIDI_INLINE
+shared_configurations
+create_shared_context(const libremidi::API api, [[maybe_unused]] std::string_view client_name)
+{
+  switch (api)
+  {
+#if __has_include(<boost/lockfree/spsc_queue.hpp>)
+  #if defined(LIBREMIDI_ALSA)
+    case libremidi::API::ALSA_SEQ:
+      return alsa_seq::shared_handler::make(client_name);
+  #endif
+
+  #if defined(LIBREMIDI_JACK)
+    case libremidi::API::JACK_MIDI:
+      return jack::shared_handler::make(client_name);
+  #endif
+#endif
+    case libremidi::API::COREMIDI:
+    case libremidi::API::COREMIDI_UMP:
+      // TODO
+
+    case libremidi::API::WINDOWS_MIDI_SERVICES:
+      // TODO
+
+    default:
+      return {
+          .context = {},
+          .observer = observer_configuration_for(api),
+          .in = midi_in_configuration_for(api),
+          .out = midi_out_configuration_for(api)};
+  }
+}
+}
diff --git a/vendor/libremidi/libremidi-c.cpp b/vendor/libremidi/libremidi-c.cpp
new file mode 100644
--- /dev/null
+++ b/vendor/libremidi/libremidi-c.cpp
@@ -0,0 +1,507 @@
+// clang-format off
+#include <libremidi/libremidi-c.h>
+#include <libremidi/libremidi.hpp>
+#include <libremidi/backends.hpp>
+// clang-format on
+
+#include <cerrno>
+#include <cstdlib>
+#include <cstring>
+#include <limits>
+
+struct libremidi_midi_observer_handle
+{
+  libremidi::observer self;
+};
+
+struct libremidi_midi_in_handle
+{
+  libremidi::midi_in self;
+};
+
+struct libremidi_midi_out_handle
+{
+  libremidi::midi_out self;
+};
+
+namespace libremidi
+{
+
+static void assign_error_callback(const auto& src, auto& dst)
+{
+  if (src.callback)
+  {
+    dst = [src](std::string_view errorText, const source_location& loc) {
+      src.callback(src.context, errorText.data(), errorText.size(), &loc);
+    };
+  }
+}
+}
+
+extern "C" {
+const char* libremidi_get_version(void)
+{
+  return LIBREMIDI_VERSION;
+}
+
+void libremidi_available_midi1_apis(void* ctx, void (*cb)(void* ctx, libremidi_api))
+{
+  if (!cb)
+    return;
+  libremidi::midi1::for_all_backends([=](auto b) { cb(ctx, b.API); });
+}
+
+void libremidi_available_midi2_apis(void* ctx, void (*cb)(void* ctx, libremidi_api))
+{
+  if (!cb)
+    return;
+  libremidi::midi2::for_all_backends([=](auto b) { cb(ctx, b.API); });
+}
+
+const char* libremidi_api_identifier(libremidi_api api)
+{
+  return libremidi::get_api_name(api).data();
+}
+
+const char* libremidi_api_display_name(libremidi_api api)
+{
+  return libremidi::get_api_display_name(api).data();
+}
+
+libremidi_api libremidi_get_compiled_api_by_identifier(const char* name)
+{
+  libremidi_api ret = libremidi_api::UNSPECIFIED;
+  libremidi::midi_any::for_all_backends([&](auto& b) {
+    if (name == b.name)
+      ret = b.API;
+  });
+  return ret;
+}
+
+int libremidi_midi_api_configuration_init(libremidi_api_configuration* conf)
+{
+  memset(conf, 0, sizeof(*conf));
+  return 0;
+}
+
+int libremidi_midi_observer_configuration_init(libremidi_observer_configuration* conf)
+{
+  memset(conf, 0, sizeof(*conf));
+  return 0;
+}
+
+int libremidi_midi_configuration_init(libremidi_midi_configuration* conf)
+{
+  memset(conf, 0, sizeof(*conf));
+  return 0;
+}
+
+int libremidi_midi_in_port_clone(const libremidi_midi_in_port* port, libremidi_midi_in_port** dst)
+{
+  if (!port || !dst)
+    return -EINVAL;
+
+  auto copied = new libremidi::input_port{*reinterpret_cast<const libremidi::input_port*>(port)};
+  *dst = reinterpret_cast<libremidi_midi_in_port*>(copied);
+  return 0;
+}
+
+int libremidi_midi_in_port_free(libremidi_midi_in_port* port)
+{
+  delete reinterpret_cast<libremidi::input_port*>(port);
+  return 0;
+}
+
+int libremidi_midi_in_port_name(const libremidi_midi_in_port* port, const char** name, size_t* len)
+{
+  if (!port || !name || !len)
+    return -EINVAL;
+
+  auto& p = *reinterpret_cast<const libremidi::input_port*>(port);
+  *name = p.port_name.data();
+  *len = p.port_name.size();
+  return 0;
+}
+
+int libremidi_midi_out_port_clone(
+    const libremidi_midi_out_port* port, libremidi_midi_out_port** dst)
+{
+  if (!port || !dst)
+    return -EINVAL;
+
+  auto copied = new libremidi::output_port{*reinterpret_cast<const libremidi::output_port*>(port)};
+  *dst = reinterpret_cast<libremidi_midi_out_port*>(copied);
+  return 0;
+}
+
+int libremidi_midi_out_port_free(libremidi_midi_out_port* port)
+{
+  delete reinterpret_cast<libremidi::output_port*>(port);
+  return 0;
+}
+
+int libremidi_midi_out_port_name(
+    const libremidi_midi_out_port* port, const char** name, size_t* len)
+{
+  if (!port || !name || !len)
+    return -EINVAL;
+
+  auto& p = *reinterpret_cast<const libremidi::output_port*>(port);
+  *name = p.port_name.data();
+  *len = p.port_name.size();
+  return 0;
+}
+
+int libremidi_midi_observer_new(
+    const libremidi_observer_configuration* c, libremidi_api_configuration* api,
+    libremidi_midi_observer_handle** out)
+{
+  if (!out || !c)
+    return -EINVAL;
+
+  libremidi::observer_configuration conf;
+  libremidi::assign_error_callback(c->on_error, conf.on_error);
+  libremidi::assign_error_callback(c->on_warning, conf.on_warning);
+
+  conf.track_hardware = c->track_hardware;
+  conf.track_virtual = c->track_virtual;
+  conf.track_any = c->track_any;
+  conf.notify_in_constructor = c->notify_in_constructor;
+
+  auto api_conf = libremidi::observer_configuration_for(static_cast<libremidi::API>(api->api));
+
+  if (c->input_added.callback)
+  {
+    conf.input_added = [cb = c->input_added](const auto& port) {
+      cb.callback(cb.context, reinterpret_cast<const libremidi_midi_in_port*>(&port));
+    };
+  }
+  if (c->input_removed.callback)
+  {
+    conf.input_removed = [cb = c->input_removed](const auto& port) {
+      cb.callback(cb.context, reinterpret_cast<const libremidi_midi_in_port*>(&port));
+    };
+  }
+  if (c->output_added.callback)
+  {
+    conf.output_added = [cb = c->output_added](const auto& port) {
+      cb.callback(cb.context, reinterpret_cast<const libremidi_midi_out_port*>(&port));
+    };
+  }
+  if (c->output_removed.callback)
+  {
+    conf.output_removed = [cb = c->output_removed](const auto& port) {
+      cb.callback(cb.context, reinterpret_cast<const libremidi_midi_out_port*>(&port));
+    };
+  }
+
+  try
+  {
+    auto ptr = new libremidi_midi_observer_handle{
+        libremidi::observer{std::move(conf), std::move(api_conf)}};
+    *out = ptr;
+    return 0;
+  }
+  catch (...)
+  {
+    return -EINVAL;
+  }
+}
+
+int libremidi_midi_observer_enumerate_input_ports(
+    libremidi_midi_observer_handle* ptr, void* context,
+    void (*cb)(void* ctx, const libremidi_midi_in_port*))
+{
+  if (!ptr || !cb)
+    return -EINVAL;
+
+  for (const auto& port : ptr->self.get_input_ports())
+  {
+    cb(context, reinterpret_cast<const libremidi_midi_in_port*>(&port));
+  }
+
+  return 0;
+}
+
+int libremidi_midi_observer_enumerate_output_ports(
+    libremidi_midi_observer_handle* ptr, void* context,
+    void (*cb)(void* ctx, const libremidi_midi_out_port*))
+{
+  if (!ptr || !cb)
+    return -EINVAL;
+
+  for (const auto& port : ptr->self.get_output_ports())
+  {
+    cb(context, reinterpret_cast<const libremidi_midi_out_port*>(&port));
+  }
+
+  return 0;
+}
+
+int libremidi_midi_observer_free(libremidi_midi_observer_handle* ptr)
+{
+  delete ptr;
+  return 0;
+}
+
+int libremidi_midi_in_new(
+    const libremidi_midi_configuration* c, const libremidi_api_configuration* api,
+    libremidi_midi_in_handle** out)
+{
+  if (!out || !c)
+    return -EINVAL;
+  if (!c->virtual_port && !c->in_port)
+    return -EINVAL;
+
+  *out = nullptr;
+
+  auto api_conf = libremidi::midi_in_configuration_for(static_cast<libremidi::API>(api->api));
+
+  // Create the MIDI object
+  switch (c->version)
+  {
+    case libremidi_midi_configuration::MIDI1:
+    case libremidi_midi_configuration::MIDI1_RAW: {
+      libremidi::input_configuration conf;
+      libremidi::assign_error_callback(c->on_error, conf.on_error);
+      libremidi::assign_error_callback(c->on_warning, conf.on_warning);
+      conf.ignore_sensing = c->ignore_sensing;
+      conf.ignore_sysex = c->ignore_sysex;
+      conf.ignore_timing = c->ignore_timing;
+      conf.timestamps = c->timestamps;
+
+      if (c->get_timestamp.callback)
+      {
+        conf.get_timestamp
+            = [cb = c->get_timestamp](int64_t msg) { return cb.callback(cb.context, msg); };
+      }
+
+      if (c->on_midi1_message.callback)
+        conf.on_message = [cb = c->on_midi1_message](const libremidi::message& msg) {
+          cb.callback(cb.context, msg.timestamp, msg.bytes.data(), msg.size());
+        };
+      else if (c->on_midi1_raw_data.callback)
+      {
+        conf.on_raw_data = [cb = c->on_midi1_raw_data](std::span<const uint8_t> msg, int64_t ts) {
+          cb.callback(cb.context, ts, msg.data(), msg.size());
+        };
+      }
+      else
+      {
+        return -EINVAL;
+      }
+
+      try
+      {
+        auto ptr = new libremidi_midi_in_handle{
+            libremidi::midi_in{std::move(conf), std::move(api_conf)}};
+        *out = ptr;
+      }
+      catch (...)
+      {
+        return -EINVAL;
+      }
+      break;
+    }
+    case libremidi_midi_configuration::MIDI2:
+    case libremidi_midi_configuration::MIDI2_RAW: {
+      libremidi::ump_input_configuration conf;
+      libremidi::assign_error_callback(c->on_error, conf.on_error);
+      libremidi::assign_error_callback(c->on_warning, conf.on_warning);
+      conf.ignore_sensing = c->ignore_sensing;
+      conf.ignore_sysex = c->ignore_sysex;
+      conf.ignore_timing = c->ignore_timing;
+      conf.timestamps = c->timestamps;
+
+      if (c->get_timestamp.callback)
+      {
+        conf.get_timestamp
+            = [cb = c->get_timestamp](int64_t msg) { return cb.callback(cb.context, msg); };
+      }
+
+      if (c->on_midi2_message.callback)
+        conf.on_message = [cb = c->on_midi2_message](const libremidi::ump& msg) {
+          cb.callback(cb.context, msg.timestamp, msg.data, msg.size());
+        };
+      else if (c->on_midi2_raw_data.callback)
+      {
+        conf.on_raw_data = [cb = c->on_midi2_raw_data](std::span<const uint32_t> msg, int64_t ts) {
+          cb.callback(cb.context, ts, msg.data(), msg.size());
+        };
+      }
+      else
+      {
+        return -EINVAL;
+      }
+
+      try
+      {
+        auto ptr = new libremidi_midi_in_handle{
+            libremidi::midi_in{std::move(conf), std::move(api_conf)}};
+        *out = ptr;
+      }
+      catch (...)
+      {
+        return -EINVAL;
+      }
+      break;
+    }
+    default:
+      return -EINVAL;
+  }
+
+  // Open the port
+  auto ptr = *out;
+  if (!ptr)
+    return -EINVAL;
+
+  if (c->virtual_port)
+  {
+    if (auto ret = ptr->self.open_virtual_port(c->port_name); ret != stdx::error{})
+    {
+      *out = nullptr;
+      delete ptr;
+      return -EIO;
+    }
+  }
+  else
+  {
+    auto port = reinterpret_cast<const libremidi::input_port*>(c->in_port);
+    if (auto ret = ptr->self.open_port(*port); ret != stdx::error{})
+    {
+      *out = nullptr;
+      delete ptr;
+      return -EIO;
+    }
+  }
+
+  return 0;
+}
+int libremidi_midi_in_is_connected(const libremidi_midi_in_handle* in)
+{
+  if (!in)
+    return -EINVAL;
+  return in->self.is_port_connected() ? 1 : 0;
+}
+
+libremidi_timestamp libremidi_midi_in_absolute_timestamp(libremidi_midi_in_handle* in)
+{
+  if (!in)
+    return -EINVAL;
+  return in->self.absolute_timestamp();
+}
+
+int libremidi_midi_in_free(libremidi_midi_in_handle* ptr)
+{
+  delete ptr;
+  return 0;
+}
+
+int libremidi_midi_out_new(
+    const libremidi_midi_configuration* c, const libremidi_api_configuration* api,
+    libremidi_midi_out_handle** out)
+{
+  if (!out || !c)
+    return -EINVAL;
+  if (!c->virtual_port && !c->out_port)
+    return -EINVAL;
+
+  *out = nullptr;
+
+  auto api_conf = libremidi::midi_out_configuration_for(static_cast<libremidi::API>(api->api));
+
+  // Create the MIDI object
+  libremidi::output_configuration conf;
+  libremidi::assign_error_callback(c->on_error, conf.on_error);
+  libremidi::assign_error_callback(c->on_warning, conf.on_warning);
+  conf.timestamps = c->timestamps;
+  try
+  {
+    auto ptr
+        = new libremidi_midi_out_handle{libremidi::midi_out{std::move(conf), std::move(api_conf)}};
+    *out = ptr;
+  }
+  catch (...)
+  {
+    return -EINVAL;
+  }
+
+  // Open the port
+  auto ptr = *out;
+  if (!ptr)
+    return -EINVAL;
+
+  if (c->virtual_port)
+  {
+    if (auto ret = ptr->self.open_virtual_port(c->port_name); ret != stdx::error{})
+    {
+      *out = nullptr;
+      delete ptr;
+      return -EIO;
+    }
+  }
+  else
+  {
+    auto port = reinterpret_cast<const libremidi::output_port*>(c->in_port);
+    if (auto ret = ptr->self.open_port(*port); ret != stdx::error{})
+    {
+      *out = nullptr;
+      delete ptr;
+      return -EIO;
+    }
+  }
+  return 0;
+}
+
+int libremidi_midi_out_is_connected(const libremidi_midi_out_handle* out)
+{
+  if (!out)
+    return -EINVAL;
+  return out->self.is_port_connected() ? 1 : 0;
+}
+
+int libremidi_midi_out_send_message(
+    libremidi_midi_out_handle* out, const libremidi_midi1_symbol* msg, size_t sz)
+{
+  if (!out || !msg || sz > std::numeric_limits<int32_t>::max())
+    return -EINVAL;
+
+  auto res = out->self.send_message(msg, sz);
+  return res != stdx::error{} ? -EIO : 0;
+}
+
+int libremidi_midi_out_send_ump(libremidi_midi_out_handle* out, const libremidi_midi2_symbol* msg, size_t sz)
+{
+  if (!out || !msg || sz > std::numeric_limits<int32_t>::max())
+    return -EINVAL;
+
+  auto res = out->self.send_ump(msg, sz);
+  return res != stdx::error{} ? -EIO : 0;
+}
+
+int libremidi_midi_out_schedule_message(
+    libremidi_midi_out_handle* out, int64_t ts, const libremidi_midi1_symbol* msg, size_t sz)
+{
+  if (!out || !msg || sz > std::numeric_limits<int32_t>::max())
+    return -EINVAL;
+
+  auto res = out->self.schedule_message(ts, msg, sz);
+  return res != stdx::error{} ? -EIO : 0;
+}
+
+int libremidi_midi_out_schedule_ump(
+    libremidi_midi_out_handle* out, int64_t ts, const libremidi_midi2_symbol* msg, size_t sz)
+{
+  if (!out || !msg || sz > std::numeric_limits<int32_t>::max())
+    return -EINVAL;
+
+  auto res = out->self.schedule_ump(ts, msg, sz);
+  return res != stdx::error{} ? -EIO : 0;
+}
+
+int libremidi_midi_out_free(libremidi_midi_out_handle* ptr)
+{
+  delete ptr;
+  return 0;
+}
+}
diff --git a/vendor/libremidi/libremidi-c.h b/vendor/libremidi/libremidi-c.h
new file mode 100644
--- /dev/null
+++ b/vendor/libremidi/libremidi-c.h
@@ -0,0 +1,275 @@
+#pragma once
+#include <libremidi/api-c.h>
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#if defined(LIBREMIDI_EXPORTS)
+  #if defined(_MSC_VER)
+    #define LIBREMIDI_EXPORT __declspec(dllexport)
+  #elif defined(__GNUC__) || defined(__clang__)
+    #define LIBREMIDI_EXPORT __attribute__((visibility("default")))
+  #endif
+#else
+  #define LIBREMIDI_EXPORT
+#endif
+
+#if __cplusplus
+extern "C" {
+#endif
+typedef unsigned char libremidi_midi1_symbol;
+typedef libremidi_midi1_symbol* libremidi_midi1_message;
+
+typedef uint32_t libremidi_midi2_symbol;
+typedef libremidi_midi2_symbol* libremidi_midi2_message;
+
+typedef int64_t libremidi_timestamp;
+
+typedef struct libremidi_midi_in_port libremidi_midi_in_port;
+typedef struct libremidi_midi_out_port libremidi_midi_out_port;
+typedef struct libremidi_midi_in_handle libremidi_midi_in_handle;
+typedef struct libremidi_midi_out_handle libremidi_midi_out_handle;
+typedef struct libremidi_midi_observer_handle libremidi_midi_observer_handle;
+
+typedef struct libremidi_api_configuration libremidi_api_configuration;
+
+enum libremidi_timestamp_mode
+{
+  NoTimestamp,
+
+  Relative,
+  Absolute,
+  SystemMonotonic,
+  AudioFrame,
+  Custom
+};
+
+typedef struct libremidi_api_configuration
+{
+  enum libremidi_api api;
+  enum
+  {
+    Observer,
+    Input,
+    Output
+  } configuration_type;
+
+  void* data;
+} libremidi_api_configuration;
+
+typedef struct libremidi_observer_configuration
+{
+  struct
+  {
+    void* context;
+    void (*callback)(void* ctx, const char* error, size_t error_len, const void* source_location);
+  } on_error;
+  struct
+  {
+    void* context;
+    void (*callback)(void* ctx, const char* error, size_t error_len, const void* source_location);
+  } on_warning;
+
+  struct
+  {
+    void* context;
+    void (*callback)(void* ctx, const libremidi_midi_in_port*);
+  } input_added;
+  struct
+  {
+    void* context;
+    void (*callback)(void* ctx, const libremidi_midi_in_port*);
+  } input_removed;
+  struct
+  {
+    void* context;
+    void (*callback)(void* ctx, const libremidi_midi_out_port*);
+  } output_added;
+  struct
+  {
+    void* context;
+    void (*callback)(void* ctx, const libremidi_midi_out_port*);
+  } output_removed;
+
+  bool track_hardware;
+  bool track_virtual;
+  bool track_any;
+  bool notify_in_constructor;
+} libremidi_observer_configuration;
+
+typedef struct libremidi_midi1_callback
+{
+  void* context;
+  void (*callback)(void* ctx, libremidi_timestamp, const libremidi_midi1_symbol*, size_t len);
+} libremidi_midi1_callback;
+
+typedef struct libremidi_midi2_callback
+{
+  void* context;
+  void (*callback)(void* ctx, libremidi_timestamp, const libremidi_midi2_symbol*, size_t len);
+} libremidi_midi2_callback;
+
+typedef struct libremidi_midi_configuration
+{
+  // Indicates the kind of callback requested, e.g. set MIDI1_RAW if you use the on_midi1_raw_data
+  enum
+  {
+    MIDI1 = (1 << 1),
+    MIDI1_RAW = (1 << 2),
+    MIDI2 = (1 << 3),
+    MIDI2_RAW = (1 << 4)
+  } version;
+
+  union
+  {
+    libremidi_midi_in_port* in_port;
+    libremidi_midi_out_port* out_port;
+  };
+
+  union
+  {
+    libremidi_midi1_callback on_midi1_message;
+    libremidi_midi1_callback on_midi1_raw_data;
+    libremidi_midi2_callback on_midi2_message;
+    libremidi_midi2_callback on_midi2_raw_data;
+  };
+
+  struct
+  {
+    void* context;
+    libremidi_timestamp (*callback)(void* ctx, libremidi_timestamp);
+  } get_timestamp;
+
+  struct
+  {
+    void* context;
+    void (*callback)(void* ctx, const char* error, size_t error_len, const void* source_location);
+  } on_error;
+  struct
+  {
+    void* context;
+    void (*callback)(void* ctx, const char* error, size_t error_len, const void* source_location);
+  } on_warning;
+
+  const char* port_name;
+  bool virtual_port;
+
+  bool ignore_sysex;
+  bool ignore_timing;
+  bool ignore_sensing;
+
+  enum libremidi_timestamp_mode timestamps;
+} libremidi_midi_configuration;
+
+/// API utilities
+LIBREMIDI_EXPORT
+const char* libremidi_get_version(void);
+
+LIBREMIDI_EXPORT
+void libremidi_midi1_available_apis(void* ctx, void (*)(void*, libremidi_api));
+LIBREMIDI_EXPORT
+void libremidi_midi2_available_apis(void* ctx, void (*)(void*, libremidi_api));
+LIBREMIDI_EXPORT
+const char* libremidi_api_identifier(libremidi_api);
+LIBREMIDI_EXPORT
+const char* libremidi_api_display_name(libremidi_api);
+LIBREMIDI_EXPORT
+libremidi_api libremidi_get_compiled_api_by_identifier(const char*);
+
+/// Create configurations
+LIBREMIDI_EXPORT
+int libremidi_midi_api_configuration_init(libremidi_api_configuration*);
+
+LIBREMIDI_EXPORT
+int libremidi_midi_observer_configuration_init(libremidi_observer_configuration*);
+
+LIBREMIDI_EXPORT
+int libremidi_midi_configuration_init(libremidi_midi_configuration*);
+
+/// Read information about port objects
+LIBREMIDI_EXPORT
+int libremidi_midi_in_port_clone(const libremidi_midi_in_port* port, libremidi_midi_in_port** dst);
+
+LIBREMIDI_EXPORT
+int libremidi_midi_in_port_free(libremidi_midi_in_port* port);
+
+LIBREMIDI_EXPORT
+int libremidi_midi_in_port_name(
+    const libremidi_midi_in_port* port, const char** name, size_t* len);
+
+LIBREMIDI_EXPORT
+int libremidi_midi_out_port_clone(
+    const libremidi_midi_out_port* port, libremidi_midi_out_port** dst);
+
+LIBREMIDI_EXPORT
+int libremidi_midi_out_port_free(libremidi_midi_out_port* port);
+
+LIBREMIDI_EXPORT
+int libremidi_midi_out_port_name(
+    const libremidi_midi_out_port* port, const char** name, size_t* len);
+
+/// Observer API
+LIBREMIDI_EXPORT
+int libremidi_midi_observer_new(
+    const libremidi_observer_configuration*, libremidi_api_configuration*,
+    libremidi_midi_observer_handle**);
+
+LIBREMIDI_EXPORT
+int libremidi_midi_observer_enumerate_input_ports(
+    libremidi_midi_observer_handle*, void* context,
+    void (*)(void* ctx, const libremidi_midi_in_port*));
+
+LIBREMIDI_EXPORT
+int libremidi_midi_observer_enumerate_output_ports(
+    libremidi_midi_observer_handle*, void* context,
+    void (*)(void* ctx, const libremidi_midi_out_port*));
+
+LIBREMIDI_EXPORT
+int libremidi_midi_observer_free(libremidi_midi_observer_handle*);
+
+/// MIDI input API (read MIDI messages)
+LIBREMIDI_EXPORT
+int libremidi_midi_in_new(
+    const libremidi_midi_configuration*, const libremidi_api_configuration*,
+    libremidi_midi_in_handle**);
+
+LIBREMIDI_EXPORT
+int libremidi_midi_in_is_connected(const libremidi_midi_in_handle*);
+
+LIBREMIDI_EXPORT
+libremidi_timestamp libremidi_midi_in_absolute_timestamp(libremidi_midi_in_handle*);
+
+LIBREMIDI_EXPORT
+int libremidi_midi_in_free(libremidi_midi_in_handle*);
+
+/// MIDI output API (send MIDI messages)
+LIBREMIDI_EXPORT
+int libremidi_midi_out_new(
+    const libremidi_midi_configuration*, const libremidi_api_configuration*,
+    libremidi_midi_out_handle**);
+
+LIBREMIDI_EXPORT
+int libremidi_midi_out_is_connected(const libremidi_midi_out_handle*);
+
+LIBREMIDI_EXPORT
+int libremidi_midi_out_send_message(
+    libremidi_midi_out_handle*, const libremidi_midi1_symbol*, size_t);
+
+LIBREMIDI_EXPORT
+int libremidi_midi_out_send_ump(libremidi_midi_out_handle*, const libremidi_midi2_symbol*, size_t);
+
+LIBREMIDI_EXPORT
+int libremidi_midi_out_schedule_message(
+    libremidi_midi_out_handle*, int64_t ts, const libremidi_midi1_symbol*, size_t);
+
+LIBREMIDI_EXPORT
+int libremidi_midi_out_schedule_ump(
+    libremidi_midi_out_handle*, int64_t ts, const libremidi_midi2_symbol*, size_t);
+
+LIBREMIDI_EXPORT
+int libremidi_midi_out_free(libremidi_midi_out_handle*);
+
+#if __cplusplus
+}
+#endif
diff --git a/vendor/libremidi/libremidi.cpp b/vendor/libremidi/libremidi.cpp
new file mode 100644
--- /dev/null
+++ b/vendor/libremidi/libremidi.cpp
@@ -0,0 +1,186 @@
+#if !defined(LIBREMIDI_HEADER_ONLY)
+  #include <libremidi/libremidi.hpp>
+#endif
+
+#include <libremidi/backends.hpp>
+#include <libremidi/detail/midi_api.hpp>
+
+#include <cmath>
+
+namespace libremidi
+{
+
+LIBREMIDI_INLINE
+std::string_view get_version() noexcept
+{
+  return LIBREMIDI_VERSION;
+}
+
+LIBREMIDI_INLINE std::string_view get_api_name(libremidi::API api)
+{
+  std::string_view ret;
+  midi_any::for_backend(api, [&](auto& b) { ret = b.name; });
+  return ret;
+}
+
+LIBREMIDI_INLINE std::string_view get_api_display_name(libremidi::API api)
+{
+  std::string_view ret;
+  midi_any::for_backend(api, [&](auto& b) { ret = b.display_name; });
+  return ret;
+}
+
+LIBREMIDI_INLINE libremidi::API get_compiled_api_by_name(std::string_view name)
+{
+  libremidi::API ret = libremidi::API::UNSPECIFIED;
+  midi_any::for_all_backends([&](auto& b) {
+    if (name == b.name)
+      ret = b.API;
+  });
+  return ret;
+}
+
+[[nodiscard]] LIBREMIDI_INLINE std::vector<libremidi::API> available_apis() noexcept
+{
+  std::vector<libremidi::API> apis;
+  midi1::for_all_backends([&](auto b) { apis.push_back(b.API); });
+  return apis;
+}
+
+[[nodiscard]] LIBREMIDI_INLINE std::vector<libremidi::API> available_ump_apis() noexcept
+{
+  std::vector<libremidi::API> apis;
+  midi2::for_all_backends([&](auto b) { apis.push_back(b.API); });
+  return apis;
+}
+LIBREMIDI_INLINE
+libremidi::API midi_api(const std::any& conf)
+{
+  libremidi::API ret = libremidi::API::UNSPECIFIED;
+  midi_any::for_all_backends([&]<typename T>(T) {
+    if (std::any_cast<typename T::midi_in_configuration>(&conf)
+        || std::any_cast<typename T::midi_out_configuration>(&conf)
+        || std::any_cast<typename T::midi_observer_configuration>(&conf))
+    {
+      ret = T::API;
+    }
+  });
+  return ret;
+}
+LIBREMIDI_INLINE
+std::any midi_in_configuration_for(libremidi::API api)
+{
+  std::any ret;
+  midi_any::for_backend(api, [&]<typename T>(T) {
+    using conf_type = typename T::midi_in_configuration;
+    ret = conf_type{};
+  });
+  return ret;
+}
+
+LIBREMIDI_INLINE
+std::any midi_out_configuration_for(libremidi::API api)
+{
+  std::any ret;
+  midi_any::for_backend(api, [&]<typename T>(T) {
+    using conf_type = typename T::midi_out_configuration;
+    ret = conf_type{};
+  });
+  return ret;
+}
+
+LIBREMIDI_INLINE
+std::any observer_configuration_for(libremidi::API api)
+{
+  std::any ret;
+  midi_any::for_backend(api, [&]<typename T>(T) {
+    using conf_type = typename T::midi_observer_configuration;
+    ret = conf_type{};
+  });
+  return ret;
+}
+
+LIBREMIDI_INLINE
+std::any midi_in_configuration_for(const libremidi::observer& obs)
+{
+  return midi_in_configuration_for(obs.get_current_api());
+}
+
+LIBREMIDI_INLINE
+std::any midi_out_configuration_for(const libremidi::observer& obs)
+{
+  // FIXME reuse context when meaningful, e.g. ALSA, JACK...
+  return midi_out_configuration_for(obs.get_current_api());
+}
+
+LIBREMIDI_INLINE
+std::optional<input_port> in_default_port(libremidi::API api) noexcept
+try
+{
+  libremidi::observer obs{{}, observer_configuration_for(api)};
+  if (auto ports = obs.get_input_ports(); !ports.empty())
+    return ports.front();
+  return std::nullopt;
+}
+catch (const std::exception& e)
+{
+  return std::nullopt;
+}
+
+LIBREMIDI_INLINE
+std::optional<output_port> out_default_port(libremidi::API api) noexcept
+try
+{
+  libremidi::observer obs{{}, observer_configuration_for(api)};
+  if (auto ports = obs.get_output_ports(); !ports.empty())
+    return ports.front();
+  return std::nullopt;
+}
+catch (const std::exception& e)
+{
+  return std::nullopt;
+}
+
+namespace midi1
+{
+LIBREMIDI_INLINE
+std::any in_default_configuration()
+{
+  return midi_in_configuration_for(default_api());
+}
+
+LIBREMIDI_INLINE
+std::any out_default_configuration()
+{
+  return midi_out_configuration_for(default_api());
+}
+
+LIBREMIDI_INLINE
+std::any observer_default_configuration()
+{
+  return observer_configuration_for(default_api());
+}
+}
+
+namespace midi2
+{
+LIBREMIDI_INLINE
+std::any in_default_configuration()
+{
+  return midi_in_configuration_for(default_api());
+}
+
+LIBREMIDI_INLINE
+std::any out_default_configuration()
+{
+  return midi_out_configuration_for(default_api());
+}
+
+LIBREMIDI_INLINE
+std::any observer_default_configuration()
+{
+  return observer_configuration_for(default_api());
+}
+}
+
+}
diff --git a/vendor/libremidi/midi_in.cpp b/vendor/libremidi/midi_in.cpp
new file mode 100644
--- /dev/null
+++ b/vendor/libremidi/midi_in.cpp
@@ -0,0 +1,373 @@
+#if !defined(LIBREMIDI_HEADER_ONLY)
+  #include <libremidi/libremidi.hpp>
+#endif
+
+#include <libremidi/backends.hpp>
+#include <libremidi/detail/midi_api.hpp>
+
+#include <cassert>
+
+namespace libremidi
+{
+
+static libremidi::ump_input_configuration
+convert_midi1_to_midi2_input_configuration(const input_configuration& base_conf) noexcept
+{
+  libremidi::ump_input_configuration c2;
+  c2.on_message = [cb = base_conf.on_message,
+                   converter = midi2_to_midi1{}](libremidi::ump&& msg) mutable -> void {
+    converter.convert(
+        msg.data, 1, msg.timestamp, [cb](const unsigned char* midi, std::size_t n, int64_t ts) {
+      cb(libremidi::message{{midi, midi + n}, ts});
+      return stdx::error{};
+    });
+  };
+  c2.get_timestamp = base_conf.get_timestamp;
+  c2.on_error = base_conf.on_error;
+  c2.on_warning = base_conf.on_warning;
+  c2.ignore_sysex = base_conf.ignore_sysex;
+  c2.ignore_timing = base_conf.ignore_timing;
+  c2.ignore_sensing = base_conf.ignore_sensing;
+  c2.timestamps = base_conf.timestamps;
+  return c2;
+}
+
+static libremidi::input_configuration
+convert_midi2_to_midi1_input_configuration(const ump_input_configuration& base_conf) noexcept
+{
+  libremidi::input_configuration c2;
+  c2.on_message = [cb = base_conf.on_message,
+                   converter = midi1_to_midi2{}](libremidi::message&& msg) mutable -> void {
+    converter.convert(
+        msg.bytes.data(), msg.bytes.size(), msg.timestamp,
+        [cb](const uint32_t* ump, std::size_t n, int64_t ts) {
+      if(n >= 4)
+      {
+        libremidi::ump u{ump[0],ump[1],ump[2],ump[3]};
+        u.timestamp = ts;
+        cb(std::move(u));
+      }
+      return stdx::error{};
+    });
+  };
+  c2.get_timestamp = base_conf.get_timestamp;
+  c2.on_error = base_conf.on_error;
+  c2.on_warning = base_conf.on_warning;
+  c2.ignore_sysex = base_conf.ignore_sysex;
+  c2.ignore_timing = base_conf.ignore_timing;
+  c2.ignore_sensing = base_conf.ignore_sensing;
+  c2.timestamps = base_conf.timestamps;
+  return c2;
+}
+
+static LIBREMIDI_INLINE std::unique_ptr<midi_in_api>
+make_midi_in(auto base_conf, std::any api_conf, auto backends)
+{
+  std::unique_ptr<midi_in_api> ptr;
+
+  assert(base_conf.on_message || base_conf.on_raw_data);
+
+  auto from_api = [&]<typename T>(T& /*backend*/) mutable {
+    if (auto conf = std::any_cast<typename T::midi_in_configuration>(&api_conf))
+    {
+      ptr = libremidi::make<typename T::midi_in>(std::move(base_conf), std::move(*conf));
+      return true;
+    }
+    return false;
+  };
+  std::apply([&](auto&&... b) { (from_api(b) || ...); }, backends);
+  return ptr;
+}
+
+/// MIDI 1 helpers
+static LIBREMIDI_INLINE std::unique_ptr<midi_in_api>
+make_midi1_in(const input_configuration& base_conf)
+{
+  for (const auto& api : available_apis())
+  {
+    try
+    {
+      auto impl
+          = make_midi_in(base_conf, midi_in_configuration_for(api), midi1::available_backends);
+      if (impl)
+        return impl;
+    }
+    catch (const std::exception& e)
+    {
+    }
+  }
+
+  // No MIDI 1 backend, try the MIDI 2 ones with a wrap:
+  {
+    auto c2 = convert_midi1_to_midi2_input_configuration(base_conf);
+    for (const auto& api : available_ump_apis())
+    {
+      try
+      {
+        auto impl = make_midi_in(c2, midi_in_configuration_for(api), midi2::available_backends);
+        if (impl)
+          return impl;
+      }
+      catch (const std::exception& e)
+      {
+      }
+    }
+  }
+
+  return std::make_unique<midi_in_dummy>(input_configuration{}, dummy_configuration{});
+}
+
+static LIBREMIDI_INLINE std::unique_ptr<midi_in_api>
+make_midi1_in(const input_configuration& base_conf, const std::any& api_conf, libremidi::API api)
+{
+  if (libremidi::is_midi1(api))
+  {
+    return make_midi_in(base_conf, api_conf, midi1::available_backends);
+  }
+  else if (libremidi::is_midi2(api))
+  {
+    auto c2 = convert_midi1_to_midi2_input_configuration(base_conf);
+    return make_midi_in(c2, api_conf, midi2::available_backends);
+  }
+  return {};
+}
+
+static LIBREMIDI_INLINE std::unique_ptr<midi_in_api>
+make_midi1_in(const input_configuration& base_conf, const std::any& api_conf)
+{
+  if (!api_conf.has_value())
+  {
+    return make_midi1_in(base_conf);
+  }
+  else if (auto api_p = std::any_cast<libremidi::API>(&api_conf))
+  {
+    if (*api_p == libremidi::API::UNSPECIFIED)
+    {
+      return make_midi1_in(base_conf);
+    }
+    else
+    {
+      return make_midi1_in(base_conf, midi_in_configuration_for(*api_p), *api_p);
+    }
+  }
+  else
+  {
+    if (auto api = libremidi::midi_api(api_conf); api == libremidi::API::UNSPECIFIED)
+      return {};
+    else
+      return make_midi1_in(base_conf, api_conf, libremidi::midi_api(api_conf));
+  }
+}
+
+/// MIDI 1 constructors
+LIBREMIDI_INLINE midi_in::midi_in(const input_configuration& base_conf) noexcept
+    : impl_{make_midi1_in(base_conf)}
+{
+}
+
+LIBREMIDI_INLINE
+midi_in::midi_in(input_configuration base_conf, std::any api_conf)
+    : impl_{make_midi1_in(base_conf, api_conf)}
+{
+  if (!impl_)
+  {
+    error_handler e;
+    e.libremidi_handle_error(base_conf, "Could not open midi in for the given api");
+    impl_ = std::make_unique<midi_in_dummy>(input_configuration{}, dummy_configuration{});
+  }
+}
+
+/// MIDI 2 helpers
+static LIBREMIDI_INLINE std::unique_ptr<midi_in_api>
+make_midi2_in(const ump_input_configuration& base_conf)
+{
+  for (const auto& api : available_ump_apis())
+  {
+    try
+    {
+      if (auto ret
+          = make_midi_in(base_conf, midi_in_configuration_for(api), midi2::available_backends))
+        return ret;
+    }
+    catch (const std::exception& e)
+    {
+    }
+  }
+
+  // No MIDI 2 backend, try the MIDI 1 ones with a wrap:
+  {
+    auto c2 = convert_midi2_to_midi1_input_configuration(base_conf);
+    for (const auto& api : available_apis())
+    {
+      try
+      {
+        if (auto ret = make_midi_in(c2, midi_in_configuration_for(api), midi1::available_backends))
+          return ret;
+      }
+      catch (const std::exception& e)
+      {
+      }
+    }
+  }
+
+  return {};
+}
+
+static LIBREMIDI_INLINE std::unique_ptr<midi_in_api> make_midi2_in(
+    const ump_input_configuration& base_conf, const std::any& api_conf, libremidi::API api)
+{
+  if (is_midi2(api))
+  {
+    return make_midi_in(base_conf, api_conf, midi2::available_backends);
+  }
+  else if (is_midi1(api))
+  {
+    auto c2 = convert_midi2_to_midi1_input_configuration(base_conf);
+    return make_midi_in(c2, api_conf, midi1::available_backends);
+  }
+
+  return {};
+}
+
+static LIBREMIDI_INLINE std::unique_ptr<midi_in_api>
+make_midi2_in(const ump_input_configuration& base_conf, const std::any& api_conf)
+{
+  if (!api_conf.has_value())
+  {
+    return make_midi2_in(base_conf);
+  }
+  else if (auto api_p = std::any_cast<libremidi::API>(&api_conf))
+  {
+    if (*api_p == libremidi::API::UNSPECIFIED)
+    {
+      return make_midi2_in(base_conf);
+    }
+    else
+    {
+      return make_midi2_in(base_conf, midi_in_configuration_for(*api_p), *api_p);
+    }
+  }
+  else
+  {
+    if (auto api = libremidi::midi_api(api_conf); api == libremidi::API::UNSPECIFIED)
+      return {};
+    else
+      return make_midi2_in(base_conf, api_conf, libremidi::midi_api(api_conf));
+  }
+}
+
+/// MIDI 2 constructors
+LIBREMIDI_INLINE midi_in::midi_in(ump_input_configuration base_conf) noexcept
+    : impl_{make_midi2_in(base_conf)}
+{
+}
+
+LIBREMIDI_INLINE
+midi_in::midi_in(ump_input_configuration base_conf, std::any api_conf)
+    : impl_{make_midi2_in(base_conf, api_conf)}
+{
+  if (!impl_)
+  {
+    error_handler e;
+    e.libremidi_handle_error(base_conf, "Could not open midi in for the given api");
+    impl_ = std::make_unique<midi_in_dummy>(input_configuration{}, dummy_configuration{});
+  }
+}
+
+LIBREMIDI_INLINE midi_in::~midi_in() = default;
+
+LIBREMIDI_INLINE midi_in::midi_in(midi_in&& other) noexcept
+    : impl_{std::move(other.impl_)}
+{
+  other.impl_
+      = std::make_unique<libremidi::midi_in_dummy>(input_configuration{}, dummy_configuration{});
+}
+
+LIBREMIDI_INLINE
+stdx::error midi_in::set_port_name(std::string_view portName)
+{
+  if(impl_->is_port_open())
+    return impl_->set_port_name(portName);
+
+  return std::errc::not_connected;
+}
+
+LIBREMIDI_INLINE midi_in& midi_in::operator=(midi_in&& other) noexcept
+{
+  this->impl_ = std::move(other.impl_);
+  other.impl_
+      = std::make_unique<libremidi::midi_in_dummy>(input_configuration{}, dummy_configuration{});
+  return *this;
+}
+
+LIBREMIDI_INLINE
+libremidi::API midi_in::get_current_api() const noexcept
+{
+  return impl_->get_current_api();
+}
+
+LIBREMIDI_INLINE
+stdx::error midi_in::open_port(const input_port& port, std::string_view portName)
+{
+  if (auto err = impl_->is_client_open(); err != stdx::error{})
+    return std::errc::not_connected;
+
+  if (impl_->is_port_open())
+    return std::errc::operation_not_supported;
+
+  auto ret = impl_->open_port(port, portName);
+  if (ret == stdx::error{})
+  {
+    impl_->connected_ = true;
+    impl_->port_open_ = true;
+  }
+  return ret;
+}
+
+LIBREMIDI_INLINE
+stdx::error midi_in::open_virtual_port(std::string_view portName)
+{
+  if (auto err = impl_->is_client_open(); err != stdx::error{})
+    return std::errc::not_connected;
+
+  if (impl_->is_port_open())
+    return std::errc::operation_not_supported;
+
+  auto ret = impl_->open_virtual_port(portName);
+  if (ret == stdx::error{})
+    impl_->port_open_ = true;
+  return ret;
+}
+
+LIBREMIDI_INLINE
+stdx::error midi_in::close_port()
+{
+  if (auto err = impl_->is_client_open(); err != stdx::error{})
+    return std::errc::not_connected;
+
+  auto ret = impl_->close_port();
+
+  impl_->connected_ = false;
+  impl_->port_open_ = false;
+
+  return ret;
+}
+
+LIBREMIDI_INLINE
+bool midi_in::is_port_open() const noexcept
+{
+  return impl_->is_port_open();
+}
+
+LIBREMIDI_INLINE
+bool midi_in::is_port_connected() const noexcept
+{
+  return impl_->is_port_connected();
+}
+
+LIBREMIDI_INLINE
+int64_t midi_in::absolute_timestamp() const noexcept
+{
+  return impl_->absolute_timestamp();
+}
+}
diff --git a/vendor/libremidi/midi_out.cpp b/vendor/libremidi/midi_out.cpp
new file mode 100644
--- /dev/null
+++ b/vendor/libremidi/midi_out.cpp
@@ -0,0 +1,326 @@
+#if !defined(LIBREMIDI_HEADER_ONLY)
+  #include <libremidi/libremidi.hpp>
+#endif
+
+#include <libremidi/backends.hpp>
+#include <libremidi/detail/midi_api.hpp>
+
+#include <array>
+#include <cassert>
+
+namespace libremidi
+{
+static LIBREMIDI_INLINE std::unique_ptr<midi_out_api>
+make_midi_out_impl(auto base_conf, std::any api_conf)
+{
+  std::unique_ptr<midi_out_api> ptr;
+  auto from_api = [&]<typename T>(T& /*backend*/) mutable {
+    if (auto conf = std::any_cast<typename T::midi_out_configuration>(&api_conf))
+    {
+      ptr = libremidi::make<typename T::midi_out>(std::move(base_conf), std::move(*conf));
+      return true;
+    }
+    return false;
+  };
+  std::apply([&](auto&&... b) { (from_api(b) || ...); }, midi1::available_backends);
+  std::apply([&](auto&&... b) { (from_api(b) || ...); }, midi2::available_backends);
+  return ptr;
+}
+
+static LIBREMIDI_INLINE std::unique_ptr<midi_out_api>
+make_midi_out(const output_configuration& base_conf)
+{
+  for (const auto& api : available_apis())
+  {
+    try
+    {
+      if (auto ret = make_midi_out_impl(base_conf, midi_out_configuration_for(api)))
+        return ret;
+    }
+    catch (const std::exception& e)
+    {
+    }
+  }
+
+  for (const auto& api : available_ump_apis())
+  {
+    try
+    {
+      if (auto ret = make_midi_out_impl(base_conf, midi_out_configuration_for(api)))
+        return ret;
+    }
+    catch (const std::exception& e)
+    {
+    }
+  }
+
+  return std::make_unique<midi_out_dummy>(output_configuration{}, dummy_configuration{});
+}
+
+static LIBREMIDI_INLINE std::unique_ptr<midi_out_api>
+make_midi_out(const output_configuration& base_conf, const std::any& api_conf)
+{
+  if (!api_conf.has_value())
+  {
+    return make_midi_out(base_conf);
+  }
+  else if (auto api_p = std::any_cast<libremidi::API>(&api_conf))
+  {
+    if (*api_p == libremidi::API::UNSPECIFIED)
+    {
+      return make_midi_out(base_conf);
+    }
+    else
+    {
+      return make_midi_out_impl(base_conf, midi_out_configuration_for(*api_p));
+    }
+  }
+  else
+  {
+    if (auto api = libremidi::midi_api(api_conf); api == libremidi::API::UNSPECIFIED)
+      return make_midi_out(base_conf);
+    else
+      return make_midi_out_impl(base_conf, api_conf);
+  }
+}
+
+LIBREMIDI_INLINE midi_out::midi_out(const output_configuration& base_conf) noexcept
+    : impl_{make_midi_out(base_conf)}
+{
+}
+
+LIBREMIDI_INLINE
+midi_out::midi_out(output_configuration base_conf, std::any api_conf)
+    : impl_{make_midi_out(base_conf, api_conf)}
+{
+  if (!impl_)
+  {
+    error_handler e;
+    e.libremidi_handle_error(base_conf, "Could not open midi out for the given api");
+    impl_ = std::make_unique<midi_out_dummy>(output_configuration{}, dummy_configuration{});
+  }
+}
+
+LIBREMIDI_INLINE midi_out::~midi_out() = default;
+
+LIBREMIDI_INLINE midi_out::midi_out(midi_out&& other) noexcept
+    : impl_{std::move(other.impl_)}
+{
+  other.impl_
+      = std::make_unique<libremidi::midi_out_dummy>(output_configuration{}, dummy_configuration{});
+}
+
+LIBREMIDI_INLINE midi_out& midi_out::operator=(midi_out&& other) noexcept
+{
+  this->impl_ = std::move(other.impl_);
+  other.impl_
+      = std::make_unique<libremidi::midi_out_dummy>(output_configuration{}, dummy_configuration{});
+  return *this;
+}
+
+LIBREMIDI_INLINE
+stdx::error midi_out::set_port_name(std::string_view portName) const
+{
+  if(impl_->is_port_open())
+    return impl_->set_port_name(portName);
+
+  return std::errc::not_connected;
+}
+
+LIBREMIDI_INLINE
+libremidi::API midi_out::get_current_api() const noexcept
+{
+  return impl_->get_current_api();
+}
+
+LIBREMIDI_INLINE
+stdx::error midi_out::open_port(const output_port& port, std::string_view portName) const
+{
+  if (auto err = impl_->is_client_open(); err != stdx::error{})
+    return std::errc::not_connected;
+
+  if (impl_->is_port_open())
+    return std::errc::operation_not_supported;
+
+  auto ret = impl_->open_port(port, portName);
+  if (ret == stdx::error{})
+  {
+    impl_->connected_ = true;
+    impl_->port_open_ = true;
+  }
+  return ret;
+}
+
+LIBREMIDI_INLINE
+stdx::error midi_out::open_virtual_port(std::string_view portName) const
+{
+  if (auto err = impl_->is_client_open(); err != stdx::error{})
+    return std::errc::not_connected;
+
+  if (impl_->is_port_open())
+    return std::errc::operation_not_supported;
+
+  auto ret = impl_->open_virtual_port(portName);
+  if (ret == stdx::error{})
+    impl_->port_open_ = true;
+  return ret;
+}
+
+LIBREMIDI_INLINE
+stdx::error midi_out::close_port() const
+{
+  if (auto err = impl_->is_client_open(); err != stdx::error{})
+    return std::errc::not_connected;
+
+  auto ret = impl_->close_port();
+  impl_->connected_ = false;
+  impl_->port_open_ = false;
+  return ret;
+}
+
+LIBREMIDI_INLINE
+bool midi_out::is_port_open() const noexcept
+{
+  return impl_->is_port_open();
+}
+
+LIBREMIDI_INLINE
+bool midi_out::is_port_connected() const noexcept
+{
+  return impl_->is_port_connected();
+}
+
+LIBREMIDI_INLINE
+stdx::error midi_out::send_message(const libremidi::message& message) const
+{
+  return send_message(message.bytes.data(), message.bytes.size());
+}
+
+LIBREMIDI_INLINE
+stdx::error midi_out::send_message(std::span<const unsigned char> message) const
+{
+  return send_message(message.data(), message.size());
+}
+
+LIBREMIDI_INLINE
+stdx::error midi_out::send_message(unsigned char b0) const
+{
+  return send_message(&b0, 1);
+}
+
+LIBREMIDI_INLINE
+stdx::error midi_out::send_message(unsigned char b0, unsigned char b1) const
+{
+  return send_message(std::to_array({b0, b1}));
+}
+
+LIBREMIDI_INLINE
+stdx::error midi_out::send_message(unsigned char b0, unsigned char b1, unsigned char b2) const
+{
+  return send_message(std::to_array({b0, b1, b2}));
+}
+
+LIBREMIDI_INLINE
+stdx::error midi_out::send_message(const unsigned char* message, size_t size) const
+{
+#if defined(LIBREMIDI_ASSERTIONS)
+  assert(size > 0);
+#endif
+
+  return impl_->send_message(message, size);
+}
+
+LIBREMIDI_INLINE
+int64_t midi_out::current_time()
+{
+  return impl_->current_time();
+}
+
+LIBREMIDI_INLINE
+stdx::error midi_out::schedule_message(int64_t ts, const unsigned char* message, size_t size) const
+{
+#if defined(LIBREMIDI_ASSERTIONS)
+  assert(size > 0);
+#endif
+
+  return impl_->schedule_message(ts, message, size);
+}
+
+LIBREMIDI_INLINE
+stdx::error midi_out::send_ump(const uint32_t* message, size_t size) const
+{
+#if defined(LIBREMIDI_ASSERTIONS)
+  assert(size > 0);
+  assert(size <= 4);
+#endif
+
+  return impl_->send_ump(message, size);
+}
+LIBREMIDI_INLINE
+stdx::error midi_out::send_ump(const libremidi::ump& message) const
+{
+  return send_ump(message.data, message.size());
+}
+
+LIBREMIDI_INLINE
+stdx::error midi_out::send_ump(std::span<const uint32_t> message) const
+{
+  return send_ump(message.data(), message.size());
+}
+
+LIBREMIDI_INLINE
+stdx::error midi_out::send_ump(uint32_t b0) const
+{
+  return send_ump(&b0, 1);
+}
+
+LIBREMIDI_INLINE
+stdx::error midi_out::send_ump(uint32_t b0, uint32_t b1) const
+{
+  return send_ump(std::to_array({b0, b1}));
+}
+
+LIBREMIDI_INLINE
+stdx::error midi_out::send_ump(uint32_t b0, uint32_t b1, uint32_t b2) const
+{
+  return send_ump(std::to_array({b0, b1, b2}));
+}
+
+LIBREMIDI_INLINE
+stdx::error midi_out::send_ump(uint32_t b0, uint32_t b1, uint32_t b2, uint32_t b3) const
+{
+  return send_ump(std::to_array({b0, b1, b2, b3}));
+}
+
+LIBREMIDI_INLINE
+stdx::error midi_out::send_ump(int32_t b0) const
+{
+  return send_ump(reinterpret_cast<uint32_t*>(&b0), 1);
+}
+
+LIBREMIDI_INLINE
+stdx::error midi_out::send_ump(int64_t b01) const
+{
+  auto ptr = reinterpret_cast<uint32_t*>(&b01);
+  uint32_t msg[2]{ptr[1], ptr[0]};
+  return send_ump(msg, 2);
+}
+
+LIBREMIDI_INLINE
+stdx::error midi_out::send_ump(uint64_t b01) const
+{
+  auto ptr = reinterpret_cast<uint32_t*>(&b01);
+  uint32_t msg[2]{ptr[1], ptr[0]};
+  return send_ump(msg, 2);
+}
+
+LIBREMIDI_INLINE
+stdx::error midi_out::schedule_ump(int64_t ts, const uint32_t* message, size_t size) const
+{
+#if defined(LIBREMIDI_ASSERTIONS)
+  assert(size > 0);
+#endif
+
+  return impl_->schedule_ump(ts, message, size);
+}
+}
diff --git a/vendor/libremidi/observer.cpp b/vendor/libremidi/observer.cpp
new file mode 100644
--- /dev/null
+++ b/vendor/libremidi/observer.cpp
@@ -0,0 +1,133 @@
+#if !defined(LIBREMIDI_HEADER_ONLY)
+  #include <libremidi/libremidi.hpp>
+#endif
+
+#include <libremidi/backends.hpp>
+#include <libremidi/detail/midi_api.hpp>
+
+namespace libremidi
+{
+
+static LIBREMIDI_INLINE std::unique_ptr<observer_api> make_observer(auto base_conf)
+{
+  for (const auto& api : available_apis())
+  {
+    try
+    {
+      if (auto impl_ = make_observer(base_conf, observer_configuration_for(api)))
+        return impl_;
+    }
+    catch (const std::exception& e)
+    {
+    }
+  }
+
+  for (const auto& api : available_ump_apis())
+  {
+    try
+    {
+      if (auto impl_ = make_observer(base_conf, observer_configuration_for(api)))
+        return impl_;
+    }
+    catch (const std::exception& e)
+    {
+    }
+  }
+
+  return std::make_unique<observer_dummy>(observer_configuration{}, dummy_configuration{});
+}
+
+LIBREMIDI_INLINE auto make_observer_impl(auto base_conf, std::any api_conf)
+{
+  std::unique_ptr<observer_api> ptr;
+  auto from_api = [&]<typename T>(T& /*backend*/) mutable {
+    if (auto conf = std::any_cast<typename T::midi_observer_configuration>(&api_conf))
+    {
+      ptr = libremidi::make<typename T::midi_observer>(std::move(base_conf), std::move(*conf));
+      return true;
+    }
+    return false;
+  };
+  std::apply([&](auto&&... b) { (from_api(b) || ...); }, midi1::available_backends);
+  std::apply([&](auto&&... b) { (from_api(b) || ...); }, midi2::available_backends);
+  return ptr;
+}
+
+LIBREMIDI_INLINE std::unique_ptr<observer_api> make_observer(auto base_conf, std::any api_conf)
+{
+  if (!api_conf.has_value())
+  {
+    return make_observer(base_conf);
+  }
+  else if (auto api_p = std::any_cast<libremidi::API>(&api_conf))
+  {
+    if (*api_p == libremidi::API::UNSPECIFIED)
+    {
+      return make_observer(base_conf);
+    }
+    else
+    {
+      return make_observer_impl(base_conf, observer_configuration_for(*api_p));
+    }
+  }
+  else
+  {
+    if (auto api = libremidi::midi_api(api_conf); api == libremidi::API::UNSPECIFIED)
+      return {};
+    else
+      return make_observer_impl(base_conf, api_conf);
+  }
+}
+
+LIBREMIDI_INLINE observer::observer(const observer_configuration& base_conf) noexcept
+    : impl_{make_observer(base_conf)}
+{
+}
+
+LIBREMIDI_INLINE observer::observer(observer_configuration base_conf, std::any api_conf)
+    : impl_{make_observer(base_conf, api_conf)}
+{
+  if (!impl_)
+  {
+    error_handler e;
+    e.libremidi_handle_error(base_conf, "Could not open observer for the given api");
+    impl_ = std::make_unique<observer_dummy>(observer_configuration{}, dummy_configuration{});
+  }
+}
+
+LIBREMIDI_INLINE observer::observer(observer&& other) noexcept
+    : impl_{std::move(other.impl_)}
+{
+  other.impl_ = std::make_unique<libremidi::observer_dummy>(
+      observer_configuration{}, dummy_configuration{});
+}
+
+LIBREMIDI_INLINE observer& observer::operator=(observer&& other) noexcept
+{
+  this->impl_ = std::move(other.impl_);
+  other.impl_ = std::make_unique<libremidi::observer_dummy>(
+      observer_configuration{}, dummy_configuration{});
+  return *this;
+}
+
+LIBREMIDI_INLINE
+observer::~observer() = default;
+
+LIBREMIDI_INLINE
+libremidi::API observer::get_current_api() const noexcept
+{
+  return impl_->get_current_api();
+}
+
+LIBREMIDI_INLINE
+std::vector<libremidi::input_port> observer::get_input_ports() const noexcept
+{
+  return impl_->get_input_ports();
+}
+
+LIBREMIDI_INLINE
+std::vector<libremidi::output_port> observer::get_output_ports() const noexcept
+{
+  return impl_->get_output_ports();
+}
+}
diff --git a/vendor/libremidi/reader.cpp b/vendor/libremidi/reader.cpp
new file mode 100644
--- /dev/null
+++ b/vendor/libremidi/reader.cpp
@@ -0,0 +1,752 @@
+/*
+Copyright (c) 2015, Dimitri Diakopoulos All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#if !defined(LIBREMIDI_HEADER_ONLY)
+  #include <libremidi/reader.hpp>
+#endif
+#include <libremidi/message.hpp>
+
+#include <algorithm>
+#include <iostream>
+
+// File Parsing Validation Todo:
+// ==============================
+// [] Bad file name
+// [] Bad header
+// [] Unknown header type
+// [] Bad header size
+// [] Bad type
+// [] Bad tmecode
+// [] Header too short
+// [] Track too short
+// [] Event too short
+// ==============================
+
+#if defined(__LIBREMIDI_DEBUG__)
+std::ostream& operator<<(std::ostream& s, const libremidi::message& m)
+{
+  s << "[ MIDI: ";
+  for (auto b : m)
+    s << (unsigned int)b << ' ';
+  s << "]\n";
+  return s;
+}
+#endif
+
+namespace libremidi
+{
+namespace util
+{
+struct no_validator
+{
+  static inline bool validate_track([[maybe_unused]] const midi_track& track) { return true; }
+};
+
+struct validator
+{
+  static inline bool validate_track(const midi_track& track)
+  {
+    if (track.empty())
+    {
+#if defined(__LIBREMIDI_DEBUG__)
+      std::cerr << "empty track" << std::endl;
+#endif
+      return false;
+    }
+
+    // Ensure that there is a unique EOT at the end of the track
+    auto it = std::find_if(track.begin(), track.end(), [](const libremidi::track_event& msg) {
+      static const auto eot = meta_events::end_of_track();
+      return msg.m.bytes == eot.bytes;
+    });
+
+    if (it == track.end())
+    {
+#if defined(__LIBREMIDI_DEBUG__)
+      std::cerr << "track has no END OF TRACK" << std::endl;
+#endif
+      return false;
+    }
+
+    if (&it->m != &track.back().m)
+    {
+#if defined(__LIBREMIDI_DEBUG__)
+      std::cerr << std::distance(it, track.end());
+      std::cerr << "track does not end with END OF TRACK" << std::endl;
+#endif
+      return false;
+    }
+
+    return true;
+  }
+};
+
+// Used when we know that we have enough space
+struct read_unchecked
+{
+  // Read a MIDI-style variable-length integer (big-endian value in groups of 7 bits,
+  // with top bit set to signify that another byte follows).
+  static inline void ensure_size(
+      [[maybe_unused]] const uint8_t* begin, [[maybe_unused]] const uint8_t* end,
+      [[maybe_unused]] int64_t needed)
+  {
+  }
+
+  static inline uint32_t
+  read_variable_length(uint8_t const*& data, [[maybe_unused]] uint8_t const* end)
+  {
+    uint32_t result = 0;
+    while (true)
+    {
+      uint8_t b = *data++;
+      if (b & 0x80)
+      {
+        result += (b & 0x7F);
+        result <<= 7;
+      }
+      else
+      {
+        return result + b; // b is the last byte
+      }
+    }
+  }
+
+  static inline void read_bytes(
+      midi_bytes& buffer, uint8_t const*& data, [[maybe_unused]] const uint8_t* end,
+      const std::size_t num)
+  {
+    buffer.reserve(buffer.size() + num);
+    for (std::size_t i = 0; i < num; ++i)
+      buffer.push_back(*data++);
+  }
+
+  static inline void read_bytes(
+      midi_bytes& buffer, uint8_t const*& data, [[maybe_unused]] const uint8_t* end, const int num)
+  {
+    read_bytes(buffer, data, end, static_cast<std::size_t>(num));
+  }
+
+  static inline uint16_t read_uint16_be(uint8_t const*& data, [[maybe_unused]] const uint8_t* end)
+  {
+    uint16_t result = *data++ << 8u;
+    result += *data++;
+    return result;
+  }
+
+  static inline uint32_t read_uint24_be(uint8_t const*& data, [[maybe_unused]] const uint8_t* end)
+  {
+    uint32_t result = *data++ << 16u;
+    result += static_cast<uint32_t>(*data++ << 8u);
+    result += *data++;
+    return result;
+  }
+
+  static inline uint32_t read_uint32_be(uint8_t const*& data, [[maybe_unused]] const uint8_t* end)
+  {
+    uint32_t result = *data++ << 24u;
+    result += static_cast<uint32_t>(*data++ << 16u);
+    result += static_cast<uint32_t>(*data++ << 8u);
+    result += *data++;
+    return result;
+  }
+};
+
+// Used when we do not know if we have enough bytes and have to check before reading
+struct read_checked
+{
+  // Read a MIDI-style variable-length integer (big-endian value in groups of 7 bits,
+  // with top bit set to signify that another byte follows).
+  static inline void
+  ensure_size(const uint8_t* begin, const uint8_t* end, const std::size_t needed)
+  {
+    if (const auto available = static_cast<std::size_t>(end - begin); available < needed)
+      throw std::runtime_error("MIDI reader: not enough data to process");
+  }
+
+  static inline std::size_t read_variable_length(uint8_t const*& data, uint8_t const* end)
+  {
+    std::size_t result = 0;
+    while (true)
+    {
+      ensure_size(data, end, 1);
+      uint8_t b = *data++;
+      if (b & 0x80)
+      {
+        result += (b & 0x7F);
+        result <<= 7;
+      }
+      else
+      {
+        return result + b; // b is the last byte
+      }
+    }
+  }
+
+  static inline void
+  read_bytes(midi_bytes& buffer, uint8_t const*& data, uint8_t const* end, const std::size_t num)
+  {
+    ensure_size(data, end, num);
+    read_unchecked::read_bytes(buffer, data, end, num);
+  }
+
+  static inline uint16_t read_uint16_be(uint8_t const*& data, uint8_t const* end)
+  {
+    ensure_size(data, end, 2);
+    return read_unchecked::read_uint16_be(data, end);
+  }
+
+  static inline uint32_t read_uint24_be(uint8_t const*& data, uint8_t const* end)
+  {
+    ensure_size(data, end, 3);
+    return read_unchecked::read_uint24_be(data, end);
+  }
+
+  static inline uint32_t read_uint32_be(uint8_t const*& data, uint8_t const* end)
+  {
+    ensure_size(data, end, 4);
+    return read_unchecked::read_uint32_be(data, end);
+  }
+};
+}
+
+#if defined(LIBREMIDI_UNCHECKED)
+using byte_reader = util::read_unchecked;
+#else
+using byte_reader = util::read_checked;
+#endif
+
+#if defined(LIBREMIDI_UNVALIDATED)
+using validator = util::no_validator;
+#else
+using validator = util::validator;
+#endif
+
+LIBREMIDI_INLINE
+track_event parse_event(
+    int tick, int track, const uint8_t*& dataStart, const uint8_t* dataEnd,
+    message_type lastEventTypeByte)
+{
+  byte_reader::ensure_size(dataStart, dataEnd, 1);
+  auto type = static_cast<message_type>(*dataStart++);
+
+  track_event event{tick, track, message{}};
+
+  if ((static_cast<uint8_t>(type) & 0xF0) == 0xF0)
+  {
+    // Meta event
+    if (static_cast<uint8_t>(type) == 0xFF)
+    {
+      byte_reader::ensure_size(dataStart, dataEnd, 1);
+      auto subtype = static_cast<meta_event_type>(*dataStart++);
+
+      event.m.bytes.reserve(3);
+      event.m.bytes.push_back(static_cast<uint8_t>(type));
+      event.m.bytes.push_back(static_cast<uint8_t>(subtype));
+
+      uint32_t length = 0;
+      // Here we read the meta-event length manually, as this way we can also put it into
+      // event.m.bytes
+      while (true)
+      {
+        byte_reader::ensure_size(dataStart, dataEnd, 1);
+        uint8_t b = *dataStart++;
+        event.m.bytes.push_back(b);
+        if (b & 0x80)
+        {
+          const uint8_t byte = (b & 0x7F);
+
+          length += byte;
+          length <<= 7;
+        }
+        else
+        {
+          length += b; // b is the last byte
+          break;
+        }
+      }
+
+      switch (subtype)
+      {
+        case meta_event_type::SEQUENCE_NUMBER: {
+          switch (length)
+          {
+            case 0:
+              return event;
+            case 2:
+              byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, 2);
+              return event;
+            default:
+              throw std::invalid_argument("Expected length for SEQUENCE_NUMBER event is 0 or 2");
+          }
+        }
+        case meta_event_type::TEXT:
+        case meta_event_type::COPYRIGHT:
+        case meta_event_type::TRACK_NAME:
+        case meta_event_type::INSTRUMENT:
+        case meta_event_type::LYRIC:
+        case meta_event_type::MARKER:
+        case meta_event_type::CUE:
+        case meta_event_type::PATCH_NAME:
+        case meta_event_type::DEVICE_NAME: {
+          byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length);
+          return event;
+        }
+
+        case meta_event_type::END_OF_TRACK: {
+          if (length != 0)
+            throw std::invalid_argument("Expected length for END_OF_TRACK event is 0");
+          return event;
+        }
+        case meta_event_type::TEMPO_CHANGE: {
+          if (length != 3)
+            throw std::invalid_argument("Expected length for TEMPO_CHANGE event is 3");
+          // event.m.bytes[3] = read_uint24_be(dataStart); // @dimitri TOFIX
+          byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length);
+          return event;
+        }
+        case meta_event_type::SMPTE_OFFSET: {
+          if (length != 5)
+            throw std::invalid_argument("Expected length for SMPTE_OFFSET event is 5");
+          byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length);
+          auto& b = event.m.bytes;
+
+          uint8_t format = (b[3] & 0b01100000) >> 5;
+          uint8_t h = (b[3] & 0b00011111);
+
+          if (format > 3)
+            throw std::invalid_argument("SMPTE_OFFSET has unknown format");
+
+          int max = 0;
+          switch (format)
+          {
+            case 0: // 24
+              max = 24;
+              break;
+            case 1: // 25
+              max = 25;
+              break;
+            case 2: // 29
+              max = 29;
+              break;
+            case 3: // 30
+              max = 30;
+              break;
+            default:
+              break;
+          }
+
+          if (h >= 24 || b[4] >= 60 || b[5] >= 60 || b[6] >= max || b[7] >= 100)
+            throw std::invalid_argument("SMPTE_OFFSET is out-of-23:59:59:xx:99 bounds");
+          return event;
+        }
+        case meta_event_type::TIME_SIGNATURE: {
+          if (length != 4)
+            throw std::invalid_argument("Expected length for TIME_SIGNATURE event is 4");
+          byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length);
+          return event;
+        }
+        case meta_event_type::KEY_SIGNATURE: {
+          if (length != 2)
+            throw std::invalid_argument("Expected length for KEY_SIGNATURE event is 2");
+          byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length);
+          auto k = static_cast<int8_t>(event.m[3]);
+          if (k < -7 || k > 7)
+            throw std::invalid_argument("Invalid KEY_SIGNATURE");
+          if (event.m[4] > 1)
+            throw std::invalid_argument("Invalid KEY_SIGNATURE");
+          return event;
+        }
+        case meta_event_type::PROPRIETARY: {
+          byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length);
+          return event;
+        }
+        case meta_event_type::CHANNEL_PREFIX: {
+          if (length != 1)
+            throw std::invalid_argument("Expected length for CHANNEL_PREFIX event is 1");
+          byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length);
+          return event;
+        }
+        case meta_event_type::MIDI_PORT: {
+          if (length != 1)
+            throw std::invalid_argument("Expected length for MIDI_PORT event is 1");
+          byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length);
+          return event;
+        }
+        case meta_event_type::UNKNOWN:
+        default: {
+          // Unknown events?
+          byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length);
+          return event;
+        }
+      }
+    }
+
+    else if (type == message_type::SYSTEM_EXCLUSIVE)
+    {
+      const auto length = byte_reader::read_variable_length(dataStart, dataEnd);
+      event.m.bytes = {static_cast<uint8_t>(type)};
+      byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length);
+      return event;
+    }
+
+    else if (type == message_type::EOX)
+    {
+      const auto length = byte_reader::read_variable_length(dataStart, dataEnd);
+      byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length);
+      return event;
+    }
+    else
+    {
+      throw std::runtime_error("Unrecognised MIDI event type byte");
+    }
+  }
+
+  // Channel events
+  else
+  {
+    event.m.bytes.clear();
+
+    // Running status...
+    if ((static_cast<uint8_t>(type) & 0x80) == 0)
+    {
+      // Reuse lastEventTypeByte as the event type.
+      // eventTypeByte is actually the first parameter
+      event.m.bytes.push_back(static_cast<uint8_t>(lastEventTypeByte));
+      event.m.bytes.push_back(static_cast<uint8_t>(type));
+      type = lastEventTypeByte;
+    }
+    else
+    {
+      event.m.bytes.push_back(static_cast<uint8_t>(type));
+
+      byte_reader::ensure_size(dataStart, dataEnd, 1);
+      event.m.bytes.push_back(static_cast<uint8_t>(*dataStart++));
+      lastEventTypeByte = type;
+    }
+
+    static constexpr auto validate = [](midi_bytes& b) {
+      if (b[1] < 128 && b[2] < 128)
+        return true;
+      throw std::invalid_argument("MIDI message has arguments > 127");
+    };
+
+    switch (static_cast<message_type>(static_cast<uint8_t>(type) & 0xF0))
+    {
+      case message_type::NOTE_OFF:
+      case message_type::NOTE_ON:
+      case message_type::POLY_PRESSURE:
+      case message_type::CONTROL_CHANGE:
+        byte_reader::ensure_size(dataStart, dataEnd, 1);
+        event.m.bytes.push_back(*dataStart++);
+        validate(event.m.bytes);
+        return event;
+      case message_type::PROGRAM_CHANGE:
+        if (event.m.bytes[1] >= 128)
+          throw std::invalid_argument("MIDI PC has arguments > 127");
+        return event;
+      case message_type::AFTERTOUCH:
+        if (event.m.bytes[1] >= 128)
+          throw std::invalid_argument("MIDI Atertouch has arguments > 127");
+        return event;
+      case message_type::PITCH_BEND:
+        byte_reader::ensure_size(dataStart, dataEnd, 1);
+        event.m.bytes.push_back(*dataStart++);
+        validate(event.m.bytes);
+        return event;
+
+      case message_type::TIME_CODE:
+        throw std::runtime_error("Unsupported MIDI event type TIME_CODE");
+      case message_type::SONG_POS_POINTER:
+        throw std::runtime_error("Unsupported MIDI event type SONG_POS_POINTER");
+      case message_type::SONG_SELECT:
+        throw std::runtime_error("Unsupported MIDI event type SONG_SELECT");
+      case message_type::RESERVED1:
+        throw std::runtime_error("Unsupported MIDI event type RESERVED1");
+      case message_type::RESERVED2:
+        throw std::runtime_error("Unsupported MIDI event type RESERVED2");
+      case message_type::TUNE_REQUEST:
+        throw std::runtime_error("Unsupported MIDI event type TUNE_REQUEST");
+      case message_type::EOX:
+        throw std::runtime_error("Unsupported MIDI event type EOX");
+        // System Realtime Messages :
+      case message_type::TIME_CLOCK:
+        throw std::runtime_error("Unsupported MIDI event type TIME_CLOCK");
+      case message_type::RESERVED3:
+        throw std::runtime_error("Unsupported MIDI event type RESERVED3");
+      case message_type::START:
+        throw std::runtime_error("Unsupported MIDI event type START");
+      case message_type::CONTINUE:
+        throw std::runtime_error("Unsupported MIDI event type CONTINUE");
+      case message_type::STOP:
+        throw std::runtime_error("Unsupported MIDI event type STOP");
+      case message_type::RESERVED4:
+        throw std::runtime_error("Unsupported MIDI event type RESERVED4");
+      case message_type::ACTIVE_SENSING:
+        throw std::runtime_error("Unsupported MIDI event type ACTIVE_SENSING");
+      case message_type::SYSTEM_RESET:
+        throw std::runtime_error("Unsupported MIDI event type SYSTEM_RESET");
+      case message_type::INVALID:
+        throw std::runtime_error("Unsupported MIDI event type INVALID");
+      case message_type::SYSTEM_EXCLUSIVE:
+        throw std::runtime_error("Unsupported MIDI event type SYSTEM_EXCLUSIVE");
+      default:
+        throw std::runtime_error("Unsupported MIDI event type");
+    }
+  }
+}
+
+LIBREMIDI_INLINE
+reader::reader(bool useAbsolute)
+    : ticksPerBeat(480)
+    , startingTempo(120)
+    , useAbsoluteTicks(useAbsolute)
+{
+}
+
+LIBREMIDI_INLINE
+reader::~reader() { }
+
+constexpr int str_to_headerid(const char* str)
+{
+  return str[0] << 24 | str[1] << 16 | str[2] << 8 | str[3];
+}
+
+LIBREMIDI_INLINE
+auto reader::parse(const uint8_t* dataPtr, std::size_t size) noexcept -> parse_result
+try
+{
+  using namespace libremidi::util;
+
+  tracks.clear();
+
+  if (size == 0)
+  {
+#if defined(__LIBREMIDI_DEBUG__)
+    std::cerr << "empty buffer passed to parse." << std::endl;
+#endif
+    return parse_result::invalid;
+  }
+
+  const uint8_t* const dataEnd = dataPtr + size;
+
+  uint32_t headerId = read_checked::read_uint32_be(dataPtr, dataEnd);
+  uint32_t headerLength = read_checked::read_uint32_be(dataPtr, dataEnd);
+
+  if (static_cast<int>(headerId) != str_to_headerid("MThd") || headerLength != 6)
+  {
+#if defined(__LIBREMIDI_DEBUG__)
+    std::cerr << "couldn't parse header" << std::endl;
+#endif
+    return parse_result::invalid;
+  }
+
+  format = read_checked::read_uint16_be(
+      dataPtr, dataEnd); //@tofix format type -> save for later eventually
+  if (format > 2)
+  {
+#if defined(__LIBREMIDI_DEBUG__)
+    std::cerr << "unknown format" << std::endl;
+#endif
+    return parse_result::invalid;
+  }
+  int trackCount = read_checked::read_uint16_be(dataPtr, dataEnd);
+  uint16_t timeDivision = read_checked::read_uint16_be(dataPtr, dataEnd);
+
+  // CBB: deal with the SMPTE style time coding
+  // timeDivision is described here http://www.sonicspot.com/guide/midifiles.html
+  if (timeDivision & 0x8000)
+  {
+#if defined(__LIBREMIDI_DEBUG__)
+    std::cerr << "found SMPTE time frames (unsupported)" << std::endl;
+    int fps = (timeDivision >> 16) & 0x7f;
+    if (fps != -30 && fps != -29 && fps != -25 && fps != -24)
+      return parse_result::invalid;
+    int ticksPerFrame = timeDivision & 0xff;
+#endif
+    // given beats per second, timeDivision should be derivable.
+    return parse_result::invalid;
+  }
+
+  startingTempo = 120.0f;             // midi default
+  ticksPerBeat = float(timeDivision); // ticks per beat (a beat is defined as a quarter note)
+
+  parse_result result = parse_result::validated;
+
+  for (int i = 0; i < trackCount; ++i)
+  {
+    midi_track track;
+
+    headerId = read_checked::read_uint32_be(dataPtr, dataEnd);
+    headerLength = read_checked::read_uint32_be(dataPtr, dataEnd);
+
+    if (headerId != str_to_headerid("MTrk"))
+    {
+#if defined(__LIBREMIDI_DEBUG__)
+      std::cerr << "couldn't find track header" << std::endl;
+#endif
+      return parse_result::incomplete;
+    }
+
+    int64_t available = dataEnd - dataPtr;
+    if (available < headerLength)
+    {
+#if defined(__LIBREMIDI_DEBUG__)
+      std::cerr << "not enough data available" << std::endl;
+#endif
+      return parse_result::incomplete;
+    }
+
+    track.reserve(headerLength / 3);
+
+    const uint8_t* const trackEnd = dataPtr + headerLength;
+
+    auto runningEvent = message_type::INVALID;
+
+    std::size_t tickCount = 0;
+
+    while (dataPtr < trackEnd)
+    {
+      const auto tick = read_checked::read_variable_length(dataPtr, trackEnd);
+      if (useAbsoluteTicks)
+      {
+        tickCount += tick;
+      }
+      else
+      {
+        tickCount = tick;
+      }
+
+      try
+      {
+        track_event ev
+            = parse_event(static_cast<int>(tickCount), i, dataPtr, trackEnd, runningEvent);
+        if (!ev.m.empty())
+        {
+          if (!ev.m.is_meta_event())
+          {
+            runningEvent = static_cast<message_type>(ev.m.bytes[0]);
+          }
+        }
+        else
+        {
+#if defined(__LIBREMIDI_DEBUG__)
+          std::cerr << "could not read event" << std::endl;
+#endif
+          dataPtr = trackEnd;
+          result = parse_result::incomplete;
+          continue;
+        }
+
+        track.push_back(std::move(ev));
+      }
+      catch (const std::exception& e)
+      {
+#if defined(__LIBREMIDI_DEBUG__)
+        std::cerr << "" << e.what() << std::endl;
+#endif
+        dataPtr = trackEnd;
+        result = parse_result::incomplete;
+        continue;
+      }
+    }
+
+    if (result == parse_result::validated)
+    {
+      if (!validator::validate_track(track))
+      {
+        result = parse_result::complete;
+      }
+    }
+    tracks.push_back(std::move(track));
+  }
+
+  if (result == parse_result::validated)
+  {
+    if (dataPtr != dataEnd)
+    {
+#if defined(__LIBREMIDI_DEBUG__)
+      std::cerr << "midifile has junk at end: " << std::intptr_t(dataEnd - dataPtr) << std::endl;
+#endif
+      result = parse_result::complete;
+    }
+  }
+  return result;
+}
+catch (const std::exception& e)
+{
+#if defined(__LIBREMIDI_DEBUG__)
+  std::cerr << "" << e.what() << std::endl;
+#endif
+  return parse_result::invalid;
+}
+
+// In ticks
+LIBREMIDI_INLINE
+double reader::get_end_time() const noexcept
+{
+  if (useAbsoluteTicks)
+  {
+    double totalLength = 0.;
+    for (const auto& t : tracks)
+    {
+      if (!t.empty())
+      {
+        const auto& last_event = t.back();
+        if (last_event.tick > totalLength)
+          totalLength = last_event.tick;
+      }
+    }
+    return totalLength;
+  }
+  else
+  {
+    double totalLength = 0.;
+    for (const auto& t : tracks)
+    {
+      double trackLength = 0.;
+      for (const auto& e : t)
+        trackLength += e.tick;
+
+      if (trackLength > totalLength)
+        totalLength = trackLength;
+    }
+    return totalLength;
+  }
+}
+
+LIBREMIDI_INLINE
+auto reader::parse(const std::vector<uint8_t>& buffer) noexcept -> parse_result
+{
+  return parse(buffer.data(), buffer.size());
+}
+
+#if defined(LIBREMIDI_HAS_SPAN)
+LIBREMIDI_INLINE
+auto reader::parse(std::span<uint8_t> buffer) noexcept -> parse_result
+{
+  return parse(buffer.data(), buffer.size());
+}
+#endif
+}
diff --git a/vendor/libremidi/writer.cpp b/vendor/libremidi/writer.cpp
new file mode 100644
--- /dev/null
+++ b/vendor/libremidi/writer.cpp
@@ -0,0 +1,213 @@
+/*
+Copyright (c) 2015, Dimitri Diakopoulos All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#if !defined(LIBREMIDI_HEADER_ONLY)
+  #include <libremidi/writer.hpp>
+#endif
+#include <algorithm>
+#include <bit>
+#include <ostream>
+#include <string>
+
+#if !defined(__cpp_lib_bit_cast)
+namespace std
+{
+template <typename R, typename T>
+[[nodiscard]]
+constexpr R bit_cast(const T& v) noexcept
+{
+  union
+  {
+    R res;
+    T init;
+  } u{.init = v};
+  return u.res;
+}
+}
+#endif
+namespace libremidi
+{
+namespace util
+{
+template <typename T>
+static LIBREMIDI_INLINE std::ostream& write_be(std::ostream& out, T value)
+{
+  static_assert(
+      std::endian::native == std::endian::big || std::endian::native == std::endian::little);
+  if constexpr (std::endian::native == std::endian::big)
+  {
+    out << value;
+  }
+  else
+  {
+    static constexpr auto N = sizeof(value);
+    struct storage
+    {
+      uint8_t bytes[N];
+    };
+    auto data = std::bit_cast<storage>(value);
+    std::reverse(data.bytes, data.bytes + N);
+    out.write(reinterpret_cast<const char*>(data.bytes), static_cast<std::streamsize>(N));
+  }
+  return out;
+}
+
+// Write a number to the midifile
+// as a variable length value which segments a file into 7-bit
+// values.  Maximum size of aValue is 0x7fffffff
+static LIBREMIDI_INLINE void write_variable_length(uint32_t aValue, std::vector<uint8_t>& outdata)
+{
+  uint8_t bytes[5] = {0};
+
+  bytes[0] = static_cast<uint8_t>((aValue >> 28) & 0x7F); // most significant 5 bits
+  bytes[1] = static_cast<uint8_t>((aValue >> 21) & 0x7F); // next largest 7 bits
+  bytes[2] = static_cast<uint8_t>((aValue >> 14) & 0x7F);
+  bytes[3] = static_cast<uint8_t>((aValue >> 7) & 0x7F);
+  bytes[4] = static_cast<uint8_t>((aValue) & 0x7F); // least significant 7 bits
+
+  int start = 0;
+  while (start < 5 && bytes[start] == 0)
+    start++;
+
+  for (int i = start; i < 4; i++)
+  {
+    bytes[i] = bytes[i] | 0x80;
+    outdata.push_back(bytes[i]);
+  }
+  outdata.push_back(bytes[4]);
+}
+
+static LIBREMIDI_INLINE void
+add_event_track_count_check(std::vector<midi_track>& tracks, int track)
+{
+  if (track < 0)
+    throw std::out_of_range("Refusing to add an event to track " + std::to_string(track) + ".");
+  if (track > 65535)
+    throw std::out_of_range(
+        "Refusing to add an event to track " + std::to_string(track)
+        + " ; change add_event_track_count_check in libremidi writer.cpp to increase the limit.");
+
+  while (tracks.size() < static_cast<std::size_t>(track + 1)) // NOLINT(*-misplaced-widening-cast)
+    tracks.emplace_back();
+}
+}
+
+LIBREMIDI_INLINE
+void writer::add_event(const int tick, const int track, const message& m)
+{
+  util::add_event_track_count_check(tracks, track);
+
+  tracks[static_cast<std::size_t>(track)].push_back({tick, track, m});
+}
+
+LIBREMIDI_INLINE
+void writer::add_event(int track, const track_event& m)
+{
+  util::add_event_track_count_check(tracks, track);
+
+  tracks[static_cast<std::size_t>(track)].push_back(m);
+}
+
+LIBREMIDI_INLINE
+void writer::add_track()
+{
+  util::add_event_track_count_check(tracks, static_cast<int>(tracks.size() + 1));
+}
+
+LIBREMIDI_INLINE
+void writer::write(std::ostream& out) const
+{
+  // MIDI File Header
+  out.write("MThd", 4);
+  util::write_be<uint32_t>(out, 6);
+  util::write_be<uint16_t>(out, (tracks.size() == 1) ? 0 : 1);
+  util::write_be<uint16_t>(out, static_cast<uint16_t>(tracks.size()));
+  util::write_be<uint16_t>(out, ticksPerQuarterNote);
+
+  std::vector<uint8_t> trackRawData;
+  for (const auto& event_list : tracks)
+  {
+    trackRawData.clear();
+    // Rough estimation of the memory to allocate
+    trackRawData.reserve(event_list.size() * 3);
+
+    for (const auto& event : event_list)
+    {
+      const auto& msg = event.m;
+      if (msg.empty())
+        continue;
+
+      // Suppress end-of-track meta messages (one will be added
+      // automatically after all track data has been written).
+      if (msg.get_meta_event_type() == meta_event_type::END_OF_TRACK)
+        continue;
+
+      util::write_variable_length(static_cast<uint32_t>(event.tick), trackRawData);
+
+      if ((msg.get_message_type() == message_type::SYSTEM_EXCLUSIVE)
+          || (event.m.get_message_type() == message_type::EOX))
+      {
+        // 0xf0 == Complete sysex message (0xf0 is part of the raw MIDI).
+        // 0xf7 == Raw byte message (0xf7 not part of the raw MIDI).
+        // Print the first byte of the message (0xf0 or 0xf7), then
+        // print a VLV length for the rest of the bytes in the message.
+        // In other words, when creating a 0xf0 or 0xf7 MIDI message,
+        // do not insert the VLV byte length yourself, as this code will
+        // do it for you automatically.
+        trackRawData.emplace_back(msg.bytes[0]); // 0xf0 or 0xf7;
+
+        util::write_variable_length(static_cast<uint32_t>(msg.size()) - 1, trackRawData);
+
+        trackRawData.insert(
+            trackRawData.end(), msg.bytes.data() + 1, msg.bytes.data() + msg.bytes.size());
+      }
+      else
+      {
+        // Non-sysex type of message, so just output the bytes of the message:
+        trackRawData.insert(
+            trackRawData.end(), msg.bytes.data(), msg.bytes.data() + msg.bytes.size());
+      }
+    }
+
+    auto size = trackRawData.size();
+    const auto eot = meta_events::end_of_track();
+
+    if ((size < 3) || !((trackRawData[size - 3] == 0xFF) && (trackRawData[size - 2] == 0x2F)))
+    {
+      trackRawData.emplace_back(0x0); // tick
+      trackRawData.emplace_back(eot[0]);
+      trackRawData.emplace_back(eot[1]);
+      trackRawData.emplace_back(eot[2]);
+    }
+
+    // Write the track ID marker "MTrk":
+    out.write("MTrk", 4);
+    util::write_be<uint32_t>(out, static_cast<uint32_t>(trackRawData.size()));
+    out.write(
+        reinterpret_cast<char*>(trackRawData.data()),
+        static_cast<std::streamsize>(trackRawData.size()));
+  }
+}
+}
