diff --git a/cmake/libremidi.examples.cmake b/cmake/libremidi.examples.cmake index 54e3d269..7181551b 100644 --- a/cmake/libremidi.examples.cmake +++ b/cmake/libremidi.examples.cmake @@ -17,6 +17,7 @@ add_example(midiobserve) add_example(echo) add_example(cmidiin) add_example(cmidiin2) +add_example(lookup) add_example(midi1_to_midi2) add_example(midiclock_in) add_example(midiclock_out) diff --git a/cmake/libremidi.sources.cmake b/cmake/libremidi.sources.cmake index 26413844..f7c0c392 100644 --- a/cmake/libremidi.sources.cmake +++ b/cmake/libremidi.sources.cmake @@ -142,6 +142,8 @@ target_sources(libremidi PRIVATE include/libremidi/input_configuration.hpp include/libremidi/libremidi.hpp include/libremidi/message.hpp + include/libremidi/port_comparison.hpp + include/libremidi/port_information.hpp include/libremidi/output_configuration.hpp include/libremidi/ump_events.hpp diff --git a/examples/lookup.cpp b/examples/lookup.cpp new file mode 100644 index 00000000..19392f91 --- /dev/null +++ b/examples/lookup.cpp @@ -0,0 +1,61 @@ +// midiprobe.cpp +// +// Simple program to check MIDI inputs and outputs. +// +// by Gary Scavone, 2003-2012. + +#include "utils.hpp" + +#include +#include + +#if defined(_WIN32) && __has_include() + #include +#endif + +#include +#include +#include +#include + +void lookup_api(libremidi::API api, const libremidi::input_port& searched) +{ + std::string_view api_name = libremidi::get_api_display_name(api); + std::cout << "Displaying ports for: " << api_name << std::endl; + + // On Windows 10, apparently the MIDI devices aren't exactly available as soon as the app open... + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + libremidi::observer midi{ + {.track_hardware = true, .track_virtual = true}, libremidi::observer_configuration_for(api)}; + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + { + // Check inputs. + auto ports = midi.get_input_ports(); + auto res = libremidi::find_closest_port(searched, ports); + if (res.found) { + std::cout << "Found: " << *res.port << "\n"; + } + } +} + +int main() +{ +#if defined(_WIN32) && __has_include() + // Necessary for using WinUWP and WinMIDI, must be done as early as possible in your main() + winrt::init_apartment(); +#endif + + // This will find the port that is closest to this. + // For instance, given a Launchpad and a Launchpad Mini, + // the launchpad will be returned. Otherwise, the mini will be returned. + libremidi::input_port searched{{.port_name = "launchpad"}}; + + for (auto& api : libremidi::available_apis()) + lookup_api(api, searched); + for (auto& api : libremidi::available_ump_apis()) + lookup_api(api, searched); + return 0; +} diff --git a/examples/midiprobe.cpp b/examples/midiprobe.cpp index 680fd523..21818f7f 100644 --- a/examples/midiprobe.cpp +++ b/examples/midiprobe.cpp @@ -19,11 +19,6 @@ void enumerate_api(libremidi::API api) { -#if defined(_WIN32) && __has_include() - // Necessary for using WinUWP and WinMIDI, must be done as early as possible in your main() - winrt::init_apartment(); -#endif - std::string_view api_name = libremidi::get_api_display_name(api); std::cout << "Displaying ports for: " << api_name << std::endl; @@ -58,6 +53,10 @@ void enumerate_api(libremidi::API api) int main() { +#if defined(_WIN32) && __has_include() + // Necessary for using WinUWP and WinMIDI, must be done as early as possible in your main() + winrt::init_apartment(); +#endif for (auto& api : libremidi::available_apis()) enumerate_api(api); for (auto& api : libremidi::available_ump_apis()) diff --git a/include/libremidi/backends/jack/helpers.hpp b/include/libremidi/backends/jack/helpers.hpp index 9ce74311..afee8296 100644 --- a/include/libremidi/backends/jack/helpers.hpp +++ b/include/libremidi/backends/jack/helpers.hpp @@ -50,7 +50,7 @@ struct jack_client .client = reinterpret_cast(client), .port = 0, .manufacturer = "", - .device_name = "", + .device_name = jack_get_client_name(client), .port_name = jack_port_name(port), .display_name = get_port_display_name(port), }}; diff --git a/include/libremidi/observer_configuration.hpp b/include/libremidi/observer_configuration.hpp index f6486d2d..89669979 100644 --- a/include/libremidi/observer_configuration.hpp +++ b/include/libremidi/observer_configuration.hpp @@ -1,129 +1,8 @@ #pragma once -#include -#include -#include -#include - -#include -#include -#include -#include +#include namespace libremidi { -struct LIBREMIDI_EXPORT port_information -{ - // Compat - using port_type = libremidi::transport_type; - - /// Which API is this port for. port_information objects are in general - /// not useable for different APIs than the API of the observer that created them. - libremidi::API api{}; - - /// Handle to the API client object if the API provides one - // ALSA Raw: unused - // ALSA Seq: snd_seq_t* - // CoreMIDI: MidiClientRef - // JACK: jack_client_t* - // PipeWire: unused // FIXME: pw_context? pw_main_loop? - // WebMIDI: unused - // WinMIDI: TODO - // WinMM: unused - // WinUWP: unused - client_handle client = static_cast(-1); - - /// Container identifier if the API provides one - // ALSA: device id (std::string), e.g. ID_PATH as returned by udev: "pci-0000:00:14.0-usb-0:12:1.0" - // CoreMIDI: USBLocationID (int32_t) - // WinMIDI: ContainerID GUID (bit_cast to a winapi or winrt::GUID ; - // this is not the string but the binary representation). - container_identifier container = std::monostate{}; - - /// Device identifier if the API provides one - // ALSA: sysfs path (std::string), e.g. "/sys/devices/pci0000:00/0000:00:02.2/0000:02:00.0/sound/card0/controlC0" - // CoreMIDI: USBVendorProduct (int32_t) - // WinMIDI: EndpointDeviceId (std::string), e.g. "\\?\swd#midisrv#midiu_ksa..." - // WinMM: MIDI{IN,OUT}CAPS mId / pId { uint16_t manufacturer_id, uint16_t product_id; } - device_identifier device = std::monostate{}; - - /// Handle to the port identifier if the API provides one - // ALSA Raw: bit_cast to struct { uint16_t card, device, sub, padding; }. - // ALSA Seq: bit_cast to struct { uint32_t client, port; } - // CoreMIDI: MidiObjectRef's kMIDIPropertyUniqueID (uint32_t) - // JACK: jack_port_id_t - // PipeWire: port.id - // WebMIDI: index of the MIDI device in the list provided by the browser. - // WinMIDI: uint64_t terminal_block_number; (MidiGroupTerminalBlock::Number(), index is 1-based) - // WinMM: port index between 0 and midi{In,Out}GetNumDevs() - // WinUWP: index of the MIDI device in the list provided by the OS. - port_handle port = static_cast(-1); - - /// User-readable information - // ALSA Raw: ID_VENDOR_FROM_DATABASE if provided by udev - // ALSA Seq: ID_VENDOR_FROM_DATABASE if provided by udev - // CoreMIDI: kMIDIPropertyManufacturer - // WinMIDI: MidiEndpointDeviceInformation::GetTransportSuppliedInfo().ManufacturerName - // WinMM: unavailable - std::string manufacturer{}; - - // ALSA Raw: ID_MODEL_FROM_DATABASE if provided by udev - // ALSA Seq: ID_MODEL_FROM_DATABASE if provided by udev - // WinMIDI: MidiEndpointDeviceInformation::GetTransportSuppliedInfo().Name - std::string product{}; - - /// "Unique" serial number. Note that this is super unreliable - pretty - /// much no MIDI device manufacturer bothers with unique per-device serial number - /// unlike most USB devices. - // ALSA Raw: ID_USB_SERIAL if provided by udev. - // ALSA Seq: ID_USB_SERIAL if provided by udev. - // WinMIDI: MidiEndpointDeviceInformation::GetTransportSuppliedInfo().SerialNumber - std::string serial{}; - - // ALSA Raw: Name returned by snd_rawmidi_info_get_name - // ALSA Seq: Name returned by snd_seq_client_info_get_name - // CoreMIDI: kMIDIPropertyModel - // WinMIDI: MidiEndpointDeviceInformation::Name - // WinMM: unavailable - std::string device_name{}; - - // ALSA Raw: Name returned by snd_rawmidi_info_get_subdevice_name - // ALSA Seq: Name returned by snd_seq_port_info_get_name - // CoreMIDI: kMIDIPropertyName - // WinMIDI: MidiGroupTerminalBlock::Name - // WinMM: szPname - std::string port_name{}; - - // CoreMIDI: kMIDIPropertyDisplayName - // Otherwise: the closest to a unique name we can get - std::string display_name{}; - - /// Port type - // CoreMIDI: available - // WinMIDI: available - // WinMM: unavailable - port_type type = port_type::unknown; - - // Equality and comparison operators are deleted as there is not one - // single correct way to compare two port_information: - // in some cases it may be useful to only compare the names, while in other cases - // it is necessary to check whether this is the exact same low-level identifier. - // Thus, the end-user must define their own custom equality operators - // if using std:: containers or algorithms - bool operator==(const port_information& other) const noexcept = delete; - std::strong_ordering operator<=>(const port_information& other) const noexcept = delete; -}; - -struct input_port : port_information -{ - bool operator==(const input_port& other) const noexcept = delete; - std::strong_ordering operator<=>(const input_port& other) const noexcept = delete; -}; -struct output_port : port_information -{ - bool operator==(const output_port& other) const noexcept = delete; - std::strong_ordering operator<=>(const output_port& other) const noexcept = delete; -}; - using input_port_callback = std::function; using output_port_callback = std::function; struct observer_configuration diff --git a/include/libremidi/port_comparison.hpp b/include/libremidi/port_comparison.hpp new file mode 100644 index 00000000..cac39a2a --- /dev/null +++ b/include/libremidi/port_comparison.hpp @@ -0,0 +1,292 @@ +#pragma once +#include + +#include +#include +#include +#include + +namespace libremidi +{ +// Simple compare-the-name equality. Useful for saving / reloading +struct port_name_equal +{ + bool operator()(const port_information& lhs, const port_information& rhs) + { + return lhs.api == rhs.api && lhs.port_name == rhs.port_name; + } +}; +struct port_name_less +{ + bool operator()(const port_information& lhs, const port_information& rhs) + { + return std::tie(lhs.api, lhs.port_name) < std::tie(rhs.api, rhs.port_name); + } +}; + +// Compare an existing port with others. +// Note that on multiple APIs, this comparison method is only valid as long +// as no device gets connected / disconnected, as the port identifier / handle +// is sadly just the index in the list of devices returned by the OS, which will +// change as soon as a device changes. +// Thus, it should only ever be used to compare devices between a group obtained +// from a single call to get_input_ports / get_output_ports or in situations where we can +// be sure that there is no hot-plugging. +struct port_identity_equal +{ + bool operator()(const port_information& lhs, const port_information& rhs) + { + return lhs.api == rhs.api && lhs.port == rhs.port; + } +}; +struct port_identity_less +{ + bool operator()(const port_information& lhs, const port_information& rhs) + { + return std::tie(lhs.api, lhs.port) < std::tie(rhs.api, rhs.port); + } +}; + +struct port_heuristic_matcher +{ + // Configuration for weights + static constexpr int W_HARDWARE_ID = 1000; // Unique HW IDs (Container, Device) + static constexpr int W_SERIAL = 800; // Serial Number + static constexpr int W_NAME_EXACT = 400; // Display/Port/Device Names + static constexpr int W_METADATA = 100; // Manufacturer/Product + static constexpr int W_HANDLE = 50; // Port Index/Handle + + // Penalties for mismatches when data is present in both but differs + static constexpr int P_HARDWARE_MISMATCH = -2000; + static constexpr int P_SERIAL_MISMATCH = -1000; + static constexpr int P_NAME_MISMATCH = -100; + static constexpr int P_PRODUCT_MISMATCH = -10; + + struct match_score + { + int score = 0; + bool api_mismatch = false; + + bool is_match() const { return !api_mismatch && score > 0; } + constexpr auto operator<=>(const match_score& other) const noexcept = default; + }; + + static inline constexpr bool chars_equal_ignore_case(char lhs, char rhs) { + if(lhs >= 'A' && lhs <= 'Z') + lhs -= 'A' - 'a'; + if(rhs >= 'A' && rhs <= 'Z') + rhs -= 'A' - 'a'; + + return lhs == rhs; + } + + static inline double fuzzy_match_name(std::string_view s1, std::string_view s2) + { + const size_t len1 = s1.size(); + const size_t len2 = s2.size(); + if (len1 == 0 && len2 == 0) + return 1.0; + if (len1 == 0 || len2 == 0) + return 0; + if (len1 > 1024 || len2 > 1024) + return 0; + if (len1 > len2) + return fuzzy_match_name(s2, s1); + + auto col = (size_t*)alloca(sizeof(size_t) * (len1 + 1)); + std::fill_n(col, len1 + 1, 0); + + // Initialize first column (0, 1, 2... len1) + for (size_t i = 0; i <= len1; ++i) col[i] = i; + + // Compute Levenshtein distance + for (size_t j = 1; j <= len2; ++j) { + size_t prev_diag = col[0]; + col[0] = j; + + for (size_t i = 1; i <= len1; ++i) { + size_t prev_col = col[i]; + size_t cost = chars_equal_ignore_case(s1[i - 1], s2[j - 1]) ? 0 : 1; + + col[i] = std::min({ + col[i] + 1, // Deletion + col[i - 1] + 1, // Insertion + prev_diag + cost // Substitution + }); + + prev_diag = prev_col; + } + } + + const size_t distance = col[len1]; + const size_t max_len = std::max(len1, len2); + + if (max_len == 0) + return 1.0; + + return 1.0 - (static_cast(distance) / static_cast(max_len)); + } + + match_score calculate(const port_information& target, const port_information& candidate) const + { + match_score result; + + // 1. API Mismatch + // It is impossible for a port to be the same if the API is different. + if (target.api != libremidi::API::UNSPECIFIED) + { + if (target.api != candidate.api) + { + result.api_mismatch = true; + result.score = std::numeric_limits::min(); + return result; + } + } + + // 2. Hardware Identifiers & Serial Number + // High value, but unreliable presence. + switch(target.api) + { + case libremidi::API::COREMIDI: + case libremidi::API::COREMIDI_UMP: + case libremidi::API::WINDOWS_MM: + { + score_variant(result.score, target.device, candidate.device, W_HARDWARE_ID, P_HARDWARE_MISMATCH); + break; + } + default: + break; + } + + score_string(result.score, target.manufacturer, candidate.manufacturer, W_METADATA, P_HARDWARE_MISMATCH); + score_string(result.score, target.product, candidate.product, W_METADATA, P_HARDWARE_MISMATCH); + score_string(result.score, target.serial, candidate.serial, W_SERIAL, P_SERIAL_MISMATCH); + + // 3. Names + // We accumulate score for every name that matches. + score_string(result.score, target.display_name, candidate.display_name, W_NAME_EXACT, P_NAME_MISMATCH); + score_string(result.score, target.port_name, candidate.port_name, W_NAME_EXACT, P_NAME_MISMATCH); + score_string(result.score, target.device_name, candidate.device_name, W_NAME_EXACT, P_NAME_MISMATCH); + + // 4. Port Handle (Index) + // Only check if it's not the default -1. + // We rely on this primarily as a tie-breaker if names/hardware IDs are identical + // (e.g. two identical controllers plugged in). + if (target.port != static_cast(-1)) + { + if (target.port == candidate.port) + { + result.score += W_HANDLE; + } + } + + return result; + } + +private: + void score_string(int& score, std::string_view target_s, std::string_view cand_s, int reward, int penalty) const + { + // If the target doesn't know this info, we can't judge. Skip. + if (target_s.empty()) + return; + + const double res = fuzzy_match_name(cand_s, target_s); + if (res >= 0.5) + { + score += res * reward; + } + else if (!cand_s.empty()) + { + // If candidate value is empty, it's just missing info, not necessarily a mismatch. + score += penalty; + } + } + + // Helper for std::variant fields (device / container identifiers) + template + void score_variant(int& score, const T& target_v, const T& cand_v, int reward, int penalty) const + { + if (std::holds_alternative(target_v)) + return; + + // For those we want an exact search + if (target_v == cand_v) + { + score += reward; + } + else if (!std::holds_alternative(cand_v)) + { + // Candidate has a specific ID, and it differs from Target's specific ID. + score += penalty; + } + } +}; + +struct input_port_search_result +{ + const input_port* port = nullptr; + int score = 0; + bool found = false; +}; + +inline input_port_search_result find_closest_port( + const input_port& target, + std::span candidates) +{ + port_heuristic_matcher matcher{}; + + const input_port* best_match = nullptr; + port_heuristic_matcher::match_score best_score; + best_score.score = -1; + + for (const auto& candidate : candidates) + { + port_heuristic_matcher::match_score current = matcher.calculate(target, candidate); + + if (current.is_match() && current > best_score) + { + best_score = current; + best_match = &candidate; + } + } + + if (best_match) + return { best_match, best_score.score, true }; + + return { nullptr, 0, false }; +} + +struct output_port_search_result +{ + const output_port* port = nullptr; + int score = 0; + bool found = false; +}; + +inline output_port_search_result find_closest_port( + const output_port& target, + std::span candidates) +{ + port_heuristic_matcher matcher{}; + + const output_port* best_match = nullptr; + port_heuristic_matcher::match_score best_score{}; + best_score.score = -1; + + for (const auto& candidate : candidates) + { + port_heuristic_matcher::match_score current = matcher.calculate(target, candidate); + + if (current.is_match() && current > best_score) + { + best_score = current; + best_match = &candidate; + } + } + + if (best_match) + return {best_match, best_score.score, true}; + + return {nullptr, 0, false}; +} +} diff --git a/include/libremidi/port_information.hpp b/include/libremidi/port_information.hpp new file mode 100644 index 00000000..5dba0e82 --- /dev/null +++ b/include/libremidi/port_information.hpp @@ -0,0 +1,164 @@ +#pragma once +#include +#include +#include +#include + +#include +#include +#include + +namespace libremidi +{ + +struct LIBREMIDI_EXPORT port_information +{ + // Compat + using port_type = libremidi::transport_type; + + /// Which API is this port for. port_information objects are in general + /// not useable for different APIs than the API of the observer that created them. + libremidi::API api{}; + + /// Handle to the API client object if the API provides one + // Android: unused + // ALSA Raw: unused + // ALSA Seq: snd_seq_t* + // CoreMIDI: MidiClientRef + // JACK: jack_client_t* + // PipeWire: unused // FIXME: pw_context? pw_main_loop? + // WebMIDI: unused + // WinMIDI: TODO + // WinMM: unused + // WinUWP: unused + client_handle client = static_cast(-1); + + /// Container identifier if the API provides one + // Android: unavailable + // ALSA: device id (std::string), e.g. ID_PATH as returned by udev: "pci-0000:00:14.0-usb-0:12:1.0" + // CoreMIDI: USBLocationID (int32_t) + // JACK: unavailable + // PipeWire: unavailable + // WinMIDI: ContainerID GUID (bit_cast to a winapi or winrt::GUID ; + // this is not the string but the binary representation). + // WinMM: unavailable + // WinUWP: unavailable + container_identifier container = std::monostate{}; + + /// Device identifier if the API provides one + // Android: unavailable + // ALSA: sysfs path (std::string), e.g. "/sys/devices/pci0000:00/0000:00:02.2/0000:02:00.0/sound/card0/controlC0" + // CoreMIDI: USBVendorProduct (int32_t) + // JACK: unavailable + // PipeWire: unavailable + // WinMIDI: EndpointDeviceId (std::string), e.g. "\\?\swd#midisrv#midiu_ksa..." + // WinMM: MIDI{IN,OUT}CAPS mId / pId { uint16_t manufacturer_id, uint16_t product_id; } + // WinUWP: unavailable + device_identifier device = std::monostate{}; + + /// Handle to the port identifier if the API provides one + // Android: index of the MIDI device in the list provided by the OS. + // ALSA Raw: bit_cast to struct { uint16_t card, device, sub, padding; }. + // ALSA Seq: bit_cast to struct { uint32_t client, port; } + // CoreMIDI: MidiObjectRef's kMIDIPropertyUniqueID (uint32_t) + // JACK: jack_port_id_t + // PipeWire: "port.id" (uint32_t) + // WebMIDI: index of the MIDI device in the list provided by the browser. + // WinMIDI: uint64_t terminal_block_number; (MidiGroupTerminalBlock::Number(), index is 1-based) + // WinMM: port index between 0 and midi{In,Out}GetNumDevs() + // WinUWP: index of the MIDI device in the list provided by the OS. + port_handle port = static_cast(-1); + + /// User-readable information + // Android: unavailable + // ALSA Raw: ID_VENDOR_FROM_DATABASE if provided by udev + // ALSA Seq: ID_VENDOR_FROM_DATABASE if provided by udev + // CoreMIDI: kMIDIPropertyManufacturer + // JACK: unavailable + // PipeWire: unavailable + // WinMIDI: MidiEndpointDeviceInformation::GetTransportSuppliedInfo().ManufacturerName + // WinMM: unavailable + // WinUWP: unavailable + std::string manufacturer{}; + + // Android: unavailable + // ALSA Raw: ID_MODEL_FROM_DATABASE if provided by udev + // ALSA Seq: ID_MODEL_FROM_DATABASE if provided by udev + // JACK: unavailable + // PipeWire: unavailable + // WinMIDI: MidiEndpointDeviceInformation::GetTransportSuppliedInfo().Name + // WinUWP: unavailable + std::string product{}; + + /// "Unique" serial number. Note that this is super unreliable - pretty + /// much no MIDI device manufacturer bothers with unique per-device serial number + /// unlike most USB devices. + // Android: unavailable + // ALSA Raw: ID_USB_SERIAL if provided by udev. + // ALSA Seq: ID_USB_SERIAL if provided by udev. + // JACK: unavailable + // PipeWire: unavailable + // WinMIDI: MidiEndpointDeviceInformation::GetTransportSuppliedInfo().SerialNumber + // WinUWP: unavailable + std::string serial{}; + + // Android: unavailable + // ALSA Raw: Name returned by snd_rawmidi_info_get_name + // ALSA Seq: Name returned by snd_seq_client_info_get_name + // CoreMIDI: kMIDIPropertyModel + // JACK: jack_get_client_name + // PipeWire: first part of port_alias, e.g. "A" in A:B + // WinMIDI: MidiEndpointDeviceInformation::Name + // WinMM: unavailable + // WinUWP: unavailable + std::string device_name{}; + + // Android: MidiDeviceInfo.getProperties().getString("name") + // ALSA Raw: Name returned by snd_rawmidi_info_get_subdevice_name + // ALSA Seq: Name returned by snd_seq_port_info_get_name + // CoreMIDI: kMIDIPropertyName + // JACK: jack_port_name + // PipeWire: "port.name" + // WinMIDI: MidiGroupTerminalBlock::Name + // WinMM: szPname + // WinUWP: DeviceInformation.Id() + std::string port_name{}; + + // CoreMIDI: kMIDIPropertyDisplayName + // WinUWP: DeviceInformation.Name() + // Otherwise: the closest to a unique name we can get + std::string display_name{}; + + /// Port type + // Android: unavailable + // ALSA Raw / Seq: available if udev is avaialble + // CoreMIDI: available + // JACK: unavailable + // PipeWire: unavailable + // WinMIDI: available + // WinMM: unavailable + // WinUWP: unavailable + port_type type = port_type::unknown; + + // Equality and comparison operators are deleted as there is not one + // single correct way to compare two port_information: + // in some cases it may be useful to only compare the names, while in other cases + // it is necessary to check whether this is the exact same low-level identifier. + // Thus, the end-user must define their own custom equality operators + // if using std:: containers or algorithms + bool operator==(const port_information& other) const noexcept = delete; + std::strong_ordering operator<=>(const port_information& other) const noexcept = delete; +}; + +struct input_port : port_information +{ + bool operator==(const input_port& other) const noexcept = delete; + std::strong_ordering operator<=>(const input_port& other) const noexcept = delete; +}; +struct output_port : port_information +{ + bool operator==(const output_port& other) const noexcept = delete; + std::strong_ordering operator<=>(const output_port& other) const noexcept = delete; +}; + +}