You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
By default, the first (in order of appearance in the config file) macro with an event matcher that matches an incoming event is executed, and then no further macros and their event matchers are evaluated. For more complex configurations, particularly involving preconditions, the concept of specificity becomes relevant to achieve the least surprising result.
The main idea of preferring more specific event matchers can be expressed as follows:
The matcher that matches the least permutations of events and conditions should be preferred over the one that matches more.
For a simple example, consider a MIDI note on event: On channel 3, key 20 is pressed with a velocity of 80:
MidiMessage::NoteOn{channel:3,key:20,velocity:80}
If we have configured the following 2 MIDI event matchers in separate macros:
// event matcher in Macro AMidiEventMatcher::NoteOn{channel_match:Some(NumberMatcher::Val(3)),key_match:Some(NumberMatcher::Range{min:11,max:30}),velocity_match:None}
// event matcher in Macro BMidiEventMatcher::NoteOn{channel_match:Some(NumberMatcher::Val(3)),key_match:Some(NumberMatcher::Val(20)),velocity_match:None}
We can analyze the two:
A: only matches on channel 3, matches keys 11-30 inclusive, matches velocity 0-127
B: only matches on channel 3, only matches key 20, matches velocity 0-127
We can calculate how many different possibilities each of them can match:
(number of channels matched) * (number of keys matched) * (number of velocities matched)
A: 1 * 30 * 128 = 3840
B: 1 * 1 * 128 = 128
From this we can see that the event matcher in macro B matches far fewer possibilities; it is more specific. When preferring macros with more specific event matchers, macro B should be executed instead of macro A.
Determining specificity
The more specific something is, the higher its specificity number should be. In the above examples, the numbers presented work the other way around. For an event matcher without preconditions involved, the specificity number is maxP - actualP where:
maxP = Maximum possible permutations for this event type (an event type can drill down into the main event type, for example Midi event -> Note on event; an event type here means drilled down to the point where there is no overlap with any others.
actualP = Calculated permutations this event matcher matches for.
Preconditions
If an event matcher has one or more preconditions, it is more specific than an identical event matcher without preconditions. For each precondition that is attached to an event matcher, the specificity of that its preconditions is added to that of the event matcher itself. An event matcher can have associated preconditions in two ways, which combine:
Required preconditions specified on the event matcher itself
Required preconditions specified on the macro level, which apply to all event matchers in the macro.
A precondition's specificity is determined in much the same way as that of an event matcher, but there is an additional complexity in determining the total specificity of all the preconditions that apply to an event matcher.
Overlap and consolidation
One could author a configuration file in which multiple preconditions are specified for an event matcher, but these conditions may have overlap with each other. In that case, summing the specificity of the preconditions would not be an accurate representation of how practically specific the conditions are. Take this example of two conditions:
They are, as you can see identical; so if one matches, the other one will always match too. If we naively add their specificities we get twice the specificity, whereas practically, it's just as specific as having only one of them. To get around this, we need to consolidate preconditions before calculating their specificity. In the above example that means removing the duplicate. In a more complex case it means consolidating the individual number matchers to the parts where they overlap. For example, take these two:
Since both preconditions must match, we look for the overlap. channel_match is identical so that can stay the same. program has number ranges that overlap, resulting in a NumberMatcher::Range { min: 21, max: 30 }, or the full consolidated Precondition:
Since NumberMatcher can be fairly complex, there will be some simplifying of NumberMatchers too, as well as specific algorithms to calculate a new NumberMatcher from the overlap between two of them, but describing the specifics of that is a bit out of scope of this issue.
A case where overlap is never possible is where the MidiPrecondition enum type differs, for example a MidiPrecondition::Program and MidiPrecondition::Control can never be consolidated.
After all the preconditions that apply to a single event matchers are consolidated as much as they can be, the total specificity of the event matcher + all its preconditions can be calculated. This processing can be done right after loading and parsing the config file. Perhaps it would be best to store the calculated specificity as a field in the EventMatcher struct.
Evaluating which macro to run when an event occurs
On an incoming event, iterate over all macros (which scopes that apply)'s event matchers, and collect ones that match.
Find the event matcher with the highest specificity
Execute the macro that matcher is a part of
Note: within each macro, if it has more than one event matcher, the matchers should be sorted in decreasing specificity, so that the first one that matches will be the one with highest specificity for that macro, meaning any further ones need not be evaluated.
Configuration
I think it would be best to have a top-level configuration flag enabling this behaviour, having default behaviour of going with "first matching event appearing in the config file". Name of this setting to be determined.
The text was updated successfully, but these errors were encountered:
By default, the first (in order of appearance in the config file) macro with an event matcher that matches an incoming event is executed, and then no further macros and their event matchers are evaluated. For more complex configurations, particularly involving preconditions, the concept of specificity becomes relevant to achieve the least surprising result.
The main idea of preferring more specific event matchers can be expressed as follows:
The matcher that matches the least permutations of events and conditions should be preferred over the one that matches more.
For a simple example, consider a MIDI note on event: On channel
3
, key20
is pressed with a velocity of80
:If we have configured the following 2 MIDI event matchers in separate macros:
We can analyze the two:
We can calculate how many different possibilities each of them can match:
1 * 30 * 128 = 3840
1 * 1 * 128 = 128
From this we can see that the event matcher in macro B matches far fewer possibilities; it is more specific. When preferring macros with more specific event matchers, macro B should be executed instead of macro A.
Determining specificity
The more specific something is, the higher its specificity number should be. In the above examples, the numbers presented work the other way around. For an event matcher without preconditions involved, the specificity number is
maxP - actualP
where:Preconditions
If an event matcher has one or more preconditions, it is more specific than an identical event matcher without preconditions. For each precondition that is attached to an event matcher, the specificity of that its preconditions is added to that of the event matcher itself. An event matcher can have associated preconditions in two ways, which combine:
A precondition's specificity is determined in much the same way as that of an event matcher, but there is an additional complexity in determining the total specificity of all the preconditions that apply to an event matcher.
Overlap and consolidation
One could author a configuration file in which multiple preconditions are specified for an event matcher, but these conditions may have overlap with each other. In that case, summing the specificity of the preconditions would not be an accurate representation of how practically specific the conditions are. Take this example of two conditions:
They are, as you can see identical; so if one matches, the other one will always match too. If we naively add their specificities we get twice the specificity, whereas practically, it's just as specific as having only one of them. To get around this, we need to consolidate preconditions before calculating their specificity. In the above example that means removing the duplicate. In a more complex case it means consolidating the individual number matchers to the parts where they overlap. For example, take these two:
Since both preconditions must match, we look for the overlap.
channel_match
is identical so that can stay the same.program
has number ranges that overlap, resulting in aNumberMatcher::Range { min: 21, max: 30 }
, or the full consolidatedPrecondition
:Since NumberMatcher can be fairly complex, there will be some simplifying of NumberMatchers too, as well as specific algorithms to calculate a new NumberMatcher from the overlap between two of them, but describing the specifics of that is a bit out of scope of this issue.
A case where overlap is never possible is where the MidiPrecondition enum type differs, for example a
MidiPrecondition::Program
andMidiPrecondition::Control
can never be consolidated.After all the preconditions that apply to a single event matchers are consolidated as much as they can be, the total specificity of the event matcher + all its preconditions can be calculated. This processing can be done right after loading and parsing the config file. Perhaps it would be best to store the calculated specificity as a field in the
EventMatcher
struct.Evaluating which macro to run when an event occurs
Note: within each macro, if it has more than one event matcher, the matchers should be sorted in decreasing specificity, so that the first one that matches will be the one with highest specificity for that macro, meaning any further ones need not be evaluated.
Configuration
I think it would be best to have a top-level configuration flag enabling this behaviour, having default behaviour of going with "first matching event appearing in the config file". Name of this setting to be determined.
The text was updated successfully, but these errors were encountered: