First, the CircuitPython libraries are imported.
import time import board import busio from adafruit_mcp230xx.mcp23017 import MCP23017 from digitalio import Direction import adafruit_ble from adafruit_ble.advertising.standard import ProvideServicesAdvertisement import adafruit_ble_midi # These import auto-register the message type with the MIDI machinery. # pylint: disable=unused-import import adafruit_midi from adafruit_midi.control_change import ControlChange from adafruit_midi.midi_message import MIDIUnknownEvent from adafruit_midi.note_off import NoteOff from adafruit_midi.note_on import NoteOn from adafruit_midi.pitch_bend import PitchBend
Next, I2C and the two MCP23017's are setup.
# i2c setup i2c = busio.I2C(board.SCL, board.SDA) # i2c addresses for muxes mcp1 = MCP23017(i2c, address=0x20) mcp2 = MCP23017(i2c, address=0x21)
This is followed by the arrays (noids0 and noids1) that will hold the solenoids that are connected to the two multiplexers. This allows them to be accessed as digital outputs.
# 1st solenoid array, corresponds with 1st mux
noids0 = []
for pin in range(16):
noids0.append(mcp1.get_pin(pin))
for n in noids0:
n.direction = Direction.OUTPUT
# 2nd solenoid array, corresponds with 2nd mux
noids1 = []
for pin in range(16):
noids1.append(mcp2.get_pin(pin))
for n in noids1:
n.direction = Direction.OUTPUT
Following the solenoid arrays are two arrays for the MIDI note numbers. notes0 will correspond with noids0 and notes1 will correspond with noids1. Later in the loop, these arrays will be used to match against incoming NoteOn MIDI messages.
# MIDI note arrays. notes0 = noids0; notes1 = noids1 notes0 = [55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70] notes1 = [71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86]
The BLE MIDI service is setup, followed by the BLE connection.
# setup MIDI BLE service
midi_service = adafruit_ble_midi.MIDIService()
advertisement = ProvideServicesAdvertisement(midi_service)
# BLE connection setup
ble = adafruit_ble.BLERadio()
if ble.connected:
for c in ble.connections:
c.disconnect()
midi is setup to be a MIDI-in device on MIDI channel 1. Channel 1 is defined as 0 in the CircuitPython MIDI library. A MIDI-in device is able to receive MIDI output from a digital audio workstation (DAW) or other MIDI communication method.
# MIDI in setup midi = adafruit_midi.MIDI(midi_in=midi_service, in_channel=0)
With everything setup, BLE can begin advertising for a connection.
# start BLE advertising
print("advertising")
ble.start_advertising(advertisement)
The last step before the loop is setting up speed to hold the delay between solenoids triggering and retracting. You can adjust this depending on your preferences.
# delay for solenoids speed = 0.01
The loop begins with the initial BLE connection. Once a connection is established, "Connected" will print to the REPL. This is followed by a delay that helps to stabilize the BLE MIDI communications before everything begins.
while True:
# waiting for BLE connection
print("Waiting for connection")
while not ble.connected:
pass
print("Connected")
# delay after connection established
time.sleep(1.0)
Once BLE is connected, msg is setup to hold the incoming MIDI messages.
while ble.connected:
# msg holds MIDI messages
msg = midi.receive()
The for statement allows for the ItsyBitsy to identify if an individual MIDI note number has been received and control individual solenoids attached to the multiplexers. notes0_played and notes1_played will handle the MIDI note numbers. noid0_output and noid1_output will handle the multiplexer outputs.
for i in range(16):
# states for solenoid on/off
# noid0 = mux1
# noid1 = mux2
noid0_output = noids0[i]
noid1_output = noids1[i]
# states for MIDI note recieved
# notes0 = mux1
# notes1 = mux2
notes0_played = notes0[i]
notes1_played = notes1[i]
The following if statements, nested in the previous for statement, are where the action is. The code is looking for a NoteOn message that contains one of the MIDI notes in either notes0_played or notes1_played. If there's a match, then the solenoid in the corresponding array index will trigger and retract with the predefined speed acting as the delay.
# if NoteOn msg comes in and the MIDI note
# matches with predefined notes:
if isinstance(msg, NoteOn) and msg.note is notes0_played:
print(time.monotonic(), msg.note)
# solenoid is triggered
noid0_output.value = True
# quick delay
time.sleep(speed)
# solenoid retracts
noid0_output.value = False
# identical to above if statement but for mux2
if isinstance(msg, NoteOn) and msg.note is notes1_played:
print(time.monotonic(), msg.note)
noid1_output.value = True
time.sleep(speed)
noid1_output.value = False
Why is it only looking for a NoteOn message? The way that mallet instruments work is that they have to be struck quickly in order for the note to resonate properly. If the code were waiting for a NoteOff message, then for longer note values the solenoid may not retract quickly enough to sound the note. That's why everything is relying on that initial NoteOn message.
The code ends by checking if BLE disconnects. If it does, then BLE will begin advertising again to reconnect and print to the REPL that it has disconnected.
# if BLE disconnects try reconnecting
print("Disconnected")
print()
ble.start_advertising(advertisement)
Page last edited March 08, 2024
Text editor powered by tinymce.