Take your Grand Central M4 Express board and plug it into your computer via a known good data + power USB cable. Your operating system will show a drive named CIRCUITPY when a board is plugged in. If you get a drive named GCM4BOOT, you'll likely need to install CircuitPython. This is easy, see the steps below to do this, get library files, and copy the code to your board.

CircuitPython Setup for the Grand Central M4 Express

First, to make sure you're running the most recent version of CircuitPython for the Grand Central M4 Express.

Grand Central M4 Express Libraries

You'll need a few CircuitPython libraries in the lib folder on the Grand Central M4 Express CIRCUITPY drive for the code to work. Head to https://circuitpython.org/libraries to download the latest library bundle matching the major version of CircuitPython now on your board (5 for CircuitPython 5.x, etc.). The procedure is available in the Grand Central M4 Express guide.

Once you've downloaded the libraries bundle, add these libraries to the lib folder on the Grand Central:

  • adafruit_bus_device
  • adafruit_lis3dh.mpy
  • adafruit_midi
  • simpleio.mpy

Your Grand Central CIRCUITPY drive should look like this after you load the code below:

Download the Code from GitHub

Once your Grand Central M4 Express is all setup with CircuitPython and the necessary libraries, you can click on the Download: Project Zip link above the code to get the code file.

import time
import board
import simpleio
import busio
import adafruit_lis3dh
import digitalio
from digitalio import DigitalInOut, Direction, Pull
from analogio import AnalogIn
import usb_midi
import adafruit_midi
from adafruit_midi.note_on          import NoteOn
from adafruit_midi.note_off         import NoteOff
from adafruit_midi.control_change   import ControlChange
from adafruit_midi.pitch_bend       import PitchBend

#  imports MIDI
midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=0)

#  setup for LIS3DH accelerometer
i2c = busio.I2C(board.SCL, board.SDA)
lis3dh = adafruit_lis3dh.LIS3DH_I2C(i2c)

lis3dh.range = adafruit_lis3dh.RANGE_2_G

#  setup for 3 potentiometers
pitchbend_pot = AnalogIn(board.A1)
mod_pot = AnalogIn(board.A2)
velocity_pot = AnalogIn(board.A3)

#  setup for two switches that will switch modes
mod_select = DigitalInOut(board.D52)
mod_select.direction = Direction.INPUT
mod_select.pull = Pull.UP

strum_select = DigitalInOut(board.D53)
strum_select.direction = Direction.INPUT
strum_select.pull = Pull.UP

#  setup for strummer switches
strumUP = DigitalInOut(board.D22)
strumUP.direction = Direction.INPUT
strumUP.pull = Pull.UP

strumDOWN = DigitalInOut(board.D23)
strumDOWN.direction = Direction.INPUT
strumDOWN.pull = Pull.UP

#  setup for cherry mx switches on neck
note_pins = [board.D14, board.D2, board.D3, board.D4, board.D5,
             board.D6, board.D7, board.D8, board.D9, board.D10, board.D11, board.D12]

note_buttons = []

for pin in note_pins:
    note_pin = digitalio.DigitalInOut(pin)
    note_pin.direction = digitalio.Direction.INPUT
    note_pin.pull = digitalio.Pull.UP
    note_buttons.append(note_pin)

#  setup for rotary switch
oct_sel_pins = [board.D24, board.D25, board.D26, board.D27, board.D28, board.D29,
                board.D30, board.D31]

octave_selector = []

for pin in oct_sel_pins:
    sel_pin = digitalio.DigitalInOut(pin)
    sel_pin.direction = digitalio.Direction.INPUT
    sel_pin.pull = digitalio.Pull.UP
    octave_selector.append(sel_pin)

#  cherry mx switch states
note_e_pressed = None
note_f_pressed = None
note_fsharp_pressed = None
note_g_pressed = None
note_gsharp_pressed = None
note_a_pressed = None
note_asharp_pressed = None
note_b_pressed = None
note_c_pressed = None
note_csharp_pressed = None
note_d_pressed = None
note_dsharp_pressed = None

