The code begins by importing the libraries.

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

MIDI Setup

After the libraries, midi is declared to bring in the adafruit_midi library. It declares that the USB port on the Grand Central board will be transmitting the MIDI data.

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

LIS3DH Accelerometer Setup

Next the LIS3DH accelerometer is setup to be controlled via I2C, with its range being 2G.

i2c = busio.I2C(board.SCL, board.SDA)
lis3dh = adafruit_lis3dh.LIS3DH_I2C(i2c)

lis3dh.range = adafruit_lis3dh.RANGE_2_G

Analog Inputs

The potentiometers for modulation and velocity control.

The three potentiometers are then brought in to control three different MIDI parameters: pitch bend, modulation and velocity. The modulation and velocity potentiometers are located on the body of the MIDI guitar and the pitch bend potentiometer is in the whammy bar assembly.

pitchbend_pot = AnalogIn(board.A1)
mod_pot = AnalogIn(board.A2)
velocity_pot = AnalogIn(board.A3)
Whammy bar. The pitchbend_pot is inside this 3D printed assembly.

Mode Switches

This is followed by two switches, which will switch between two different modes. The first is for modulation control. It will allow you to switch between either using the potentiometer to control modulation or the LIS3DH, which will affect the modulation based on the physical position of the MIDI guitar.

The second switch is for strum mode. Since this is a MIDI guitar, you can strum the notes using the strummer, which will activate the switches inside the mechanism. However, this might not always be ideal. By using this switch you can turn off the strummer and just hit the note buttons on the neck of the MIDI guitar to play it like a traditional MIDI keyboard.

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
The two mode switches.

Strumming Switches

Finally, the two switch inputs for the strumming mechanism are setup. There are two switches inside the strummer mechanism. They're activated when you strum up and down with the strummer.

strumUP = DigitalInOut(board.D22)
strumUP.direction = Direction.INPUT
strumUP.pull = Pull.UP

strumDOWN = DigitalInOut(board.D23)
strumDOWN.direction = Direction.INPUT
strumDOWN.pull = Pull.UP
The strumming mechanism. Two switches are inside.

Guitar Neck Buttons

Now two arrays of inputs are brought in. The first is for the note inputs, or buttons that are on the neck of the guitar. There are 12 of them.

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
The Cherry MX Keys on the neck of the guitar.

Octave Selector

This is followed by the inputs for the rotary switch. The switch has 8 inputs and as a result 8 pins are set up. The switch will allow you to change octaves for the notes that you can play. This expands the range of the MIDI guitar by quite a bit rather than being stuck with just the 12 notes setup with the buttons.

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
The rotary switch. It is located next to the two mode switches.

Digital Input States

After the peripherals have been setup, the state machines can follow. There are 12 states setup for the 12 different notes to help with debouncing. Their names correspond with the note name that will be assigned to each button.

There are also two states, strummed and pick, that will be used to track the state of the strummer.

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

strummed = None
pick = None

Analog Input States

There are some variables setup to help track the positions of the potentiometers and the LIS3DH. These will be used to compare to the previous value reading to get the current reading for each input.

pitchbend_val2 = 0
mod_val2 = 0
velocity_val2 = 0
acc_pos_val2 = 0
acc_neg_val2 = 0

Note Input Arrays

The 12 states that were just set up for the note inputs are put into an array called note_states. This array will be able to match with the note_buttons array in the loop.

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]

This is followed by an array of the MIDI note numbers called note_numbers. Next is a list of the note names that correspond with those numbers. The list is stored in parenthesis and is set to equal note_numbers. Those note names are now variables that are holding the note numbers. This works since the names are in the same order as the numbers. This lets you easily type in the note names when creating your note arrays that will be tied to the buttons on the guitar neck. It tends to make more sense to look at note names than what can appear to be random 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]

