Libraries

First, the CircuitPython libraries are imported.

Download: file
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

I2C Setup

Next, I2C and the two MCP23017's are setup.

Download: file
#  i2c setup
i2c = busio.I2C(board.SCL, board.SDA)

#  i2c addresses for muxes
mcp1 = MCP23017(i2c, address=0x20)
mcp2 = MCP23017(i2c, address=0x21)

Solenoids

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.

Download: file
#  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

MIDI Note Numbers

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.

Download: file
#  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]

BLE Setup

The BLE MIDI service is setup, followed by the BLE connection.

Download: file
#  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 Setup

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.

Download: file
#  MIDI in setup
midi = adafruit_midi.MIDI(midi_in=midi_service, in_channel=0)

Begin Advertising

With everything setup, BLE can begin advertising for a connection.

Download: file
#  start BLE advertising
print("advertising")
ble.start_advertising(advertisement)

Solenoid Delay

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.

Download: file
#  delay for solenoids
speed = 0.01

The Loop

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.

Download: file
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)

MIDI Messages

Once BLE is connected, msg is setup to hold the incoming MIDI messages.

Download: file
while ble.connected:

        # msg holds MIDI messages
        msg = midi.receive()

Arrays of Arrays

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.

Download: file
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]

Let the Solenoids Play!

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.

Download: file
# 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.

Reconnect BLE

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.

Download: file
#  if BLE disconnects try reconnecting
    print("Disconnected")
    print()
    ble.start_advertising(advertisement)

This guide was first published on May 27, 2020. It was last updated on May 27, 2020.

This page (CircuitPython Code Walkthrough) was last updated on Nov 06, 2020.