#  state machines
strummed = None
pick = None
up_pick = None
down_pick = None

#  states for analog inputs
pitchbend_val2 = 0
mod_val2 = 0
velocity_val2 = 0
acc_pos_val2 = 0
acc_neg_val2 = 0

#  array for cherry mx switch states
note_states = [note_e_pressed, note_f_pressed, note_fsharp_pressed, note_g_pressed,
               note_gsharp_pressed, note_a_pressed, note_asharp_pressed, note_b_pressed,
               note_c_pressed, note_csharp_pressed, note_d_pressed, note_dsharp_pressed]

#  array of MIDI note numbers
note_numbers = [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, 120, 123, 124, 125, 126, 127]

#  list of note name variables that are assigned to the note_numbers array
#  this allows you to use the note names rather than numbers when assigning
#  them to the cherry mx switches
(A0, Bb0, B0, C1, Db1, D1, Eb1, E1, F1, Gb1, G1, Ab1,
 A1, Bb1, B1, C2, Db2, D2, Eb2, E2, F2, Gb2, G2, Ab2,
 A2, Bb2, B2, C3, Db3, D3, Eb3, E3, F3, Gb3, G3, Ab3,
 A3, Bb3, B3, C4, Db4, D4, Eb4, E4, F4, Gb4, G4, Ab4,
 A4, Bb4, B4, C5, Db5, D5, Eb5, E5, F5, Gb5, G5, Ab5,
 A5, Bb5, B5, C6, Db6, D6, Eb6, E6, F6, Gb6, G6, Ab6,
 A6, Bb6, B6, C7, Db7, D7, Eb7, E7, F7, Gb7, G7, Ab7,
 A7, Bb7, B7, C8, Db8, D8, Eb8, E8, F8, Gb8, G8, Ab8,
 A8, Bb8, B8, C9, Db9, D9, Eb9, E9, F9, Gb9, G9) = note_numbers

#  arrays for note inputs that are tied to the rotary switch
octave_8_cc = [E8, F8, Gb8, G8, Ab8, A8, Bb8, B8, C9, Db9, D9, Eb9]
octave_7_cc = [E7, F7, Gb7, G7, Ab7, A7, Bb7, B7, C8, Db8, D8, Eb8]
octave_6_cc = [E6, F6, Gb6, G6, Ab6, A6, Bb6, B6, C7, Db7, D7, Eb7]
octave_5_cc = [E5, F5, Gb5, G5, Ab5, A5, Bb5, B5, C6, Db6, D6, Eb6]
octave_4_cc = [E4, F4, Gb4, G4, Ab4, A4, Bb4, B4, C5, Db5, D5, Eb5]
octave_3_cc = [E3, F3, Gb3, G3, Ab3, A3, Bb3, B3, C4, Db4, D4, Eb4]
octave_2_cc = [E2, F2, Gb2, G2, Ab2, A2, Bb2, B2, C3, Db3, D3, Eb3]
octave_1_cc = [E1, F1, Gb1, G1, Ab1, A1, Bb1, B1, C2, Db2, D2, Eb2]

octave_select = [octave_1_cc, octave_2_cc, octave_3_cc, octave_4_cc,
                 octave_5_cc, octave_6_cc, octave_7_cc, octave_8_cc]

#  function for reading analog inputs
def val(voltage):
    return voltage.value

#  beginning script REPL printout
print("MX MIDI Guitar")

print("Default output MIDI channel:", midi.out_channel + 1)

