midi-macros allows you to run scripts triggered by actions from a MIDI capable device. This effectively turns a MIDI controller (such as an AKAI MPK mini) into a Stream Deck. Types of actions include playing a note, playing a chord, playing a sequence of notes and chords, and other MIDI events such as turning a knob or moving a slider.
To somebody comfortable with scripting, a MIDI controller with midi-macros is far more flexible than devices like the Stream Deck since press velocity, analog controls (knobs or sliders), multi-key sequences, chords, channels, sustain, and basically anything else MIDI has to offer, can be used to better define macros. Arguments can even be passed into scripts to modify its function based on extra notes played, or other MIDI events like those genererated by turning a knob. This enables complex uses cases like changing volume and brightness where analog hardware controls do better than a keyboard or Stream Deck.
midi-macros allows for multiple MIDI devices to be used at once using device profiles, and device profiles can have subprofiles that have different sets of macros depending on your current use case. Device profiles also have a set of global macros that are always present regardless of which subprofile is selected.
Open xterm when middle C is pressed
C4 → xterm
# same as above but channel must be 0
C4{c==0} → xterm
Open xterm when a middle C major chord is played
C4+E4+G4 → xterm
# same as above but all notes must be played on channel 0
(C4+E4+G4){c==0} → xterm
# you can specify chords by surrounding a set of notes in brackets, seperating them with |
# this allows you to play C4, E4, G4 in any order
[C4|E4|G4] → xterm
# same as above but all notes must be played on channel 0
[C4|E4|G4]{c==0} → xterm
Here are some examples of how I use midi-macros:
Controlling volumes with knobs
# main volume with knob 1
MIDI{STATUS==cc}{CC_FUNCTION==70}("{}"→CC_VALUE_PERCENT) [BLOCK|DEBOUNCE]→ pactl set-sink-volume @DEFAULT_SINK@ {}%
# cmus volume with knob 2
MIDI{STATUS==cc}{CC_FUNCTION==71}("{}"→CC_VALUE_PERCENT) [BLOCK|DEBOUNCE]→ cmus-remote --volume {}%
# control focused application volume with knob 3
# this solution is built for use with sway
MIDI{STATUS==cc}{CC_FUNCTION==72}("{}"→f"{CC_VALUE_SCALED(0, 1)}") (python)[BLOCK|DEBOUNCE]→
{
import subprocess
focused_pid = int(
subprocess.check_output(
"swaymsg -t get_tree | jq '.. | select(.type?) | select(.focused==true).pid'",
text=True,
shell=True
)
)
import psutil
focused_process = psutil.Process(focused_pid)
process_hierarchy = {p.pid for p in focused_process.children(recursive=True)}
process_hierarchy.add(focused_pid)
import pulsectl
pid_property = "application.process.id"
with pulsectl.Pulse("mm-pulseaudio-client") as pulse:
for sink_input in pulse.sink_input_list():
sink_input_pid = sink_input.proplist.get(pid_property)
if sink_input_pid and int(sink_input_pid) in process_hierarchy:
pulse.volume_set_all_chans(sink_input, {})
break
}
Controlling cmus
40{c==9} → cmus-remote --pause
41{c==9} → cmus-remote --prev
42{c==9} → cmus-remote --next
43{c==9} → cmus-remote -C "toggle repeat_current"
# seek current song with knob
C3 MIDI{STATUS==cc}{CC_FUNCTION==72}("{}"→CC_VALUE) [BLOCK|DEBOUNCE]→
{
current_song_duration=$(cmus-remote -Q | grep duration | cut -d " " -f 2)
cmus-remote --seek $(python -c "print(round(({} / 127) * $current_song_duration))")
}
Controlling vlc
# fast-forward with pitch bend
# vlc sadly doesn't support negative playback rates yet, so no rewind
# this solution is built for use with sway and dbus
MIDI{STATUS==pb}{DATA_2>=64}("{}"→f"{lerp(((DATA_2 - 64) / 63), 1, 8)}") (python)[BLOCK|DEBOUNCE]→
{
import subprocess
focused_pid = int(
subprocess.check_output(
"swaymsg -t get_tree | jq '.. | select(.type?) | select(.focused==true).pid'",
text=True,
shell=True
)
)
import dbus
session_bus = dbus.SessionBus()
object_path = "/org/mpris/MediaPlayer2"
object_base_name = "org.mpris.MediaPlayer2.vlc"
focused_object_name = f"{object_base_name}.instance{focused_pid}"
object_name = focused_object_name if focused_object_name in session_bus.list_names() else object_base_name
vlc_object = session_bus.get_object(object_name, object_path)
vlc_object.Set("org.mpris.MediaPlayer2.Player", "Rate", {}, dbus_interface="org.freedesktop.DBus.Properties")
}
Controlling the brightness of a smart light with HomeAssistant
MIDI{STATUS==cc}{CC_FUNCTION==77}("{}"→f"{round(CC_VALUE_SCALED(0, 255))}") [BLOCK|DEBOUNCE]→
{
hass-cli service call --arguments "entity_id=light.color_lights,brightness={}" light.turn_on
}
Switching between subprofiles with rofi
48{c==9} →
{
profile="MPK mini"
subprofile=$(mm-msg profile "$profile" get-loaded-subprofiles | rofi -dmenu)
mm-msg profile "$profile" set-subprofile "$subprofile"
}