Post

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.

Bottom cover in SketchUp: bottom

Faceplate: top

Selection of some printed faceplates: faceplates

One of the first prototypes looked something like this: proto

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.

internals

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:

ready

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.

This post is licensed under CC BY 4.0 by the author.