(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

Up next there are 8 arrays containing 12 note names each. These variables are being imported from the list above. Each note name corresponds to a MIDI note number. When these numbers are sent along with a NoteOn message in MIDI then the proper pitch is played.

These arrays are for the different octaves that can be selected using the rotary switch. Each array begins with an E natural note. This was chosen because the lowest string on a guitar in standard tuning is tuned to E natural. The notes are chromatic and since there are 12 (one for each button) you're just one note shy of a full octave.

You can also customize the notes that correspond to each button by editing the arrays.

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]

These arrays are then put into the octave_select array. This allows for the rotary switch to select the different note ranges.

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]

Reading Analog Inputs

This is followed by a fairly standard function when you work with analog inputs in CircuitPython. It allows you to read the value of an analog input continuously.

def val(pin):
    return pin.value

Beginning REPL Message

Finally, right before the loop, there are some debugging strings that are printed out to the REPL to let you know which MIDI channel output is active.

print("MIDI Guitar")

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

The Loop

Mapping Analog Values to MIDI Parameter Values

At the beginning of the loop, x, y and z are defined to hold the values of the accelerometer.

Following that, there are five variables assigned to hold analog values of the three potentiometers and two accelerometer readings. The values for the potentiometers are being brought in with the val() function setup before the loop. The map_range() function is being used to convert the analog voltage value range to values that match with the MIDI CC ranges being controlled by the potentiometers and accelerometer.

The accelerometer setup is a little different than the potentiometers. The goal for the accelerometer is to control modulation. When the accelerometer is in a centralized, or neutral, position the modulation will be set to 0. However, when you tilt the guitar up or down the modulation amount will increase. That's why there are mapped values for negative and positive ranges.

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

    pitchbend_val1 = round(simpleio.map_range(val(tonebend_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))
You may need to experiment with your accelerometer value ranges depending on your preferences.

Sending MIDI Data with Analog Data

The rest of the loop defines what MIDI messages are sent as a result of using all of the different control interfaces. These are stored in a series of if statements.

The first if statement is checking the value of the mod_select switch. The mod_select determines whether modulation is being controlled by the potentiometer (mod_pot) or the accelerometer. For the accelerometer, a neutral position sends a value of 0 for modulation and then if you tilt the MIDI guitar up or down the value increases. The potentiometer allows you to set an unchanging value for modulation.

This is followed by an if statement for the potentiometer that controls the velocity value. Velocity in MIDI is the volume level that a note is recorded at. You won't necessarily notice it while you're actively playing (depending on the patch), but on playing back MIDI data it can make the music sound more human if there's variation in the levels.

The last analog potentiometer controls PitchBend and lives in the whammy bar mechanism. This parameter allows you to adjust the pitch up or down on a note that you’re playing.

if not mod_select.value:
        if (abs(acc_pos_val1 - acc_pos_val2) < 50):
            acc_pos_val2 = acc_pos_val1
            accelerator_pos = int(acc_pos_val2)
            accWheel_pos = ControlChange(1, accelerator_pos)
        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)
        if (abs(mod_val1 - mod_val2) > 2):
            mod_val2 = mod_val1
            modulation = int(mod_val2)
            modWheel = ControlChange(1, modulation)
if (abs(velocity_val1 - velocity_val2) > 2):
        velocity_val2 = velocity_val1
        velocity = int(velocity_val2)

if (abs(pitchbend_val1 - pitchbend_val2) > 75):
        pitchbend_val2 = pitchbend_val1
        a_pitch_bend = PitchBend(int(pitchbend_val2))

How the MIDI Messages Are Being Sent

All of these if statements are using the same code structure to translate these analog values to MIDI values that can be successfully sent to your MIDI software. First, it checks to see if the value of the input has changed since the last time it was checked. If it has, it checks to see if it has surpassed the threshold, which varies depending on the range of the input and MIDI parameter.

if (abs(mod_val1 - mod_val2) > 2):

