Git Product home page Git Product logo

mmpd's Introduction

mmpd

mmpd turns a MIDI keyboard or controller hooked up to your computer into a versatile macro pad. The aim is to assign behavior to keys and controllers, while differentiating based on the application that is currently focused.

Essentially, think of it as an additional keyboard that does custom things based on what application you're working with.

You can also set up "global" actions that work regardless of the application.

Initially written for use on Linux distributions using the X windowing system, it was structured to easily allow adding implementations for other platforms. At the moment, Linux with X windowing system, Windows, and Mac OS are supported. The Mac OS implementation is quite slow since it works through shell calls to osascript. It's an area that could do with improvement, perhaps through rust bindings to the CoreGraphics library.

Current status: tentatively ready for some use

What's implemented so far:

  • Detecting focused window (window class, name)
  • Connecting to a MIDI input device, receiving and parsing its messages
  • Data structures for describing:
    • Scopes (focused window matching)
      • With flexible string matching
    • Actions (to be run in response to MIDI events)
    • Event matchers (describes an event to matched to trigger an event)
      • Midi Event matcher with flexible parameter value matching options
    • Macros (combining scopes, event matchers, and actions into one package)
    • Preconditions (state that must be satisfied in addition to an event matching in order to execute a macro)
      • Midi preconditions for note_on, control, program, pitch_bend
  • Configuration: YAML parser to intermediary "RawConfig" format, plus a parser from RawConfig into the aforementioned data structures
  • Command line interfaces covering
    • Picking a config file or loading one from default location
    • list-midi-devices subcommand
    • monitor subcommand (to view incoming events without running macros)
    • (no subcommand) listening for events and running configured macros in response
  • Support for Linux (using X server), Windows, and Mac OS

There's documentation on the configuration format in docs/config.md including some future plans.

To do:

  • See Issues
  • Rewrite this readme with full guide on building / installing etc

Dependencies

Linux

  • xdotool (get it through your system's package manager)

    xdotool is needed for its library (libxdo).

Windows

Nothing specific I think, but see Installing rustup on Windows.

Mac OS

Nothing specific. Mac OS-specific parts interface with the system through AppleScript.

mmpd's People

Contributors

michd avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

mmpd's Issues

Variable substitution in Actions' string parameters

Allow using a %-surrounded syntax within strings to substitute other values, be it data from the event that triggered the action, or data retrieved from the state keeper.

The syntax is already described in docs/config.md (Link to version of the document at the latest commit on main at the time of writing), but I propose a few additions to the format.

Normal syntax: "My string here %varname%" if varname evaluates to a value of "MYVALUE" this results in the string "My string here MYVALUE". If there is no value available for the variable named varname, it is substituted with empty string, resulting in "My string here "

Specifying a fallback value: include a | character after the variable name and specify the fallback text. Example: "My string here %varname|MYFALLBACK%". In case varname exists and contains "MYVALUE", this results, as before, in "My string here MYVALUE". If varname does not evaluate to any value, this variant will result in "My string here MYFALLBACK".

Further diverging from the current config documentation, to insert a literal % in the string (and don't have it evaluated for variable replacement), use \%.

Variable naming

Variables can consist of multiple .-separated segments for namespacing. Within each segment name / variable name, the characters a-z, A-Z, 0-9, _ are valid.

Fallback values

In fallback values, you can use any character, but if you need a literal %, you must escape it: \%.


Data sources

Event data

Any field from the data object of the event that lead to this action being run may be used:

In case of a MIDI note_on event: event.channel, event.key, event.velocity. Note that values will be returned in their normal format, so if you specified an event matcher with a key of C3, that will return the number 48 here instead.

State data

Any known state data, such as state.midi.channels.3.notes_on.12 Exactly how these should be resolved is to be worked out. Perhaps even a * to match "any" could be helpful here.

Allow specifying multiple key sequences by space-separating them

Currently to do a bunch of key_sequence actions in a row, you need to create a separate action for each:

- type: key_sequence
  data: "ctrl+t"
- type: key_sequence
  data:
    sequence: "Tab"
    count: 3
- type: key_sequence
  data: "Return"

This could be streamlined if specifying more than one sequence, space-separated were supported (currently it errors out when trying to run it due to the invalid key sequence syntax).

The desirable format for the above example is:

- type: key_sequence
  data: "ctrl+t Tab Tab Tab Return"

Give EnterText and KeySequence Actions a delay parameter

When entering text or key sequences in some applications, that hard-coded 100 microsecond delay between key presses results in some things happening before appropriate UI is ready. This leads to an otherwise perfectly fine macro not executing reliably.

A good example of this is trying to enter a / -command in Slack. Typing the / character results in popup interface being opened, in which you then type the rest of the command, but this interface takes a while to open, resulting in the rest of the typed text going missing.

In other applications, the response might be more instantaneous, so per-action configuration of this key press delay would be very useful.

MIDI events/preconditions: allow specifying `key` as a string like "D3"

Currently, to specify a MIDI event involving a key, you must know the decimal MIDI note number of that key. While this can be found by using the monitor subcommand and pressing the key you're looking for, it would be easier to be able to specify by key name + octave number.

This would allow specifying an event as follows:

- type: midi
  data:
      message_type: note_on
      channel: 0
      key: D4

Which would be equivalent to how that is currently achieved:

- type: midi
  data:
      message_type: note_on
      channel: 0
      key: 64

Notes should be specified in uppercase. Both b and # notations should be supported for describing the black keys on a keyboard. Repeated b and # should not be supported.

Any specified string for key that does not resolve to a valid note in the 0-127 range or can otherwise not be parsed should cause a ConfigError::InvalidConfig error.

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

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.

Integrate getting focused window info without running shell commands

Currently the (sole, linux) implementation of FocusAdapter relies on using std::process::Command, calling through to the xdotool and xprop commands like one would run them in a terminal emulator.

It would be much more robust to use available libraries to achieve this, and leave std::process::Command out of that implementation altogether.

Create template config file if none is found

What happened

> ./mmpd 
Error: No config file found in:
        /home/mich/.config/mmpd/mmpd.yml
        /home/mich/.config/mmpd/mmpd.yaml

Either create one, or specify a config file with --config=<file>
Specify a midi device with --midi-device (part of it is enough)
Error: No matching MIDI device found.

What I expected

Software should create blank or template config file and directory on first run.

MIDI Preconditions

There is already a bunch of placeholder code and documentation around the concept of preconditions in macros and event matchers, but nothing practical is implemented yet.

Quick rundown of desired features for MIDI preconditions:

  • Keep track of currently pressed keys across all channels (as known through note_on and note_off messages)
  • Keep track of known control values across all channels (from control_change messages)
  • Keep track of known program values across all channels (from program_change messages)
  • Keep track of known pitch ben values across all channels (from pitch_bend_change messages)

Allow checking against all these values in preconditions.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.