Skip to content

Commit 26331f5

Browse files
committed
ports: add another heuristic to compare ports
1 parent d880679 commit 26331f5

2 files changed

Lines changed: 306 additions & 31 deletions

File tree

include/libremidi/detail/memory.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#pragma once
22

3+
#include <libremidi/config.hpp>
4+
35
#include <memory>
46
#include <mutex>
57

include/libremidi/port_comparison.hpp

Lines changed: 304 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
#pragma once
22
#include <libremidi/observer_configuration.hpp>
3-
4-
#include <span>
5-
#include <variant>
3+
#if __has_include(<boost/container/flat_map.hpp>)
4+
#include <boost/container/flat_map.hpp>
5+
namespace libremidi
6+
{
7+
template <typename K, typename V>
8+
using temp_map_type = boost::container::flat_map<K, V>;
9+
}
10+
#else
11+
#include <map>
12+
namespace libremidi
13+
{
14+
template <typename K, typename V>
15+
using temp_map_type = std::map<K, V>;
16+
}
17+
#endif
18+
#include <algorithm>
619
#include <limits>
20+
#include <span>
721
#include <tuple>
822

923
namespace libremidi
@@ -47,6 +61,29 @@ struct port_identity_less
4761
}
4862
};
4963

64+
struct port_exactly_equal
65+
{
66+
bool operator()(const port_information& lhs, const port_information& rhs)
67+
{
68+
return lhs.api == rhs.api && lhs.container == rhs.container && lhs.device == rhs.device
69+
&& lhs.port == rhs.port && lhs.manufacturer == rhs.manufacturer
70+
&& lhs.product == rhs.product && lhs.serial == rhs.serial
71+
&& lhs.device_name == rhs.device_name && lhs.port_name == rhs.port_name
72+
&& lhs.display_name == rhs.display_name;
73+
}
74+
};
75+
76+
struct port_mostly_equal
77+
{
78+
bool operator()(const port_information& lhs, const port_information& rhs)
79+
{
80+
return lhs.api == rhs.api && lhs.container == rhs.container && lhs.device == rhs.device
81+
&& lhs.port == rhs.port && lhs.manufacturer == rhs.manufacturer
82+
&& lhs.product == rhs.product && lhs.serial == rhs.serial
83+
&& lhs.device_name == rhs.device_name && lhs.port_name == rhs.port_name;
84+
}
85+
};
86+
5087
struct port_heuristic_matcher
5188
{
5289
// Configuration for weights
@@ -222,20 +259,20 @@ struct port_heuristic_matcher
222259
}
223260
};
224261

225-
struct input_port_search_result
262+
template <typename T>
263+
struct port_search_result
226264
{
227-
const input_port* port = nullptr;
265+
const T* port = nullptr;
228266
int score = 0;
229267
bool found = false;
230268
};
231269

