DIY MIDI controller for M8
In my previous post I mentioned getting a Dirtywave M8. It’s pretty small and doesn’t have a lot of buttons, so working with the device needs a lot of key combinations to move through different screens and options. Getting the hang of the workflow is actually quite easy, but the interface is pretty limited if you want to perform or just jam with the device.
Fortunately it supports MIDI In so if I would have a MIDI controller I could use that instead to control the device. There are a lot of controller out there, but I have been looking for something that would be similar in size to the M8. I really like the grid controllers: they fit the profile pretty well and some people have been successfully using them with the M8.
Unfortunately the price point of the grid controllers is pretty steep so as usual I decided to build one myself.
Design
Initially I wanted to build something more complex. Something with a screen and a rotary encoder that can be used to implement a menu for the system. It wouldn’t be that crazy to build something like this, but after a few prototypes with SketchUp and a 3D printer I decided to reduce the scope a bit.
Prototyping with a 3D printer by just printing out the faceplate turned out to be very useful: you can really get the feel of the layout when you can actually play around with the device instead of just looking at a render.
Selection of some printed faceplates:
One of the first prototypes looked something like this:
After some experimentation I decided to simplify the device: remove the screen and have 12 knobs mapped to arbitrary undefined MIDI CC messages. To make it more interesting I added two buttons for automation:
- Hold down the left button and turn any knob to record the knob position over time
- After releasing the button, recorded MIDI CC information gets played back
- Push the other button to clear all automation
Build
After iterating with the case and printing out the final version (which still had a lot of issues actually, but it was good enough) I soldered all the components together and attached them to the case.
Hardware ready for testing, I started writing code. This was actually the easiest part after I managed to iron out all the hardware mistakes I made:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import time
import adafruit_midi
import board
import busio
import digitalio
import analogio
import usb_midi
from adafruit_midi.note_on import NoteOn
from adafruit_midi.note_off import NoteOff
from adafruit_midi.pitch_bend import PitchBend
from adafruit_midi.control_change import ControlChange
# Pin mappings
# Buttons: GP6, GP9
# Mux: Signal pin A0, address pins GP10, GP11, GP12, GP13
# Midi out UART GP8
# Setup hardware
mux_signal = analogio.AnalogIn(board.A0)
mux_s0 = digitalio.DigitalInOut(board.GP13)
mux_s0.direction = digitalio.Direction.OUTPUT
mux_s1 = digitalio.DigitalInOut(board.GP12)
mux_s1.direction = digitalio.Direction.OUTPUT
mux_s2 = digitalio.DigitalInOut(board.GP11)
mux_s2.direction = digitalio.Direction.OUTPUT
mux_s3 = digitalio.DigitalInOut(board.GP10)
mux_s3.direction = digitalio.Direction.OUTPUT
mux_address_pins = [mux_s3, mux_s2, mux_s1, mux_s0]
button1 = digitalio.DigitalInOut(board.GP6)
button1.direction = digitalio.Direction.INPUT
button1.pull = digitalio.Pull.UP
button2 = digitalio.DigitalInOut(board.GP9)
button2.direction = digitalio.Direction.INPUT
button2.pull = digitalio.Pull.UP
uart = busio.UART(board.GP8, None, baudrate=31250)
# Use both TRS and USB MIDI
midi = adafruit_midi.MIDI(midi_out=uart, out_channel=0)
umidi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=0)
# Setup state
# Pots were soldered to the mux very arbitrarily
pots = [0, 1, 2, 3, 5, 6, 7, 9, 10, 13, 14, 15]
# Every pot is mapped to an undefined CC message
pot_cc = [46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57]
# Initial and future states of each pot
pot_states = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
automation = []
automation_idx = 0
previous_automation = None
previous_automation_time = 0
def send_cc(pot, value):
midi.send(ControlChange(pot_cc[pot], value))
umidi.send(ControlChange(pot_cc[pot], value))
def read_pot_value(pot):
mask = "{0:b}".format(pot)
mask = "{:>04}".format(mask)
for idx, value in enumerate(mask):
mux_address_pins[idx].value = int(value)
return int(mux_signal.value)
def send_automation():
global automation_idx, previous_automation, previous_automation_time
now = time.monotonic()
# No automations or end of automation, start from beginning
if len(automation) == automation_idx:
automation_idx = 0
previous_automation = None
previous_automation_time = 0
return
current = automation[automation_idx]
previous_time = 0
if previous_automation != None:
previous_time = previous_automation["time"]
# Get the time difference between current and previously played automation
time_delay = current["time"] - previous_time
# Don't play back new automation until the same amount has passed as during the recording
if (now - previous_automation_time) > time_delay:
send_cc(current["pot"], current["value"])
previous_automation = current
previous_automation_time = now
automation_idx = automation_idx + 1
def record_automation(idx, value):
now = time.monotonic()
# Record timestamp with each value so we know the time difference between the changes
automation.append({"pot": idx, "value": value, "time": now})
while True:
# Reset automation
if not button1.value:
automation = []
automation_idx = 0
previous_automation = None
# Read all pot values
for idx, mux_pot in enumerate(pots):
raw_value = read_pot_value(mux_pot)
value = min(int(raw_value / 256), 127)
old_value = pot_states[idx]
# Try to ignore electrical fluctuations
if abs(value - old_value) > 2:
pot_states[idx] = value
# Record automation when button is pushed
if not button2.value:
record_automation(idx, value)
send_cc(idx, value)
# Only send automation when we are not recording
if button2.value:
send_automation()
Conclusion
This is what the device looks like:
It was a pretty fun project and a first physical device build that I’ve actually finished (I have started a lot of them!) where I have designed, built and coded everything myself.