#  loop
while True:
    #  values for LIS3DH
    x, y, z = [value / adafruit_lis3dh.STANDARD_GRAVITY for value in lis3dh.acceleration]

    #  mapping analog values to MIDI value ranges
    #  PitchBend MIDI has a range of 0 to 16383
    #  all others used here are 0 to 127
    pitchbend_val1 = round(simpleio.map_range(val(pitchbend_pot), 0, 65535, 0, 16383))
    mod_val1 = round(simpleio.map_range(val(mod_pot), 0, 65535, 0, 127))
    velocity_val1 = round(simpleio.map_range(val(velocity_pot), 0, 65535, 0, 127))
    acc_pos_val1 = round(simpleio.map_range(x, 0, 0.650, 127, 0))
    acc_neg_val1 = round(simpleio.map_range(y, -0.925, 0, 127, 0))

    #  checks if modulation switch is engaged
    if not mod_select.value:
        #  if it is, then get modulation MIDI data from LIS3DH
        #  positive and negative values for LIS3DH depending on
        #  orientation of the guitar neck
        #  when the guitar is held "normally" aka horizontal
        #  then the modulation value is neutral aka 0

        #  compares previous LIS3DH value to current value
        if abs(acc_pos_val1 - acc_pos_val2) < 50:
            #  updates previous value to hold current value
            acc_pos_val2 = acc_pos_val1
            #  MIDI data has to be sent as an integer
            #  this converts the LIS3DH data into an int
            accelerator_pos = int(acc_pos_val2)
            #  int is stored as a CC message
            accWheel_pos = ControlChange(1, accelerator_pos)
            #  CC message is sent
            midi.send(accWheel_pos)
            #  delay to settle MIDI data
            time.sleep(0.001)

        #  same code but for negative values
        elif abs(acc_neg_val1 - acc_neg_val2) < 50:
            acc_neg_val2 = acc_neg_val1
            accelerator_neg = int(acc_neg_val2)
            accWheel_neg = ControlChange(1, accelerator_neg)
            midi.send(accWheel_neg)
            time.sleep(0.001)

    #  if it isn't then get modulation MIDI data from pot
    else:
        #  compares previous mod_pot value to current value
        if abs(mod_val1 - mod_val2) > 2:
            #  updates previous value to hold current value
            mod_val2 = mod_val1
            #  MIDI data has to be sent as an integer
            #  this converts the pot data into an int
            modulation = int(mod_val2)
            #  int is stored as a CC message
            modWheel = ControlChange(1, modulation)
            #  CC message is sent
            midi.send(modWheel)
            #  delay to settle MIDI data
            time.sleep(0.001)

    #  reads analog input to send MIDI data for Velocity
    #  compares previous velocity pot value to current value
    if abs(velocity_val1 - velocity_val2) > 2:
        #  updates previous value to hold current value
        velocity_val2 = velocity_val1
        #  MIDI data has to be sent as an integer
        #  this converts the pot data into an int
        #  velocity data is sent with NoteOn message
        #  NoteOn is sent in the loop
        velocity = int(velocity_val2)
        #  delay to settle MIDI data
        time.sleep(0.001)

    #  reads analog input to send MIDI data for PitchBend
    #  compares previous picthbend pot value to current value
    if abs(pitchbend_val1 - pitchbend_val2) > 75:
        #  updates previous value to hold current value
        pitchbend_val2 = pitchbend_val1
        #  MIDI data has to be sent as an integer
        #  this converts the pot data into an int
        #  int is stored as a PitchBend message
        a_pitch_bend = PitchBend(int(pitchbend_val2))
        #  PitchBend message is sent
        midi.send(a_pitch_bend)
        #  delay to settle MIDI data
        time.sleep(0.001)

    #  checks position of the rotary switch
    #  determines which notes are mapped to the cherry mx switches
    for s in octave_selector:
        if not s.value:
            o = octave_selector.index(s)
            octave = octave_select[o]

        #  checks if strum select switch is engaged
        if not strum_select.value:
            #  if it is, then:
            #  setup states for both strummer switches
            if strumUP.value and up_pick is None:
                up_pick = "strummed"
                pick = time.monotonic()
            if strumDOWN.value and down_pick is None:
                down_pick = "strummed"
                pick = time.monotonic()
            #  bug fix using time.monotonic(): if you hit the strummer, but don't hit a note
            #  the state of the strummer switch is reset
            if (not pick) or ((time.monotonic() - pick) > 0.5 and
                              (down_pick or up_pick == "strummed")):
                up_pick = None
                down_pick = None

            #  if either strummer switch is hit
            if (not strumUP.value and up_pick == "strummed") or (not strumDOWN.value
                                                                 and down_pick == "strummed"):
                #  indexes the cherry mx switch array
                for i in range(12):
                    buttons = note_buttons[i]
                    #  if any of the mx cherry switches are pressed
                    #  and they weren't previously pressed (checking note_states[i])
                    #  where i is the matching index from the note_buttons array
                    if not buttons.value and not note_states[i]:
                        #  send the NoteOn message that matches with the octave[i] array
                        #  along with the velocity value
                        midi.send(NoteOn(octave[i], velocity))
                        #  note number is printed to REPL
                        print(octave[i])
                        #  note state is updated
                        note_states[i] = True
                        #  updates strummer switch states
                        up_pick = None
                        down_pick = None
                        #  delay to settle MIDI data
                        time.sleep(0.001)

            #  the next if statement allows for you to strum notes multiple times without
            #  having to release the note

            #  if either strummer switch is hit
            if (not strumUP.value and up_pick == "strummed") or (not strumDOWN.value
                                                                 and down_pick == "strummed"):
                #  indexes the cherry mx switch array
                for i in range(12):
                    buttons = note_buttons[i]
                    #  if any of the cherry mx switches are pressed
                    #  and they *were* previously pressed (checking note_states[i])
                    #  where i is the matching index from the note_buttons array
                    if not buttons.value and note_states[i]:
                        #  send the NoteOn message that matches with the octave[i] array
                        #  along with the velocity value
                        midi.send(NoteOn(octave[i], velocity))
                        #  note number is printed to REPL
                        print(octave[i])
                        #  note state is updated
                        note_states[i] = True
                        #  updates strummer switch states
                        up_pick = None
                        down_pick = None
                        #  sends a NoteOff message to prevent notes from
                        #  staying on forever aka preventing glitches
                        midi.send(NoteOff(octave[i], velocity))
                        #  delay to settle MIDI data
                        time.sleep(0.001)

            #  the next for statement sends NoteOff when the cherry mx switches
            #  are released

            #  indexes the cherry mx switch array
            for i in range(12):
                buttons = note_buttons[i]
                #  if any of the cherry mx switches are released
                #  and they *were* previously pressed (checking note_states[i])
                #  where i is the matching index from the note_buttons array
                if buttons.value and note_states[i]:
                    #  send the NoteOff message that matches with the octave[i] array
                    #  along with the velocity value
                    midi.send(NoteOff(octave[i], velocity))
                    #  note state is updated
                    note_states[i] = False
                    #  updates strummer switch states
                    down_pick = None
                    up_pick = None
                    #  delay to settle MIDI data
                    time.sleep(0.001)

        #  if strum select switch is not engaged

        else:
            #  indexes the cherry mx switch array
            for i in range(12):
                buttons = note_buttons[i]
                #  if any of the mx cherry switches are pressed
                #  and they weren't previously pressed (checking note_states[i])
                #  where i is the matching index from the note_buttons array
                if not buttons.value and not note_states[i]:
                    #  send the NoteOn message that matches with the octave[i] array
                    #  along with the velocity value
                    midi.send(NoteOn(octave[i], velocity))
                    #  note number is printed to REPL
                    print(octave[i])
                    #  note state is updated
                    note_states[i] = True
                    #  delay to settle MIDI data
                    time.sleep(0.001)

                #  if any of the cherry mx switches are released
                #  and they *were* previously pressed (checking note_states[i])
                #  where i is the matching index from the note_buttons array
                if (buttons.value and note_states[i]):
                    #  send the NoteOff message that matches with the octave[i] array
                    #  along with the velocity value
                    midi.send(NoteOff(octave[i], velocity))
                    #  note state is updated
                    note_states[i] = False
                    #  delay to settle MIDI data
                    time.sleep(0.001)

    #  delay to settle MIDI data
    time.sleep(0.005)

This guide was first published on Mar 18, 2020. It was last updated on Mar 18, 2020.

This page (CircuitPython on the Grand Central M4 Express) was last updated on Sep 09, 2020.