MIDI for the Home Office

Using a MIDI controller to control my computer.

Tagged with: midi, scripting, windows

Published on and last updated on

I’m one of the many employees who work from home because of the currently ongoing COVID-19 pandemic. This is not my normal work environment and I had to make some adjustments to my workspace at home to make it work. One of the most fun and probably one of the most unnecessary ones was it to make a USB MIDI controller act as an input device for frequently used functions or hotkeys on my Windows work computer.

Fortunately, I had an old USB MIDI controller lying around. It is an AKAI LPD8 but any MIDI controller should work. This controller has eight pads and eight control dials. Additionally, the controller can switch between four programs which can change the signals of each input. Therefore it would be possible to configure 64 different functions. I currently use four pads and one dial.

MIDI controller AKAI LPD8

I had the idea to use this controller after my first day working from home. Three things annoyed me: controlling my music, activating push to talk in the work voice chat, and controlling the volume of my music player. At first, I just installed the software for the controller and tried to find out if this would be sufficient. But the software can only be used to change the MIDI signals (e.g. which pad is mapped to which note) that the controller emits. This didn’t stop me and I started to look into alternatives. Then I found AutoHotKey which is best described by this sentence on their website:

The ultimate automation scripting language for Windows.

It makes it possible to react to keypresses and execute complex actions. For example, it is possible to send key combinations, build simple GUIs, or execute programs. But there seemed to be one drawback. AutoHotKey normally does not understand MIDI signals. A bit more digging and I stumbled upon midi4ahk. This project provides a DLL and AutoHotKey scripts that can be linked in custom scripts to react to MIDI events.

Let’s take a quick look at how a MIDI message is structured. It consists of a status byte that describes the type of the message and sometimes the channel on which the message was sent. After the status byte are two additional bytes which are the parameters of the type. The three messages which were most important to me were “Note On”, “Note Off”, and “Control Change”. A “Note On” message has the status byte 1001 00002 or a bit shorter as hexadecimal number 9016. The first half of the status byte indicates the “Note On” type and the second half is the channel number. The parameters are the musical note (0-127) and the velocity (0-127) or how hard the button was pressed. The first bit of both bytes is always 0 therefore only 128 values are possible. The “Note Off” message is the same except for the status byte which is 8016. “Control Change” messages have the status byte B016. Similar to the note messages the first half of the byte indicates that the message is of the type “Control Change” and the second half is the channel number. This message type has as parameters the controller number (0-127) and the controller value (0-127).

Now I could finally start with the implementation. The library midi4ahk provides a function called listenNote which can be used to execute another function when a specific note is played. I wanted to use pad 8 of my controller to play or pause my music. The software of the MIDI controller showed me that this pad produces the note with the number 41 (F2). So I wrote the following script:

; 1. param note number
; 2. param function name
; 3. param MIDI channel (0 == all)
listenNote(41, "playPauseMusic", 0)
return

; 1. param note number
; 2. param velocity (0-127)
playPauseMusic(note, vel)
{
    if (vel)
    {
        Send, {Media_Play_Pause}
    }
    return
}

As expected it didn’t work when I tried it out. After some MIDI debugging with the excellent Pocket MIDI tool I found out the following. My MIDI controller first sends a “Note On” message with the velocity set according to how strong I press the pad. When I release the pad it sends a “Note Off” message with the velocity set to 127. My script does not differentiate between “Note On” and “Note Off” events. Hence it pauses the music for a short moment when receiving the “Note On” message and shortly after it starts the music again when the “Note Off” message is received. To overcome this and only react to a “Note On” event I used a small hack. I only send the play or pause signal when the velocity is above zero but below 127. Because “Note Off” events always have a velocity of 127 this works as long as I don’t smash the pad with full force. The following code snippet shows the final implementation of the playPauseMusic function.

playPauseMusic(note, vel)
{
    if (vel > 0 and vel < 127)
    {
        Send, {Media_Play_Pause}
    }
    return
}

And to demonstrate that it works here is a short video of me pressing the pad on the MIDI controller and my music player reacting to it by pausing the playback.

After implementing the pads to act as media control keys and to send a push to talk key combination I moved on to controlling the volume of my music player. When I’m working I often listen to music in the background but I also want to hear my colleagues talking in the voice chat. This makes me change the volume of my music player often and it takes a fair number of clicks to do it. The control dials on my MIDI controller seemed to be a perfect fit for the task at hand. When they are turned they emit a “Control Change” message with their position mapped from 0 to 127. All I thought I had to do was listen to “Control Change” messages with the listenCC function and then AutoHotKey will surely offer a function to change the volume of a program. The first part of it was true but AutoHotKey does not offer such functionality. I had to wander again into the depths of the internet to find an answer. This time I came back with the tool NirCmd. It is a command-line program that can be used to execute a set of tasks without any GUIs. One of those tasks is called setappvolume and it can be used to set the volume of a specific application to a value between 0 and 1. So I wrote the following script.

; 1. param control number
; 2. param function name
; 3. param MIDI channel (0 == all)
listenCC(4, "musicVolume", 0)
return

; 1. param control number
; 2. param control value
musicVolume(num, val)
{
    volume := ((100/127)*val)/100
    Run, nircmd.exe setappvolume MyMusicPlayer.exe %volume%
    return
}

I register the musicVolume function to be executed every time the control with the number 4 is activated. The musicVolume function receives the control number and the control value which is between 0 and 127 as inputs. In the function, I map the value to a number between 0 and 1 and execute the NirCmd setappvolume command with the name of my music player and the calculated volume. This did work but I saw some problems with the implementation. For each value sent by the control dial, this function is executed and the volume updated. The control dial sends a value for each position that is passed while turning it. This could lead to a lot of calls and wasted CPU usage. I monitored my CPU usage when I turned the control dial and each turn let the CPU usage climb 40% higher. Unfortunately, AutoHotKey has no automatic debounce utilities when functions are used so I had to implement a simple version myself as shown in the following snippet.

; 1. param control number
; 2. param control value
musicVolume(num, val)
{
    static lastCall := -1
    thisCall := A_TickCount
    if (lastCall = -1)
    {
        lastCall := thisCall
    }
    if ((thisCall - lastCall) < 16)
    {
        lastCall := thisCall
        return
    }
    lastCall := thisCall
    volume := ((100/127)*val)/100
    Run, nircmd.exe setappvolume MyMusicPlayer.exe %volume%
    return
}

To implement a simple debounce I used a static function variable which keeps its value across executions of a function and the built-in variable A_TickCount which contains the number of milliseconds since the system was started. By remembering when the function was called the last time I was able to only execute the NirCmd setappvolume command when the last function execution was longer than 16 milliseconds ago.

The following video shows me turning the control dial and the volume bar of my music player moving up and down in sync with it.

I highly recommend that you get your hands on a MIDI controller and use it to control your computer. It makes working from home feel like controlling a space ship 🚀.