Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(Optionally) prefer Macros with more specific Event matchers and Preconditions #9

Open
michd opened this issue Apr 5, 2021 · 0 comments
Labels
enhancement New feature or request

Comments

@michd
Copy link
Owner

michd commented Apr 5, 2021

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 A
MidiEventMatcher::NoteOn {
    channel_match: Some(NumberMatcher::Val(3)),
    key_match: Some(NumberMatcher::Range { min: 11, max: 30 }),
    velocity_match: None
}
// event matcher in Macro B
MidiEventMatcher::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:

  1. Required preconditions specified on the event matcher itself
  2. 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:

MidiPrecondition::Program {
    channel_match: Some(NumberMatcher::Val(1)),
    program: Some(NumberMatcher::Range { max: 9 }
}

MidiPrecondition::Program {
    channel_match: Some(NumberMatcher::Val(1)),
    program: Some(NumberMatcher::Range { max: 9 }
}

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:

MidiPrecondition::Program {
    channel_match: Some(NumberMatcher::Val(1)),
    program: Some(NumberMatcher::Range { min: 11, max: 30 }
}

MidiPrecondition::Program {
    channel_match: Some(NumberMatcher::Val(1)),
    program: Some(NumberMatcher::Range { min: 21, max: 40 }
}

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:

MidiPrecondition::Program {
    channel_match: Some(NumberMatcher::Val(1)),
    program: Some(NumberMatcher::Range { min: 21, max: 30 }
}

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

  1. On an incoming event, iterate over all macros (which scopes that apply)'s event matchers, and collect ones that match.
  2. Find the event matcher with the highest specificity
  3. 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.

@michd michd added draft This issue isn't ready yet enhancement New feature or request and removed draft This issue isn't ready yet labels Apr 5, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

1 participant