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
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)
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
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)
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
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
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 note_buttons.append(note_pin)
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 octave_selector.append(sel_pin)
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
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
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]
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
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)
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))
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) midi.send(accWheel_pos) print(accelerator_pos) time.sleep(0.001) 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) print(accelerator_neg) time.sleep(0.001) else: if (abs(mod_val1 - mod_val2) > 2): mod_val2 = mod_val1 modulation = int(mod_val2) modWheel = ControlChange(1, modulation) midi.send(modWheel) print(modulation) time.sleep(0.001) if (abs(velocity_val1 - velocity_val2) > 2): velocity_val2 = velocity_val1 velocity = int(velocity_val2) time.sleep(0.001) if (abs(pitchbend_val1 - pitchbend_val2) > 75): pitchbend_val2 = pitchbend_val1 a_pitch_bend = PitchBend(int(pitchbend_val2)) midi.send(a_pitch_bend) print(int(pitchbend_val2)) time.sleep(0.001)
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.
midi.send(modWheel) time.sleep(0.001)
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]
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 print(octave[i]) up_pick = None down_pick = None time.sleep(0.001)
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) time.sleep(0.001)
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.
else: 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 print(octave[i]) time.sleep(0.001) if (buttons.value and note_states[i]): note_states[i] = False midi.send(NoteOff(octave[i], velocity)) time.sleep(0.001)
Page last edited March 08, 2024
Text editor powered by tinymce.