diff --git a/cmake/libremidi.examples.cmake b/cmake/libremidi.examples.cmake index 4f1ba4bc..54e3d269 100644 --- a/cmake/libremidi.examples.cmake +++ b/cmake/libremidi.examples.cmake @@ -96,6 +96,10 @@ if(LIBREMIDI_HAS_WINMIDI) add_backend_example(midi2_out_winmidi) endif() +if(LIBREMIDI_HAS_KDMAPI) + add_backend_example(midi1_out_kdmapi) +endif() + if(LIBREMIDI_HAS_NETWORK) add_example(network) endif() diff --git a/cmake/libremidi.kdmapi.cmake b/cmake/libremidi.kdmapi.cmake new file mode 100644 index 00000000..385feaa8 --- /dev/null +++ b/cmake/libremidi.kdmapi.cmake @@ -0,0 +1,18 @@ +if(LIBREMIDI_NO_KDMAPI) + return() +endif() + +if(NOT WIN32) + return() +endif() + +if(${CMAKE_SYSTEM_NAME} MATCHES WindowsStore) + return() +endif() + +message(STATUS "libremidi: using KDMAPI (OmniMIDI)") +set(LIBREMIDI_HAS_KDMAPI 1) +target_compile_definitions(libremidi + ${_public} + LIBREMIDI_KDMAPI +) diff --git a/cmake/libremidi.sources.cmake b/cmake/libremidi.sources.cmake index 6695351a..26413844 100644 --- a/cmake/libremidi.sources.cmake +++ b/cmake/libremidi.sources.cmake @@ -69,6 +69,13 @@ target_sources(libremidi PRIVATE include/libremidi/backends/jack_ump/observer.hpp include/libremidi/backends/jack_ump.hpp + include/libremidi/backends/kdmapi/config.hpp + include/libremidi/backends/kdmapi/helpers.hpp + include/libremidi/backends/kdmapi/midi_in.hpp + include/libremidi/backends/kdmapi/midi_out.hpp + include/libremidi/backends/kdmapi/observer.hpp + include/libremidi/backends/kdmapi.hpp + include/libremidi/backends/keyboard/config.hpp include/libremidi/backends/keyboard/midi_in.hpp diff --git a/cmake/libremidi.win32.cmake b/cmake/libremidi.win32.cmake index b0820ede..deb2921e 100644 --- a/cmake/libremidi.win32.cmake +++ b/cmake/libremidi.win32.cmake @@ -3,6 +3,7 @@ if(NOT WIN32) endif() include(libremidi.winmm) +include(libremidi.kdmapi) if(NOT LIBREMIDI_NO_WINMIDI OR NOT LIBREMIDI_NO_WINUWP) include(libremidi.cppwinrt) diff --git a/examples/backends/midi1_out_kdmapi.cpp b/examples/backends/midi1_out_kdmapi.cpp new file mode 100644 index 00000000..40c72680 --- /dev/null +++ b/examples/backends/midi1_out_kdmapi.cpp @@ -0,0 +1,190 @@ +//*****************************************// +// midi1_out_kdmapi.cpp +// +// Example demonstrating KDMAPI (OmniMIDI) output. +// Shows high-throughput MIDI output with KDMAPI's +// low-latency direct data path. +// +//*****************************************// + +#include +#include + +#include +#include +#include +#include +#include +#include + +int main(int argc, char** argv) +{ + using namespace std::chrono_literals; + + // Check if KDMAPI is available + if (!libremidi::kdmapi::kdmapi_loader::instance().is_available()) + { + std::cerr << "KDMAPI is not available. Please install OmniMIDI.\n"; + std::cerr << "Download from: https://github.com/KeppySoftware/OmniMIDI\n"; + return 1; + } + + std::cout << "KDMAPI is available!\n"; + + // Get KDMAPI version + auto& loader = libremidi::kdmapi::kdmapi_loader::instance(); + if (loader.ReturnKDMAPIVer) + { + DWORD major{}, minor{}, build{}, rev{}; + if (loader.ReturnKDMAPIVer(&major, &minor, &build, &rev)) + { + std::cout << "KDMAPI Version: " << major << "." << minor << "." << build << " Rev. " << rev + << "\n"; + } + } + + // Parse arguments + bool no_buffer = false; + bool stress_test = false; + int note_count = 100; + + for (int i = 1; i < argc; ++i) + { + std::string arg = argv[i]; + if (arg == "-n" || arg == "--no-buffer") + { + no_buffer = true; + } + else if (arg == "-s" || arg == "--stress") + { + stress_test = true; + } + else if (arg == "-c" && i + 1 < argc) + { + note_count = std::stoi(argv[++i]); + } + } + + // Configure KDMAPI output + libremidi::kdmapi::output_configuration kdm_conf; + kdm_conf.use_no_buffer = no_buffer; + + std::cout << "Mode: " << (no_buffer ? "No-buffer (lowest latency)" : "Buffered") << "\n"; + + // Create MIDI output with KDMAPI configuration + libremidi::midi_out midiout{{}, kdm_conf}; + + // Get available ports + libremidi::observer obs{{}, libremidi::kdmapi::observer_configuration{}}; + auto ports = obs.get_output_ports(); + + if (ports.empty()) + { + std::cerr << "No KDMAPI output ports found!\n"; + return 1; + } + + std::cout << "Output port: " << ports[0].display_name << "\n\n"; + + // Open the port + auto err = midiout.open_port(ports[0]); + if (err != stdx::error{}) + { + std::cerr << "Failed to open KDMAPI port!\n"; + return 1; + } + + if (stress_test) + { + // Stress test: blast as many notes as possible + std::cout << "Running stress test with " << note_count << " notes...\n"; + + std::random_device rd; + std::mt19937 gen(rd()); + for(int i = 0; i < 1000; i++) + { + std::uniform_int_distribution<> note_dist(36, 96); + std::uniform_int_distribution<> vel_dist(60, 127); + std::uniform_int_distribution<> ch_dist(0, 15); + + auto start = std::chrono::steady_clock::now(); + + for (int i = 0; i < note_count; ++i) + { + int note = note_dist(gen); + int vel = vel_dist(gen); + int ch = ch_dist(gen); + + // Note On + midiout.send_message(0x90 | ch, note, vel); + + // Brief delay to let some notes sound + if (i % 100 == 0) + { + std::this_thread::sleep_for(1ms); + } + } + + auto end = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration(end - start).count(); + + std::cout << "Sent " << note_count << " notes in " << std::fixed << std::setprecision(2) + << duration << " ms\n"; + std::cout << "Rate: " << std::fixed << std::setprecision(0) + << (note_count / duration * 1000.0) << " notes/sec\n"; + + // Wait a bit then silence all + std::this_thread::sleep_for(1ms); + + // All notes off on all channels + for (int ch = 0; ch < 16; ++ch) + { + midiout.send_message(0xB0 | ch, 123, 0); + midiout.send_message(0xB0 | ch, 120, 0); + } + } + } + else + { + // Simple demo: play a chord progression + std::cout << "Playing a simple chord progression...\n\n"; + + // Set piano on all channels + for (int ch = 0; ch < 16; ++ch) + { + midiout.send_message(0xC0 | ch, 0); // Program change: Piano + } + + // Define some chords (C major, F major, G major, C major) + std::vector> chords = { + {60, 64, 67}, // C major + {60, 65, 69}, // F major + {59, 62, 67}, // G major + {60, 64, 67, 72}, // C major (with octave) + }; + + for (const auto& chord : chords) + { + // Note On for all notes in chord + for (int note : chord) + { + midiout.send_message(0x90, note, 100); + std::this_thread::sleep_for(5ms); // Slight strum effect + } + + std::this_thread::sleep_for(500ms); + + // Note Off for all notes + for (int note : chord) + { + midiout.send_message(0x80, note, 0); + } + + std::this_thread::sleep_for(100ms); + } + + std::cout << "Done!\n"; + } + + return 0; +} diff --git a/include/libremidi/api-c.h b/include/libremidi/api-c.h index 69a13d0c..ab2a4ee8 100644 --- a/include/libremidi/api-c.h +++ b/include/libremidi/api-c.h @@ -23,6 +23,7 @@ typedef enum libremidi_api KEYBOARD, /*!< Computer keyboard input */ NETWORK, /*!< MIDI over IP */ ANDROID_AMIDI, /*!< Android AMidi API */ + KDMAPI, /*!< OmniMIDI KDMAPI (Windows) */ // MIDI 2.0 APIs ALSA_RAW_UMP = 0x1000, /*!< Raw ALSA API for MIDI 2.0 */ diff --git a/include/libremidi/backends.hpp b/include/libremidi/backends.hpp index 9b6fd2d1..a3a218f2 100644 --- a/include/libremidi/backends.hpp +++ b/include/libremidi/backends.hpp @@ -50,6 +50,10 @@ #include #endif +#if defined(LIBREMIDI_KDMAPI) + #include +#endif + #if defined(LIBREMIDI_WINUWP) #include #endif @@ -107,6 +111,10 @@ static constexpr auto available_backends = make_tl( , winmm_backend{} #endif +#if defined(LIBREMIDI_KDMAPI) + , + kdmapi_backend{} +#endif #if defined(LIBREMIDI_WINUWP) , winuwp_backend{} diff --git a/include/libremidi/backends/kdmapi.hpp b/include/libremidi/backends/kdmapi.hpp new file mode 100644 index 00000000..46846f92 --- /dev/null +++ b/include/libremidi/backends/kdmapi.hpp @@ -0,0 +1,43 @@ +#pragma once +#include +#include +#include + +#include + +//*********************************************************************// +// API: OmniMIDI KDMAPI (Keppy's Direct MIDI API) +//*********************************************************************// + +// OmniMIDI is a low-latency MIDI synthesizer driver for Windows. +// KDMAPI provides a direct interface to OmniMIDI, bypassing the +// Windows Multimedia API for lower latency. +// +// Note: KDMAPI is output-only. The midi_in class is a placeholder +// that always fails - use a different backend for MIDI input. +// +// https://github.com/KeppySoftware/OmniMIDI + +namespace libremidi +{ + +struct kdmapi_backend +{ + using midi_in = kdmapi::midi_in; + using midi_out = kdmapi::midi_out; + using midi_observer = kdmapi::observer; + using midi_in_configuration = kdmapi::input_configuration; + using midi_out_configuration = kdmapi::output_configuration; + using midi_observer_configuration = kdmapi::observer_configuration; + static const constexpr auto API = libremidi::API::KDMAPI; + static const constexpr std::string_view name = "kdmapi"; + static const constexpr std::string_view display_name = "OmniMIDI (KDMAPI)"; + + static inline bool available() noexcept + { + return kdmapi::kdmapi_loader::instance().is_available(); + } +}; + +} + diff --git a/include/libremidi/backends/kdmapi/config.hpp b/include/libremidi/backends/kdmapi/config.hpp new file mode 100644 index 00000000..df55793c --- /dev/null +++ b/include/libremidi/backends/kdmapi/config.hpp @@ -0,0 +1,23 @@ +#pragma once +#include + +namespace libremidi::kdmapi +{ +// KDMAPI does not support MIDI input +struct input_configuration +{ +}; + +struct output_configuration +{ + // Use the no-buffer variant of SendDirectData for lowest latency + // This bypasses OmniMIDI's internal buffer + bool use_no_buffer = false; +}; + +struct observer_configuration +{ +}; + +} + diff --git a/include/libremidi/backends/kdmapi/helpers.hpp b/include/libremidi/backends/kdmapi/helpers.hpp new file mode 100644 index 00000000..affcf1b9 --- /dev/null +++ b/include/libremidi/backends/kdmapi/helpers.hpp @@ -0,0 +1,178 @@ +#pragma once +// clang-format off +#define NOMINMAX 1 +#define WIN32_LEAN_AND_MEAN 1 + +#include +#include +// clang-format on + +#include + +#include +#include + +namespace libremidi::kdmapi +{ + +using IsKDMAPIAvailable_t = BOOL(WINAPI*)(); +using InitializeKDMAPIStream_t = BOOL(WINAPI*)(); +using TerminateKDMAPIStream_t = BOOL(WINAPI*)(); +using ResetKDMAPIStream_t = VOID(WINAPI*)(); +using SendDirectData_t = VOID(WINAPI*)(DWORD); +using SendDirectDataNoBuf_t = VOID(WINAPI*)(DWORD); +using SendDirectLongData_t = UINT(WINAPI*)(MIDIHDR*, UINT); +using SendDirectLongDataNoBuf_t = UINT(WINAPI*)(LPSTR, DWORD); +using PrepareLongData_t = UINT(WINAPI*)(MIDIHDR*, UINT); +using UnprepareLongData_t = UINT(WINAPI*)(MIDIHDR*, UINT); +using ReturnKDMAPIVer_t = BOOL(WINAPI*)(LPDWORD, LPDWORD, LPDWORD, LPDWORD); + +class kdmapi_loader +{ +public: + static kdmapi_loader& instance() + { + static kdmapi_loader loader; + return loader; + } + + bool is_available() const noexcept { return m_available; } + + HMODULE handle() const noexcept { return m_handle; } + + // KDMAPI function pointers + IsKDMAPIAvailable_t IsKDMAPIAvailable{}; + InitializeKDMAPIStream_t InitializeKDMAPIStream{}; + TerminateKDMAPIStream_t TerminateKDMAPIStream{}; + ResetKDMAPIStream_t ResetKDMAPIStream{}; + SendDirectData_t SendDirectData{}; + SendDirectDataNoBuf_t SendDirectDataNoBuf{}; + SendDirectLongData_t SendDirectLongData{}; + SendDirectLongDataNoBuf_t SendDirectLongDataNoBuf{}; + PrepareLongData_t PrepareLongData{}; + UnprepareLongData_t UnprepareLongData{}; + ReturnKDMAPIVer_t ReturnKDMAPIVer{}; + +private: + kdmapi_loader() + { + // Try to get OmniMIDI if it's already loaded + m_handle = GetModuleHandleW(L"OmniMIDI"); + if (!m_handle) + { + // Try to load it explicitly + m_handle = LoadLibraryW(L"OmniMIDI.dll"); + } + + if (!m_handle) + { + m_available = false; + return; + } + +#if !defined(_MSC_VER) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-function-type" +#endif + + // Load all function pointers + IsKDMAPIAvailable + = reinterpret_cast(GetProcAddress(m_handle, "IsKDMAPIAvailable")); + InitializeKDMAPIStream = reinterpret_cast( + GetProcAddress(m_handle, "InitializeKDMAPIStream")); + TerminateKDMAPIStream = reinterpret_cast( + GetProcAddress(m_handle, "TerminateKDMAPIStream")); + ResetKDMAPIStream + = reinterpret_cast(GetProcAddress(m_handle, "ResetKDMAPIStream")); + SendDirectData + = reinterpret_cast(GetProcAddress(m_handle, "SendDirectData")); + SendDirectDataNoBuf + = reinterpret_cast(GetProcAddress(m_handle, "SendDirectDataNoBuf")); + SendDirectLongData + = reinterpret_cast(GetProcAddress(m_handle, "SendDirectLongData")); + SendDirectLongDataNoBuf = reinterpret_cast( + GetProcAddress(m_handle, "SendDirectLongDataNoBuf")); + PrepareLongData + = reinterpret_cast(GetProcAddress(m_handle, "PrepareLongData")); + UnprepareLongData + = reinterpret_cast(GetProcAddress(m_handle, "UnprepareLongData")); + ReturnKDMAPIVer + = reinterpret_cast(GetProcAddress(m_handle, "ReturnKDMAPIVer")); + +#if !defined(_MSC_VER) +#pragma GCC diagnostic pop +#endif + + // Check if the minimum required functions are available + if (!IsKDMAPIAvailable || !InitializeKDMAPIStream || !TerminateKDMAPIStream || !SendDirectData) + { + m_available = false; + return; + } + + // Call IsKDMAPIAvailable to activate KDMAPI mode in OmniMIDI + m_available = IsKDMAPIAvailable() != FALSE; + } + + ~kdmapi_loader() + { + } + + kdmapi_loader(const kdmapi_loader&) = delete; + kdmapi_loader& operator=(const kdmapi_loader&) = delete; + + HMODULE m_handle{}; + bool m_available{false}; +}; + +class kdmapi_stream_manager +{ +public: + static kdmapi_stream_manager& instance() + { + static kdmapi_stream_manager mgr; + return mgr; + } + + bool acquire() + { + std::lock_guard lock{m_mutex}; + auto& loader = kdmapi_loader::instance(); + if (!loader.is_available()) + return false; + + if (m_rc == 0) + { + if (!loader.InitializeKDMAPIStream()) + return false; + } + ++m_rc; + return true; + } + + void release() + { + std::lock_guard lock{m_mutex}; + if (m_rc == 0) + return; + + --m_rc; + if (m_rc == 0) + { + auto& loader = kdmapi_loader::instance(); + if (loader.TerminateKDMAPIStream) + loader.TerminateKDMAPIStream(); + } + } + +private: + kdmapi_stream_manager() = default; + ~kdmapi_stream_manager() = default; + kdmapi_stream_manager(const kdmapi_stream_manager&) = delete; + kdmapi_stream_manager& operator=(const kdmapi_stream_manager&) = delete; + + std::mutex m_mutex; + std::size_t m_rc{0}; +}; + +} diff --git a/include/libremidi/backends/kdmapi/midi_in.hpp b/include/libremidi/backends/kdmapi/midi_in.hpp new file mode 100644 index 00000000..fa8eeaf3 --- /dev/null +++ b/include/libremidi/backends/kdmapi/midi_in.hpp @@ -0,0 +1,11 @@ +#pragma once +#include +#include +#include + +namespace libremidi::kdmapi +{ + +using midi_in = libremidi::midi_in_dummy; + +} diff --git a/include/libremidi/backends/kdmapi/midi_out.hpp b/include/libremidi/backends/kdmapi/midi_out.hpp new file mode 100644 index 00000000..5ade4171 --- /dev/null +++ b/include/libremidi/backends/kdmapi/midi_out.hpp @@ -0,0 +1,173 @@ +#pragma once +#include +#include +#include +#include + +namespace libremidi::kdmapi +{ + +class midi_out final + : public midi1::out_api + , public error_handler +{ +public: + struct + : libremidi::output_configuration + , kdmapi::output_configuration + { + } configuration; + + midi_out(libremidi::output_configuration&& conf, kdmapi::output_configuration&& apiconf) + : configuration{std::move(conf), std::move(apiconf)} + { + auto& loader = kdmapi_loader::instance(); + if (!loader.is_available()) + { + libremidi_handle_error( + configuration, "KDMAPI is not available. Is OmniMIDI installed?"); + this->client_open_ = std::errc::no_such_device; + return; + } + + this->client_open_ = stdx::error{}; + } + + ~midi_out() override + { + midi_out::close_port(); + this->client_open_ = std::errc::not_connected; + } + + libremidi::API get_current_api() const noexcept override { return libremidi::API::KDMAPI; } + + stdx::error open_port(const output_port& port, std::string_view) override + { + if (port.api != libremidi::API::KDMAPI) + { + libremidi_handle_error(configuration, "port is not a KDMAPI port"); + return std::errc::invalid_argument; + } + + return do_open(); + } + + stdx::error do_open() + { + auto& mgr = kdmapi_stream_manager::instance(); + if (!mgr.acquire()) + { + libremidi_handle_error(configuration, "failed to initialize KDMAPI stream"); + return std::errc::io_error; + } + + connected_ = true; + return stdx::error{}; + } + + stdx::error close_port() override + { + if (connected_) + { + auto& mgr = kdmapi_stream_manager::instance(); + mgr.release(); + connected_ = false; + } + return stdx::error{}; + } + + stdx::error send_message(const unsigned char* message, size_t size) override + { + if (!connected_) + return std::errc::not_connected; + + if (size == 0) + { + libremidi_handle_warning(configuration, "message argument is empty!"); + return std::errc::invalid_argument; + } + + auto& loader = kdmapi_loader::instance(); + + if (message[0] == 0xF0) + { + // SysEx message + return send_sysex(message, size); + } + else + { + // Channel or system message + if (size > 3) + { + libremidi_handle_warning( + configuration, "message size is greater than 3 bytes (and not sysex)!"); + return std::errc::message_size; + } + + // Pack MIDI bytes into DWORD + DWORD packet = 0; + std::copy_n(message, size, reinterpret_cast(&packet)); + + // Send the message + if (configuration.use_no_buffer && loader.SendDirectDataNoBuf) + loader.SendDirectDataNoBuf(packet); + else + loader.SendDirectData(packet); + } + + return stdx::error{}; + } + +private: + stdx::error send_sysex(const unsigned char* message, size_t size) + { + auto& loader = kdmapi_loader::instance(); + + if (configuration.use_no_buffer && loader.SendDirectLongDataNoBuf) + { + // Use the no-buffer variant - it takes raw data directly + loader.SendDirectLongDataNoBuf( + const_cast(reinterpret_cast(message)), static_cast(size)); + return stdx::error{}; + } + + // Standard path with MIDIHDR + buffer_.assign(message, message + size); + + MIDIHDR sysex{}; + sysex.lpData = reinterpret_cast(buffer_.data()); + sysex.dwBufferLength = static_cast(size); + sysex.dwFlags = 0; + + if (loader.PrepareLongData) + { + UINT result = loader.PrepareLongData(&sysex, sizeof(MIDIHDR)); + if (result != MMSYSERR_NOERROR) + { + libremidi_handle_error(configuration, "error preparing sysex header"); + return std::errc::io_error; + } + } + + if (loader.SendDirectLongData) + { + UINT result = loader.SendDirectLongData(&sysex, sizeof(MIDIHDR)); + if (result != MMSYSERR_NOERROR) + { + libremidi_handle_error(configuration, "error sending sysex message"); + // Still try to unprepare + } + } + + if (loader.UnprepareLongData) + { + loader.UnprepareLongData(&sysex, sizeof(MIDIHDR)); + } + + return stdx::error{}; + } + + std::vector buffer_; +}; + +} diff --git a/include/libremidi/backends/kdmapi/observer.hpp b/include/libremidi/backends/kdmapi/observer.hpp new file mode 100644 index 00000000..19b5df29 --- /dev/null +++ b/include/libremidi/backends/kdmapi/observer.hpp @@ -0,0 +1,86 @@ +#pragma once +#include +#include +#include + +namespace libremidi::kdmapi +{ + +class observer final : public observer_api +{ +public: + struct + : libremidi::observer_configuration + , kdmapi::observer_configuration + { + } configuration; + + explicit observer( + libremidi::observer_configuration&& conf, kdmapi::observer_configuration&& apiconf) + : configuration{std::move(conf), std::move(apiconf)} + { + if (!configuration.has_callbacks()) + return; + + auto& loader = kdmapi_loader::instance(); + if (!loader.is_available()) + return; + + // KDMAPI provides a single virtual output port + if (configuration.notify_in_constructor) + { + if (configuration.output_added) + { + auto ports = get_output_ports(); + for (const auto& port : ports) + configuration.output_added(port); + } + } + } + + ~observer() = default; + + libremidi::API get_current_api() const noexcept override { return libremidi::API::KDMAPI; } + + std::vector get_input_ports() const noexcept override + { + // KDMAPI does not provide input ports + return {}; + } + + std::vector get_output_ports() const noexcept override + { + auto& loader = kdmapi_loader::instance(); + if (!loader.is_available()) + return {}; + + // Get KDMAPI version for display name + std::string display_name = "OmniMIDI"; + if (loader.ReturnKDMAPIVer) + { + DWORD major{}, minor{}, build{}, rev{}; + if (loader.ReturnKDMAPIVer(&major, &minor, &build, &rev)) + { + display_name += " (KDMAPI "; + display_name += std::to_string(major); + display_name += "."; + display_name += std::to_string(minor); + display_name += "."; + display_name += std::to_string(build); + display_name += ")"; + } + } + + return {libremidi::output_port{ + {.api = libremidi::API::KDMAPI, + .client = 0, + .port = 0, + .manufacturer = "KeppySoftware", + .device_name = "OmniMIDI", + .port_name = "OmniMIDI", + .display_name = std::move(display_name)}, + }}; + } +}; + +} diff --git a/include/libremidi/configurations.hpp b/include/libremidi/configurations.hpp index 01d34c7c..c6de2bfe 100644 --- a/include/libremidi/configurations.hpp +++ b/include/libremidi/configurations.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -35,7 +36,7 @@ using input_api_configuration = std::variant< alsa_raw_ump::input_configuration, alsa_seq::input_configuration, alsa_seq_ump::input_configuration, coremidi_input_configuration, coremidi_ump::input_configuration, emscripten_input_configuration, jack_input_configuration, - kbd_input_configuration, libremidi::net::dgram_input_configuration, + kbd_input_configuration, kdmapi::input_configuration, libremidi::net::dgram_input_configuration, libremidi::net_ump::dgram_input_configuration, pipewire_input_configuration, winmidi::input_configuration, winmm_input_configuration, winuwp_input_configuration, jack_ump::input_configuration, pipewire_ump::input_configuration, android::input_configuration, @@ -46,21 +47,22 @@ using output_api_configuration = std::variant< alsa_raw_ump::output_configuration, alsa_seq::output_configuration, alsa_seq_ump::output_configuration, coremidi_output_configuration, coremidi_ump::output_configuration, emscripten_output_configuration, jack_output_configuration, - libremidi::net::dgram_output_configuration, libremidi::net_ump::dgram_output_configuration, - pipewire_output_configuration, winmidi::output_configuration, winmm_output_configuration, - winuwp_output_configuration, jack_ump::output_configuration, - pipewire_ump::output_configuration, android::output_configuration, libremidi::API>; + kdmapi::output_configuration, libremidi::net::dgram_output_configuration, + libremidi::net_ump::dgram_output_configuration, pipewire_output_configuration, + winmidi::output_configuration, winmm_output_configuration, winuwp_output_configuration, + jack_ump::output_configuration, pipewire_ump::output_configuration, + android::output_configuration, libremidi::API>; using observer_api_configuration = std::variant< unspecified_configuration, dummy_configuration, alsa_raw_observer_configuration, alsa_raw_ump::observer_configuration, alsa_seq::observer_configuration, alsa_seq_ump::observer_configuration, coremidi_observer_configuration, coremidi_ump::observer_configuration, emscripten_observer_configuration, - jack_observer_configuration, libremidi::net::dgram_observer_configuration, - libremidi::net_ump::dgram_observer_configuration, pipewire_observer_configuration, - winmidi::observer_configuration, winmm_observer_configuration, winuwp_observer_configuration, - jack_ump::observer_configuration, pipewire_ump::observer_configuration, - android::observer_configuration, libremidi::API>; + jack_observer_configuration, kdmapi::observer_configuration, + libremidi::net::dgram_observer_configuration, libremidi::net_ump::dgram_observer_configuration, + pipewire_observer_configuration, winmidi::observer_configuration, winmm_observer_configuration, + winuwp_observer_configuration, jack_ump::observer_configuration, + pipewire_ump::observer_configuration, android::observer_configuration, libremidi::API>; LIBREMIDI_EXPORT libremidi::API midi_api(const input_api_configuration& conf);