#pragma once #include #if __has_include() #include namespace libremidi { template using temp_map_type = boost::container::flat_map; } #else #include namespace libremidi { template using temp_map_type = std::map; } #endif #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_exactly_equal { bool operator()(const port_information& lhs, const port_information& rhs) { return lhs.api == rhs.api && lhs.container == rhs.container && lhs.device == rhs.device && lhs.port == rhs.port && lhs.manufacturer == rhs.manufacturer && lhs.product == rhs.product && lhs.serial == rhs.serial && lhs.device_name == rhs.device_name && lhs.port_name == rhs.port_name && lhs.display_name == rhs.display_name; } }; struct port_mostly_equal { bool operator()(const port_information& lhs, const port_information& rhs) { return lhs.api == rhs.api && lhs.container == rhs.container && lhs.device == rhs.device && lhs.port == rhs.port && lhs.manufacturer == rhs.manufacturer && lhs.product == rhs.product && lhs.serial == rhs.serial && lhs.device_name == rhs.device_name && lhs.port_name == rhs.port_name; } }; 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 (holds_alternative(target_v)) return; // For those we want an exact search if (target_v == cand_v) { score += reward; } else if (!holds_alternative(cand_v)) { // Candidate has a specific ID, and it differs from Target's specific ID. score += penalty; } } }; template struct port_search_result { const T* port = nullptr; int score = 0; bool found = false; }; template inline port_search_result find_closest_port(const T& target, std::span candidates) { port_heuristic_matcher matcher{}; const T* 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 }; } template inline std::vector optimistic_serialized_port_lookup(const T& target, std::span ports) { if (ports.empty()) return {}; // 1. Look for an exact match on all fields std::vector candidates; for (auto& candidate : ports) { if (port_mostly_equal{}(target, candidate)) candidates.push_back(&candidate); } switch (candidates.size()) { case 0: { break; } case 1: { return candidates; } default: { // If we have an exact match return it for (auto* candidate : candidates) if (target.display_name == candidate->display_name) return {candidate}; // Else return the entire bunch as we have no way to differentiate return candidates; } } // 2. Heuristics // Look for candidates in the same API candidates.clear(); for (auto& candidate : ports) { if (target.api != libremidi::API::UNSPECIFIED) { if (target.api == candidate.api) { // Port was set, let's give a high trust to this if (target.port != static_cast(-1)) { if (target.port == candidate.port) { if (target.port_name == candidate.port_name && target.device_name == candidate.device_name) { // We can be 99% confident it's the right one candidates.push_back(&candidate); } else if ( port_heuristic_matcher::fuzzy_match_name(target.port_name, candidate.port_name) >= 0.8 && port_heuristic_matcher::fuzzy_match_name( target.device_name, candidate.device_name) >= 0.8) { candidates.push_back(&candidate); } else { // Same API & same port, different port_name & device_name: // very likely it's the wrong one continue; } } } else { #define do_compare(MEMBER) \ { \ ok &= target.MEMBER == candidate.MEMBER || target.MEMBER.empty(); \ if (!ok) \ continue; \ } bool ok = true; // These three are compared later // do_compare(display_name); // do_compare(container); // do_compare(device); do_compare(manufacturer); do_compare(product); do_compare(serial); do_compare(device_name); do_compare(port_name); #undef do_compare // If we got there it's a very good candidate candidates.push_back(&candidate); } } } } switch (candidates.size()) { case 0: { break; } case 1: { // One candidate in the same API return {candidates[0]}; } default: { // Let's look if we have one that has the same container if (!get_if(&target.container) && !get_if(&target.device) && !target.display_name.empty()) { for (auto* candidate : candidates) if (target.container == candidate->container && target.device == candidate->device && target.display_name == candidate->display_name) return {candidate}; for (auto* candidate : candidates) if (target.display_name == candidate->display_name) return {candidate}; for (auto* candidate : candidates) if (target.container == candidate->container && target.device == candidate->device) return {candidate}; for (auto* candidate : candidates) if (target.container == candidate->container) return {candidate}; for (auto* candidate : candidates) if (target.device == candidate->device) return {candidate}; } else if ( !get_if(&target.container) && !target.display_name.empty()) { for (auto* candidate : candidates) if (target.container == candidate->container && target.display_name == candidate->display_name) return {candidate}; for (auto* candidate : candidates) if (target.display_name == candidate->display_name) return {candidate}; for (auto* candidate : candidates) if (target.container == candidate->container) return {candidate}; } else if ( !get_if(&target.device) && !target.display_name.empty()) { for (auto* candidate : candidates) if (target.device == candidate->device && target.display_name == candidate->display_name) return {candidate}; for (auto* candidate : candidates) if (target.display_name == candidate->display_name) return {candidate}; for (auto* candidate : candidates) if (target.device == candidate->device) return {candidate}; } // Else return them all as we have no way to differentiate return candidates; } } // Look for candidates in different APIs. // Here most informations are different, so we only do a fuzzy match candidates.clear(); libremidi::temp_map_type ranked_candidates; for (auto& candidate : ports) { int score = 0; if (!target.port_name.empty() && !candidate.port_name.empty()) { float res = port_heuristic_matcher::fuzzy_match_name(target.port_name, candidate.port_name); if (res > 0.7) { score += res; } } if (!target.device_name.empty() && !candidate.device_name.empty()) { float res = port_heuristic_matcher::fuzzy_match_name(target.device_name, candidate.device_name); if (res > 0.7) { score += res; } } if (!target.display_name.empty() && !candidate.display_name.empty()) { float res = port_heuristic_matcher::fuzzy_match_name(target.display_name, candidate.display_name); if (res > 0.7) { score += res; } } if (!target.display_name.empty() && !candidate.port_name.empty()) { float res = port_heuristic_matcher::fuzzy_match_name(target.display_name, candidate.port_name); if (res > 0.7) { score += res; } } if (!target.port_name.empty() && !candidate.display_name.empty()) { float res = port_heuristic_matcher::fuzzy_match_name(target.port_name, candidate.display_name); if (res > 0.7) { score += res; } } if (!target.display_name.empty() && !candidate.device_name.empty()) { float res = port_heuristic_matcher::fuzzy_match_name(target.display_name, candidate.device_name); if (res > 0.7) { score += res; } } if (!target.device_name.empty() && !candidate.display_name.empty()) { float res = port_heuristic_matcher::fuzzy_match_name(target.device_name, candidate.display_name); if (res > 0.7) { score += res; } } if (!target.manufacturer.empty() && !candidate.manufacturer.empty()) { float res = port_heuristic_matcher::fuzzy_match_name(target.manufacturer, candidate.manufacturer); if (res > 0.7) { score += 3 * res; } } if (!target.product.empty() && !candidate.product.empty()) { float res = port_heuristic_matcher::fuzzy_match_name(target.product, candidate.product); if (res > 0.7) { score += 5 * res; } } if (!target.serial.empty() && !candidate.serial.empty()) { float res = port_heuristic_matcher::fuzzy_match_name(target.serial, candidate.serial); if (res > 0.7) { score += 10 * res; } } if (score > 0) ranked_candidates[score] = &candidate; } candidates.clear(); candidates.reserve(ranked_candidates.size()); for (auto [score, candidate] : ranked_candidates) candidates.insert(candidates.begin(), candidate); return candidates; } }