232-
inline input_port_search_result find_closest_port(
233-
const input_port& target,
234-
std::span<input_port> candidates)
270+
template <typename T>
271+
inline port_search_result<T> find_closest_port(const T& target, std::span<const T> candidates)
235272
{
236273
port_heuristic_matcher matcher{};
237274

238-
const input_port* best_match = nullptr;
275+
const T* best_match = nullptr;
239276
port_heuristic_matcher::match_score best_score;
240277
best_score.score = -1;
241278

@@ -256,37 +293,273 @@ inline input_port_search_result find_closest_port(
256293
return { nullptr, 0, false };
257294
}
258295

259-
struct output_port_search_result
296+
template <typename T>
297+
inline std::vector<const T*>
298+
optimistic_serialized_port_lookup(const T& target, std::span<const T> ports)
260299
{
261-
const output_port* port = nullptr;
262-
int score = 0;
263-
bool found = false;
264-
};
300+
if (ports.empty())
301+
return {};
265302

266-
inline output_port_search_result find_closest_port(
267-
const output_port& target,
268-
std::span<output_port> candidates)
269-
{
270-
port_heuristic_matcher matcher{};
303+
// 1. Look for an exact match on all fields
304+
std::vector<const T*> candidates;
305+
for (auto& candidate : ports)
306+
{
307+
if (port_mostly_equal{}(target, candidate))
308+
candidates.push_back(&candidate);
309+
}
310+
switch (candidates.size())
311+
{
312+
case 0: {
313+
break;
314+
}
315+
case 1: {
316+
return candidates;
317+
}
318+
default: {
319+
// If we have an exact match return it
320+
for (auto* candidate : candidates)
321+
if (target.display_name == candidate->display_name)
322+
return {candidate};
323+
324+
// Else return the entire bunch as we have no way to differentiate
325+
return candidates;
326+
}
327+
}
271328

272-
const output_port* best_match = nullptr;
273-
port_heuristic_matcher::match_score best_score{};
274-
best_score.score = -1;
329+
// 2. Heuristics
275330

276-
for (const auto& candidate : candidates)
331+
// Look for candidates in the same API
332+
candidates.clear();
333+
for (auto& candidate : ports)
277334
{
278-
port_heuristic_matcher::match_score current = matcher.calculate(target, candidate);
279-
280-
if (current.is_match() && current > best_score)
335+
if (target.api != libremidi::API::UNSPECIFIED)
281336
{
282-
best_score = current;
283-
best_match = &candidate;
337+
if (target.api == candidate.api)
338+
{
339+
// Port was set, let's give a high trust to this
340+
if (target.port != static_cast<port_handle>(-1))
341+
{
342+
if (target.port == candidate.port)
343+
{
344+
if (target.port_name == candidate.port_name
345+
&& target.device_name == candidate.device_name)
346+
{
347+
// We can be 99% confident it's the right one
348+
candidates.push_back(&candidate);
349+
}
350+
else if (
351+
port_heuristic_matcher::fuzzy_match_name(target.port_name, candidate.port_name)
352+
>= 0.8
353+
&& port_heuristic_matcher::fuzzy_match_name(
354+
target.device_name, candidate.device_name)
355+
>= 0.8)
356+
{
357+
candidates.push_back(&candidate);
358+
}
359+
else
360+
{
361+
// Same API & same port, different port_name & device_name:
362+
// very likely it's the wrong one
363+
continue;
364+
}
365+
}
366+
}
367+
else
368+
{
369+
#define do_compare(MEMBER) \
370+
{ \
371+
ok &= target.MEMBER == candidate.MEMBER || target.MEMBER.empty(); \
372+
if (!ok) \
373+
continue; \
374+
}
375+
bool ok = true;
376+
377+
// These three are compared later
378+
// do_compare(display_name);
379+
// do_compare(container);
380+
// do_compare(device);
381+
do_compare(manufacturer);
382+
do_compare(product);
383+
do_compare(serial);
384+
do_compare(device_name);
385+
do_compare(port_name);
386+
387+
#undef do_compare
388+
// If we got there it's a very good candidate
389+
candidates.push_back(&candidate);
390+
}
391+
}
284392
}
285393
}
286394

287-
if (best_match)
288-
return {best_match, best_score.score, true};
395+
switch (candidates.size())
396+
{
397+
case 0: {
398+
break;
399+
}
400+
case 1: {
401+
// One candidate in the same API
402+
return {candidates[0]};
403+
}
404+
default: {
405+
// Let's look if we have one that has the same container
406+
if (!get_if<libremidi_variant_alias::monostate>(&target.container)
407+
&& !get_if<libremidi_variant_alias::monostate>(&target.device)
408+
&& !target.display_name.empty())
409+
{
410+
for (auto* candidate : candidates)
411+
if (target.container == candidate->container && target.device == candidate->device
412+
&& target.display_name == candidate->display_name)
413+
return {candidate};
414+
for (auto* candidate : candidates)
415+
if (target.display_name == candidate->display_name)
416+
return {candidate};
417+
for (auto* candidate : candidates)
418+
if (target.container == candidate->container && target.device == candidate->device)
419+
return {candidate};
420+
for (auto* candidate : candidates)
421+
if (target.container == candidate->container)
422+
return {candidate};
423+
for (auto* candidate : candidates)
424+
if (target.device == candidate->device)
425+
return {candidate};
426+
}
427+
else if (
428+
!get_if<libremidi_variant_alias::monostate>(&target.container)
429+
&& !target.display_name.empty())
430+
{
431+
for (auto* candidate : candidates)
432+
if (target.container == candidate->container
433+
&& target.display_name == candidate->display_name)
434+
return {candidate};
435+
for (auto* candidate : candidates)
436+
if (target.display_name == candidate->display_name)
437+
return {candidate};
438+
for (auto* candidate : candidates)
439+
if (target.container == candidate->container)
440+
return {candidate};
441+
}
442+
else if (
443+
!get_if<libremidi_variant_alias::monostate>(&target.device)
444+
&& !target.display_name.empty())
445+
{
446+
for (auto* candidate : candidates)
447+
if (target.device == candidate->device && target.display_name == candidate->display_name)
448+
return {candidate};
449+
for (auto* candidate : candidates)
450+
if (target.display_name == candidate->display_name)
451+
return {candidate};
452+
for (auto* candidate : candidates)
453+
if (target.device == candidate->device)
454+
return {candidate};
455+
}
456+
// Else return them all as we have no way to differentiate
457+
return candidates;
458+
}
459+
}
460+
461+
// Look for candidates in different APIs.
462+
// Here most informations are different, so we only do a fuzzy match
463+
candidates.clear();
464+
libremidi::temp_map_type<int, const T*> ranked_candidates;
465+
for (auto& candidate : ports)
466+
{
467+
int score = 0;
468+
if (!target.port_name.empty() && !candidate.port_name.empty())
469+
{
470+
float res = port_heuristic_matcher::fuzzy_match_name(target.port_name, candidate.port_name);
471+
if (res > 0.7)
472+
{
473+
score += res;
474+
}
475+
}
476+
if (!target.device_name.empty() && !candidate.device_name.empty())
477+
{
478+
float res
479+
= port_heuristic_matcher::fuzzy_match_name(target.device_name, candidate.device_name);
480+
if (res > 0.7)
481+
{
482+
score += res;
483+
}
484+
}
485+
if (!target.display_name.empty() && !candidate.display_name.empty())
486+
{
487+
float res
488+
= port_heuristic_matcher::fuzzy_match_name(target.display_name, candidate.display_name);
489+
if (res > 0.7)
490+
{
491+
score += res;
492+
}
493+
}
494+
if (!target.display_name.empty() && !candidate.port_name.empty())
495+
{
496+
float res
497+
= port_heuristic_matcher::fuzzy_match_name(target.display_name, candidate.port_name);
498+
if (res > 0.7)
499+
{
500+
score += res;
501+
}
502+
}
503+
if (!target.port_name.empty() && !candidate.display_name.empty())
504+
{
505+
float res
506+
= port_heuristic_matcher::fuzzy_match_name(target.port_name, candidate.display_name);
507+
if (res > 0.7)
508+
{
509+
score += res;
510+
}
511+
}
512+
if (!target.display_name.empty() && !candidate.device_name.empty())
513+
{
514+
float res
515+
= port_heuristic_matcher::fuzzy_match_name(target.display_name, candidate.device_name);
516+
if (res > 0.7)
517+
{
518+
score += res;
519+
}
520+
}
521+
if (!target.device_name.empty() && !candidate.display_name.empty())
522+
{
523+
float res
524+
= port_heuristic_matcher::fuzzy_match_name(target.device_name, candidate.display_name);
525+
if (res > 0.7)
526+
{
527+
score += res;
528+
}
529+
}
530+
if (!target.manufacturer.empty() && !candidate.manufacturer.empty())
531+
{
532+
float res
533+
= port_heuristic_matcher::fuzzy_match_name(target.manufacturer, candidate.manufacturer);
534+
if (res > 0.7)
535+
{
536+
score += 3 * res;
537+
}
538+
}
539+
if (!target.product.empty() && !candidate.product.empty())
540+
{
541+
float res = port_heuristic_matcher::fuzzy_match_name(target.product, candidate.product);
542+
if (res > 0.7)
543+
{
544+
score += 5 * res;
545+
}
546+
}
547+
if (!target.serial.empty() && !candidate.serial.empty())
548+
{
549+
float res = port_heuristic_matcher::fuzzy_match_name(target.serial, candidate.serial);
550+
if (res > 0.7)
551+
{
552+
score += 10 * res;
553+
}
554+
}
555+
if (score > 0)
556+
ranked_candidates[score] = &candidate;
557+
}
289558

290-
return {nullptr, 0, false};
559+
candidates.clear();
560+
candidates.reserve(ranked_candidates.size());
561+
for (auto [score, candidate] : ranked_candidates)
562+
candidates.insert(candidates.begin(), candidate);
563+
return candidates;
291564
}
292565
}

0 commit comments

Comments
 (0)