If the value has changed enough, then the current value is logged and a new variable is setup to hold that value as an integer. MIDI message values need to be integers in order to be sent properly.

mod_val2 = mod_val1
modulation = int(mod_val2)

Another variable is setup to hold the MIDI message, which includes the type of message, MIDI channel and the value as an integer.

modWheel = ControlChange(1, modulation)

The MIDI message is finally sent with midi.send(). This is followed by a quick delay and that completes the process.


Octave Selection

After the analog inputs, the code moves on to the digital inputs. This involves the rotary switch, the MX switches on the neck of the MIDI guitar and the two switches in the strummer.

This section begins by checking the position of the rotary switch to determine which octave is active. This is stored as octave and is called later when the NoteOn MIDI message is sent.

for s in octave_selector:
        if not s.value:
            o = octave_selector.index(s)
            octave = octave_select[o]

Strum Mode

Next, the strum_select switch's position is checked. This switch determines whether you are strumming your MIDI guitar or just pressing the notes on the guitar's neck like a keyboard.

After checking if strum mode is active, the states of both switches located in the strum bar mechanism are checked. This takes care of debouncing the switches. There is also a variable called pick (named for a guitar pick) that is holding time.monotonic().

There is an if statement after the strummer switch state checks. It prevents the strummer from a glitch. If a note is not pressed down when you hit the strum bar, then the code is waiting for a note to be pressed on the neck. If you hit the strummer and then a few minutes later hit a note, it will sound. By having this line, the state of the two switches is reset to its initial none state in the event that this happens.

if not strum_select.value:
  if strumUP.value and up_pick == None:
    up_pick = "strummed"
    pick = time.monotonic()
  if strumDOWN.value and down_pick == None:
    down_pick = "strummed"
    pick = time.monotonic()
  if (not pick) or ((time.monotonic() - pick) > 0.5 and (down_pick or up_pick == "strummed")):
    up_pick = None
    down_pick = None

After the strumming is all setup, the code checks to see if the strummer has been strummed in either direction. If it has, then the 12 buttons on the neck are brought in with i being the index that identifies which button has been pressed. The button's values and states are then checked.

If the button has been pressed, then the NoteOn MIDI message is sent. That message includes the matching octave's note and the velocity value. The note's state and strummed status are updated and everything is closed out by a brief delay.

if (not strumUP.value and up_pick == "strummed") or (not strumDOWN.value and down_pick == "strummed"):
  for i in range(12):
    buttons = note_buttons[i]
    if not buttons.value and not note_states[i]:
      midi.send(NoteOn(octave[i], velocity))
      note_states[i] = True
      up_pick = None
      down_pick = None

With MIDI, if you play a note you also need to tell it to stop playing. Otherwise, it will go on forever. This next portion of code does just that. It checks to see if a note button is no longer being pressed. If that is true, then the note and strum states are set to their defaults and the NoteOff MIDI message is sent.

for i in range(12):
  buttons = note_buttons[i]
  if buttons.value and note_states[i]:
    note_states[i] = False
    strummed = None
    midi.send(NoteOff(octave[i], velocity)

Tapping Mode (No Strumming Required)

A wild else appears! If you scroll up a bit, you'll see that this else is the partner to the if not strum_select.value statement. This else statement allows you to play MIDI notes on this guitar without using the strum bar. You can just press the buttons on the guitar's neck just like a MIDI keyboard.

You'll see that the code matches our previous code to allow for NoteOn and NoteOff messages. The only thing missing is the portions about the strum bar.

  for i in range(12):
    buttons = note_buttons[i]
    if not buttons.value and not note_states[i]:
      midi.send(NoteOn(octave[i], velocity))
      note_states[i] = True
    if (buttons.value and note_states[i]):
      note_states[i] = False
      midi.send(NoteOff(octave[i], velocity))

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

This page (Code Walkthrough) was last updated on Mar 08, 2024.

Text editor powered by tinymce.