GNU/Linux

Martin's
GNU/Linux Projects

To Home Page

Control (almost) anything using a MIDI controller

Last modifications: 15 January 2022

What is a MIDI controller?

From Wikipedia: A MIDI controller is any hardware or software that generates and transmits Musical Instrument Digital Interface (MIDI) data to MIDI-enabled devices, typically to trigger sounds and control parameters of an electronic music performance.

Examples of MIDI controllers are: electronic musical keyboards, electronic drum kits and control surfaces with faders, knobs and buttons. Note that these faders, knobs and buttons are also called controllers.

From a friend I got a MIDI control surface: the SubZero MiniControl. See the image below (click to enlarge).

SubZero MiniControl
SubZero MiniControl control surface

Normally you would use such a control surface in conjunction with some kind of DAW software. But you can control anything on your computer with a MIDI controller using the tool ALSA Sequencer Dump. This command line tool can show the MIDI data from a MIDI controller. And with some bash scripting you can do anything you like!

ALSA Sequencer Dump

The tool ALSA Sequencer Dump (aseqdump) is part of the package alsa-utils. If you do not have this package, install it.

How to list your MIDI controllers

In a terminal type:

$ aseqdump -l
 Port    Client name                      Port name
  0:0    System                           Timer
  0:1    System                           Announce
 14:0    Midi Through                     Midi Through Port-0
 20:0    USB MIDI Controller              USB MIDI Controller MIDI 1

The output above is the output on my computer. My SubZero MiniControl is shown as USB MIDI Controller.

How to show MIDI data from your controller

In a terminal type:

$ aseqdump -p "<client name>"
Waiting for data. Press Ctrl+C to end.
Source  Event                  Ch  Data

Now move some faders, turn some knobs and push some buttons. You will see something like this:

Waiting for data. Press Ctrl+C to end.
Source  Event                  Ch  Data
 20:0   Control change          0, controller 3, value 37
 20:0   Control change          0, controller 3, value 38
 20:0   Control change          0, controller 3, value 39
 20:0   Control change          0, controller 1, value 127
 20:0   Control change          0, controller 1, value 0
 20:0   Control change          0, controller 1, value 127
 20:0   Control change          0, controller 1, value 0

Each controller (fader, knob, button) has its own unique number. Play around and find out the number of each fader, knob and button.

A controller can have the following values:

  • Fader and knob: 0-127
  • Button pressed: 127, button released: 0

In the output example above, you can see that I operated the controller with number 3 and the controller with number 1. You can also see that controller 1 is a button and that I pressed it twice in a row.

Processing the MIDI data

An output line of aseqdump can be regarded as eight fields separated by comma's and spaces. We will store each field in a variable. Below the variables used for each field:

Header:     Source  Event               Ch  Data
Fields:     20:0    Control change      0,  controller 11,      value   32
Variables:  src     ev1     ev2         ch  label1     ctrl_no  label2  ctrl_value

To process the MIDI data, the output of aseqdump is piped to the read command. The read command reads one line at a time and the fields in a line are stored in the variables introduced above. The variables of interest are ctrl_no and ctrl_value. Variable ctrl_no contains the number of the controller and ctrl_value holds the controller value.

In order to separate the different fields, the internal field separator (IFS) must be specified before each read. Set IFS to comma and space (" ," or ", ").

How to process the MIDI data from your controller

Put the script below in a file and make the file executable. Execute the script in a terminal.

#!/bin/bash

aseqdump -p  "<client name>" |
{
  # Ignore first two output lines of aseqdump (info and header)
  read
  read
  while IFS=" ," read src ev1 ev2 ch label1 ctrl_no label2 ctrl_value rest
  do
    case $ctrl_no in
      1) echo "Controller one, value: $ctrl_value";;
      2) echo "Controller two, value: $ctrl_value";;
      3) echo "Controller three, value: $ctrl_value";;
      *) echo "Other controller";;
    esac
  done
}

After executing the script, operate the controllers 1, 2 and 3 and another one. You will see something like this:

Controller one, value: 127
Controller one, value: 0
Controller two, value: 127
Controller two, value: 0
Controller three, value: 55
Controller three, value: 56
Controller three, value: 57
Other controller
Other controller

Now that you know how to process the MIDI data of your controller, you can control whatever you like! Below I describe how I control Snapcast and the Music Player Daemon (MPD) using my SubZero MiniControl.

Controlling Snapcast and MPD

At home I created a multi-room audio system using Snapcast and the Music Player Daemon (MPD) (see my project: multi-room audio player). This multi-room audio system can be controlled by the SubZero MiniControl (or any other MIDI controller).

I hear you think: Is it very practical to control a multi-room audio system with a wired controller? The answer is: not really. But is it fun? I think it is!

Snapweb

With my MIDI controller I can control the Snapclients in the different rooms:

  • Set the audio volumes of each Snapclient.
  • Mute and unmute the audio of each Snapclient.
  • Set the latency of each Snapclient.

And with the media buttons on my MIDI controller I can control MPD:

  • Play, pause and stop my music.
  • Play previous and next song.
  • Load all songs in the queue.
  • Toggle random play.

To accomplish this I make use of the following:

  • ALSA Sequencer Dump (aseqdump) to read the MIDI data from my controller.
  • The JSON-RPC API of Snapcast to communicate with the Snapclients (communication goes via the Snapserver).
  • curl to send JSON requests to the Snapserver and receive responses over HTTP.
  • jq to filter the JSON responses and make them more readable.
  • Music Player Client (mpc) to control the Music Player Daemon.
  • A bash script to tie it all together.

Controller numbers of the SubZero MiniControl

Below you can see the controller numbers of the faders, knobs and buttons on the SubZero MiniControl. Click the image to enlarge.

SubZero MiniControl
SubZero MiniControl with controller numbers

It is strange that there are two controllers with the same number (10). Maybe a bug in the controller or a bug in aseqdump?

Assigning Snapclients to channels

The SubZero MiniControl has nine channels. A channel is just a set of controllers grouped together. On the SubZero MiniControl a channel consists of a button, a fader and a knob.

In my setup I have four Snapclients. The first four channels of the SubZero MiniControl are used to control these Snapclients. The fader controls the volume, the knob controls the latency and with the button you can mute and unmute the audio.

To control a Snapclient, you need its Snapclient ID. I put the Snapclient IDs in an array, so they can easily be assigned to controllers of adjacent channels:

ids[0]="34:17:eb:cc:89:f9" # Living room (channel 1)
ids[1]="f0:79:59:5a:ae:05" # Hobby room (channel 2)
ids[2]="18:31:bf:56:dc:d4" # Kitchen (channel 3)
ids[3]="e0:d4:e8:77:dc:97" # Laptop (channel 4)

(to find out the IDs of your Snapclients, run the script with the -c option)

Now take a look at the layout of the first four channels of the SubZero MiniControl:

Channels 1-4
Buttons, faders, knobs and their controller numbers
+------+------+------+------+
| K 14 | K 15 | K 16 | K 17 |  ids index = controller no - 14
+------+------+------+------+
| F 3  | F 4  | F 5  | F 6  |  ids index = controller no - 3
+------+------+------+------+
| B 23 | B 24 | B 25 | B 26 |  ids index = controller no - 23
+------+------+------+------+
|    1 |    2 |    3 |    4 |
+------+------+------+------+
 ids[0] ids[1] ids[2] ids[3]

K = knob / F = fader / B = button
1-4: channel number printed on the SubZero MiniControl
ids[]: Snapclient IDs assigned to channels

To assign Snapclient IDs to controllers of adjacent channels, I use the following mechanism in my script. When a button is pushed (controller 23-26), take the controller number and substract 23 to get the index of the array ids (containing the Snapclient IDs). If a fader is operated (controller 3-6), take the controller number and substract 3 to get the index of ids. And in case a knob is operated (controller 14-17), substract 14 to get the ids index.

The bash script

Take a look at the script snapcast-control.sh to see how it works. Or download the script and adapt it to your needs.