This project converts a xylophone* into a MIDI instrument, but not just any MIDI instrument: a Bluetooth Low Energy (BLE) MIDI instrument. This means that it can be setup within range of a Bluetooth connection and receive MIDI data without being tied down with a bunch of cables (besides power of course).

30 solenoids sit comfortably over the xylophone keys, waiting to strike up melodies and chords either live with a MIDI keyboard or playing along with MIDI data from a digital audio workstation (DAW). It has a lot of potential as either a live instrument or for some extra fun in the studio.

*Yes, technically this is a bell kit, or glockenspiel, and not a xylophone. However, most people will recognize instruments in the mallet family colloquially as xylophones. Xylophone is also comparatively easier to say and more commonly heard than glockenspiel or bell kit.

The Xylophone/Glockenspiel

The model instrument used for this project is fairly common in the United States, as it is often distributed to students in school who begin taking percussion lessons in music programs. You can find the identical model by searching for "Ludwig Educational Bell Kit". There's quite a few available new and used on Ebay, Amazon, Reverb, Guitar Center or perhaps your cousin's attic.

Of course, you can also modify the mounting system to fit your chosen mallet instrument, or any other instrument. As long as it has metal bars it will sound nicely with the solenoids. Any other material bar should have some sort of dampener attached to the solenoid since they could damage softer materials, such as wood.

Parts

What's smaller than a Feather but larger than a Trinket? It's an Adafruit ItsyBitsy nRF52840 Express featuring the Nordic nRF52840 Bluetooth LE...
$17.95
In Stock
Solenoids are basically electromagnets: they are made of a coil of copper wire with an armature (a slug of metal) in the middle. When the coil is energized, the slug is pulled into the...
$4.95
In Stock
Bring in some muscle to your output pins with 8 mighty Darlingtons! This DIP chip contains 8 drivers that can sink 500mA from a 50V supply and has kickback diodes included inside for...
Out of Stock
Add another 16 pins to your microcontroller using a MCP23017 port expander. The MCP23017 uses two i2c pins (these can be shared with other i2c devices), and in exchange gives you 16...
$2.95
In Stock
Fire up your engines because these are the hot-rods of toggle switches! Equipped with an aerodynamic blue protective casing, and a diffused blue LED at the tip of the switch, this...
$2.95
In Stock
1 x 1/2 Perma-Proto Breadboards - 3 Pack
Adafruit Perma-Proto Half-sized Breadboard PCB - 3 Pack
1 x Perma-Proto Quarter-sized Breadboard
Adafruit Perma-Proto Quarter-sized Breadboard PCB - Single
1 x Through-Hole Resistors - 2.2K ohm 5% 1/4W
2.2K ohm resistors - Pack of 25
1 x CONN IC DIP SOCKET 28POS
28 Pin DIP socket - DigiKey
2 x CONN IC DIP SOCKET 18POS
18 Pin DIP socket - DigiKey
1 x Hook-up Wire
Hook-up Wire Spool Set - 22AWG Solid Core - 10 x 25ft
30 x JST-PH Extension Cable
JST-PH Battery Extension Cable - 500mm
2 x 20x20 Aluminum Extrusion
Slotted Aluminum Extrusion - 20mm x 20mm - 610mm long
2 x Coupling Plate
Coupling Plate - 3 Holes - 20x20 Aluminum Extrusion
4 x Double Corner Brace
Aluminum Extrusion Double Corner Brace Support (for 20x20)
1 x Slim T-Nuts
Aluminum Extrusion Slim T-Nut for 20x20 - M4 Thread - pack of 50
1 x M4 Button Hex Screws
Button Hex Machine Screw - M4 thread - 8mm long - pack of 50

Circuit Diagram

How It Works

The ItsyBitsy nRF52840 is running CircuitPython code that allows for MIDI to be received over BLE. Additionally, it's communicating over I2C between two MCP23017's.

The MCP23017 is a multiplexer that expands the number of digital inputs and outputs that a microcontroller can communicate with. In this case, they're allowing for 30 additional outputs for the solenoid motors.

Before the solenoids can be triggered through, the MCP23017's output their signal to a ULN2803. The ULN2803 is a darlington motor driver, which is great for driving solenoids and they can take in 3.3V or 5V logic.

The CircuitPython code on the IstyBitsy nRF52840 sends on and off signals over I2C to the MCP23017's depending on the MIDI note that it receives over BLE. The MCP23017 then sends an on signal, followed quickly by an off signal, to a ULN2308 driver. Finally, the ULN2308 triggers the solenoid to hit a note on the xylophone.

Although there's quite a bit of wiring between the IC's and solenoids, the ItsyBitsy is only using 5 pins, and that's counting power and ground. The use of I2C keeps things nice and compact.

Wiring

Power:

  • 3V from the ItsyBitsy to VDD and RESET on both MCP23017's
  • 3V from the ItsyBitsy to A0 on the second MCP23017
  • USB from the ItsyBitsy to the switch
  • COMMON on all four ULN2308's to USB from the switch

Ground:

  • GND from the ItsyBitsy to VSS, A2 and A1 on both MCP23017's
  • GND from the ItsyBitsy to A0 in the first MCP23017
  • GND from the ItsyBitsy to GND on all four ULN2308's

I2C:

  • 2.2K pull-up resistor on both SCL and SDA on the first MCP23017
  • SCL from the ItsyBitsy to SCL on both MCP23017's
  • SDA from the ItsyBitsy to SDA on both MCP23017's

Solenoids:

  • The solenoid's wire 1 to the output pins (O1-O8) on all four ULN2803's
  • The solenoid's wire 2 to USB from the switch
A0, A1 and A2 on the MCP23017 determine the I2C address, which is why the wiring differs between the two.

Data Signal Flow

The chart below explains the pinouts for the multiplexers to the drivers and what MIDI note they correspond with.

Note (Name and MIDI #)

 

MUX (#, Pin Name, Pin #)

Driver (#, Pin Name, Pin Number)

G2 (55)

MUX1, GPA0, Pin 21

Driver 1, I1, Pin 1

G#2 (56)

MUX1, GPA1, Pin 22

Driver 1, I2, Pin 2

A2 (57)

MUX1, GPA2, Pin 23

Driver 1, I3, Pin 3

A#2 (58)

MUX1, GPA3, Pin 24

Driver 1, I4, Pin 4

B2 (59)

MUX1, GPA4, Pin 25

Driver 1, I5, Pin 5

C3 (60)

MUX1, GPA5, Pin 26

Driver 1, I6, Pin 6

C#3 (61)

MUX1, GPA6, Pin 27

Driver 1, I7, Pin 7

D3 (62)

MUX1, GPA7, Pin 28

Driver 1, I8, Pin 8

D#3 (63)

MUX1, GPB0, Pin 1

Driver 2, I1, Pin 1

E3 (64)

MUX1, GPB1, Pin 2

Driver 2, I2, Pin 2

F3 (65)

MUX1, GPB2, Pin 3

Driver 2, I3, Pin 3

F#3 (66)

MUX1, GPB3, Pin 4

Driver 2, I4, Pin 4

G3 (67)

MUX1, GPB4, Pin 5

Driver 2, I5, Pin 5

G#3 (68)

MUX1, GPB5, Pin 6

Driver 2, I6, Pin 6

A3 (69)

MUX1, GPB6, Pin 7

Driver 2, I7, Pin 7

A#3 (70)

MUX1, GPB7, Pin 8

Driver 2, I8, Pin 8

B3 (71)

MUX2, GPA0, Pin 21

Driver 3, I1, Pin 1

C4 (72)

MUX2, GPA1, Pin 22

Driver 3, I2, Pin 2

C#4 (73)

MUX2, GPA2, Pin 23

Driver 3, I3, Pin 3

D4 (74)

MUX2, GPA3, Pin 24

Driver 3, I4, Pin 4

D#4 (75)

MUX2, GPA4, Pin 25

Driver 3, I5, Pin 5

E4 (76)

MUX2, GPA5, Pin 26

Driver 3, I6, Pin 6

F4 (77)

MUX2, GPA6, Pin 27

Driver 3, I7, Pin 7

F#4 (78)

MUX2, GPA7, Pin 28

Driver 3, I8, Pin 8

G4 (79)

MUX2, GPB0, Pin 1

Driver 4, I1, Pin 1

G#4 (80)

MUX2, GPB1, Pin 2

Driver 4, I2, Pin 2

A4 (81)

MUX2, GPB2, Pin 3

Driver 4, I3, Pin 3

A#4 (82)

MUX2, GPB3, Pin 4

Driver 4, I4, Pin 4

B4 (83)

MUX2, GPB4, Pin 5

Driver 4, I5, Pin 5

C5 (84)

MUX2, GPB5, Pin 6

Driver 4, I6, Pin 6

Parts List

STL files for 3D printing are oriented to print "as-is" on FDM style machines. Parts are designed to 3D print without any support material. Original design source may be downloaded using the links below.

  • base-foot-left.stl
  • base-foot-right.stl
  • rail-mount.stl
  • noid-mount.stl

CAD Assembly

The left and right base feet are secured to the two pieces of 2020 aluminum stock using M4 hardware screws and nuts. The rail mount is fitted into the slotted profiles. The solenoid mount is secured to the rail mount using two M3 x 10mm screws. The mini 5V solenoid is press fitted into the solenoid mount.

Solenoid Mounts

The rail and solenoid mounts feature recesses for M3 hex nuts. The slotted tabs on the solenoid mount allow for z-height adjustment. Holes in the center allow for wire and cable pass-through.

Additional Parts

In addition to the solenoid mounts and legs, there are few other 3D printed parts available:

  • itsyBitsyXyloCaseBody.stl
  • itsyBitsyXyloCaseTop.stl
  • permaProtoBackPlate.stl
  • doubleBrace.stl
  • 3-holeCouplingPlate.stl
  • singleBrace.stl

ItsyBitsy Case

The ItsyBitsy case is a snap fit case and the design is parametric. It allows for the ItsyBitsy's 1/4 Perma-Proto to be mounted at the bottom with stand-offs. Additionally, there is a hole for a USB power cable and slots on both sides for the power and I2C wires to run from the ItsyBitsy to the other Perma-Proto boards on the xylophone. Finally, a power switch is mounted at the top and the lid has a hole for the switch to mount to.

The case mounts to the 20x20 aluminum extrusion using M4 screws and T-Nuts.

Perma-Proto Back Plate

The Perma-Proto back plate snap fits onto the 1/2 Perma-Proto boards. It allows for the solder joints to be protected from any damage or shorts. The Perma-Proto's mounting holes are included on the back plate so that they can mount to the xylophone.

Double Corner Brace, Single Corner Brace and 3 Hole Coupling Plate

These parts are CAD recreations of the original aluminum versions, with the exception of the single corner brace which is custom. You can use either the 3D printed versions or the aluminum versions.

The single corner brace is used to help support the ItsyBitsy case.

CircuitPython is a derivative of MicroPython designed to simplify experimentation and education on low-cost microcontrollers. It makes it easier than ever to get prototyping by requiring no upfront desktop software downloads. Simply copy and edit files on the CIRCUITPY drive to iterate.

Set up CircuitPython Quick Start!

Follow this quick step-by-step for super-fast Python power :)

Further Information

For more detailed info on installing CircuitPython, check out Installing CircuitPython.

Click the link above and download the latest UF2 file.

Download and save it to your desktop (or wherever is handy).

Plug your Itsy nRF52840 into your computer using a known-good USB cable.

A lot of people end up using charge-only USB cables and it is very frustrating! So make sure you have a USB cable you know is good for data sync.

In the image, the Reset button is indicated by the magenta arrow, and the BTLE status LED is indicated by the green arrow.

Double-click the Reset button on your board (magenta arrow), and you will see the BTLE LED (green arrow) will pulse quickly then slowly blue. If the DotStar LED turns red, check the USB cable, try another USB port, etc.

If double-clicking doesn't work the first time, try again. Sometimes it can take a few tries to get the rhythm right!

You will see a new disk drive appear called ITSY840BOOT.

Drag the adafruit_circuitpython_etc.uf2 file to ITSY840BOOT.

The LED will flash. Then, the ITSY840BOOT drive will disappear and a new disk drive called CIRCUITPY will appear.

That's it, you're done! :)

As we continue to develop CircuitPython and create new releases, we will stop supporting older releases. Visit https://circuitpython.org/downloads to download the latest version of CircuitPython for your board. You must download the CircuitPython Library Bundle that matches your version of CircuitPython. Please update CircuitPython and then visit https://circuitpython.org/libraries to download the latest Library Bundle.

Each CircuitPython program you run needs to have a lot of information to work. The reason CircuitPython is so simple to use is that most of that information is stored in other files and works in the background. These files are called libraries. Some of them are built into CircuitPython. Others are stored on your CIRCUITPY drive in a folder called lib. Part of what makes CircuitPython so awesome is its ability to store code separately from the firmware itself. Storing code separately from the firmware makes it easier to update both the code you write and the libraries you depend.

Your board may ship with a lib folder already, it's in the base directory of the drive. If not, simply create the folder yourself. When you first install CircuitPython, an empty lib directory will be created for you.

CircuitPython libraries work in the same way as regular Python modules so the Python docs are a great reference for how it all should work. In Python terms, we can place our library files in the lib directory because its part of the Python path by default.

One downside of this approach of separate libraries is that they are not built in. To use them, one needs to copy them to the CIRCUITPY drive before they can be used. Fortunately, we provide a bundle full of our libraries.

Our bundle and releases also feature optimized versions of the libraries with the .mpy file extension. These files take less space on the drive and have a smaller memory footprint as they are loaded.

Installing the CircuitPython Library Bundle

We're constantly updating and improving our libraries, so we don't (at this time) ship our CircuitPython boards with the full library bundle. Instead, you can find example code in the guides for your board that depends on external libraries. Some of these libraries may be available from us at Adafruit, some may be written by community members!

Either way, as you start to explore CircuitPython, you'll want to know how to get libraries on board.

You can grab the latest Adafruit CircuitPython Bundle release by clicking the button below.

Note: Match up the bundle version with the version of CircuitPython you are running - 3.x library for running any version of CircuitPython 3, 4.x for running any version of CircuitPython 4, etc. If you mix libraries with major CircuitPython versions, you will most likely get errors due to changes in library interfaces possible during major version changes.

If you need another version, you can also visit the bundle release page which will let you select exactly what version you're looking for, as well as information about changes.

Either way, download the version that matches your CircuitPython firmware version. If you don't know the version, look at the initial prompt in the CircuitPython REPL, which reports the version. For example, if you're running v4.0.1, download the 4.x library bundle. There's also a py bundle which contains the uncompressed python files, you probably don't want that unless you are doing advanced work on libraries.

After downloading the zip, extract its contents. This is usually done by double clicking on the zip. On Mac OSX, it places the file in the same directory as the zip.

Open the bundle folder. Inside you'll find two information files, and two folders. One folder is the lib bundle, and the other folder is the examples bundle.

Now open the lib folder. When you open the folder, you'll see a large number of mpy files and folders

Example Files

All example files from each library are now included in the bundles, as well as an examples-only bundle. These are included for two main reasons:

  • Allow for quick testing of devices.
  • Provide an example base of code, that is easily built upon for individualized purposes.

Copying Libraries to Your Board

First you'll want to create a lib folder on your CIRCUITPY drive. Open the drive, right click, choose the option to create a new folder, and call it lib. Then, open the lib folder you extracted from the downloaded zip. Inside you'll find a number of folders and .mpy files. Find the library you'd like to use, and copy it to the lib folder on CIRCUITPY.

This also applies to example files. They are only supplied as raw .py files, so they may need to be converted to .mpy using the mpy-cross utility if you encounter MemoryErrors. This is discussed in the CircuitPython Essentials Guide. Usage is the same as described above in the Express Boards section. Note: If you do not place examples in a separate folder, you would remove the examples from the import statement.

If a library has multiple .mpy files contained in a folder, be sure to copy the entire folder to CIRCUITPY/lib.

Example: ImportError Due to Missing Library

If you choose to load libraries as you need them, you may write up code that tries to use a library you haven't yet loaded.  We're going to demonstrate what happens when you try to utilise a library that you don't have loaded on your board, and cover the steps required to resolve the issue.

This demonstration will only return an error if you do not have the required library loaded into the lib folder on your CIRCUITPY drive.

Let's use a modified version of the blinky example.

Download: file
import board
import time
import simpleio

led = simpleio.DigitalOut(board.D13)

while True:
    led.value = True
    time.sleep(0.5)
    led.value = False
    time.sleep(0.5)

Save this file. Nothing happens to your board. Let's check the serial console to see what's going on.

We have an ImportError. It says there is no module named 'simpleio'. That's the one we just included in our code!

Click the link above to download the correct bundle. Extract the lib folder from the downloaded bundle file. Scroll down to find simpleio.mpy. This is the library file we're looking for! Follow the steps above to load an individual library file.

The LED starts blinking again! Let's check the serial console.

No errors! Excellent. You've successfully resolved an ImportError!

If you run into this error in the future, follow along with the steps above and choose the library that matches the one you're missing.

Library Install on Non-Express Boards

If you have a Trinket M0 or Gemma M0, you'll want to follow the same steps in the example above to install libraries as you need them. You don't always need to wait for an ImportError as you probably know what library you added to your code. Simply open the lib folder you downloaded, find the library you need, and drag it to the lib folder on your CIRCUITPY drive.

You may end up running out of space on your Trinket M0 or Gemma M0 even if you only load libraries as you need them. There are a number of steps you can use to try to resolve this issue. You'll find them in the Troubleshooting page in the Learn guides for your board.

Updating CircuitPython Libraries/Examples

Libraries and examples are updated from time to time, and it's important to update the files you have on your CIRCUITPY drive.

To update a single library or example, follow the same steps above. When you drag the library file to your lib folder, it will ask if you want to replace it. Say yes. That's it!

A new library bundle is released every time there's an update to a library. Updates include things like bug fixes and new features. It's important to check in every so often to see if the libraries you're using have been updated.

Once you've finished setting up your ItsyBitsy nRF52840 Express with CircuitPython, you can add these libraries to the lib folder:

  • adafruit_ble
  • adafruit_bluefruit_connect
  • adafruit_bus_device
  • adafruit_mcp230xx
  • adafruit_midi
  • adafruit_ble_midi.mpy

Then, you can click on the Download: Project Zip link above the code to download the code 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
i2c = busio.I2C(board.SCL, board.SDA)

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

#  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 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]

#  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 in setup
midi = adafruit_midi.MIDI(midi_in=midi_service, in_channel=0)

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

#  delay for solenoids
speed = 0.01

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)

    while ble.connected:

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

        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]

			#  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

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

Your ItsyBitsy CIRCUITPY drive should look like this after you load the libraries and code.py file:

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)
circuitpython_P1220643_edited.jpg
Mounts for the natural keys (left) and sharp/flat keys (right).

The solenoids sit snugly in mounts that slide onto the 20x20 aluminum extrusion pieces. They are assembled using M3 screws and nuts. The sharps and flat notes, at the top of the xylophone, will have a single mount for each note. The natural notes, at the bottom of the xylophone, will use two rail mounts and three solenoid mounts together for proper spacing.

Mount Assembly for Sharps/Flats

You'll need:

  • One rail mount
  • One solenoid mount
  • Two M3 nuts
  • Two M3x10 screws 

Turn the rail mount on its side and place the two M3 nuts into their slots.

Attach the solenoid mount to the rail mount by screwing the two M3 screws into the slotted nuts.

The fully assembled single mount. Assemble 12 total.

Triple Mount for Natural Notes

You'll need:

  • Two rail mounts
  • Three solenoid mounts
  • Four M3 nuts
  • Four M3x10 screws

Take the first rail mount and place one M3 nut in the mount's slot on the right.

Attach one solenoid mount to the first rail mount using one M3 screw.

With the second rail mount, place one M3 nut in the mount's left-hand slot.

Attach one solenoid mount to the second rail mount using one M3 screw. This will leave an empty space for the third solenoid mount to be attached.

Insert M3 nuts into the two remaining slots on both rail mounts.

Attach the third solenoid mount with M3 screws so that it's sitting on top of the original solenoid mounts. Assemble 6 total.

Insert the Solenoids

Insert the solenoids into the solenoid mounts so that their wires are running up the cable channel on the mount.

The circuit for the xylophone lives primarily on three proto boards. These boards are mounted vertically on the 20x20 aluminum extrusions with braces and coupling plates. You can use either aluminum versions of these pieces or 3D print them.

You'll need:

  • Four double corner braces
  • Two 3 hole coupling plates
  • Five M4 screws
  • Five M3x10 screws
  • One M3x16 screw
  • One M4 nut
  • Four slim T-Nuts
  • Six M3 standoffs
  • Six M3 washers (optional)

The washers are helpful if you're using the aluminum versions of the parts, since the holes can be slightly over-sized for the M3 hardware.

Mount Prep

Take the four double corner braces, four M4 screws and four slim T-Nuts.

Attach a t-nut to one of the far holes on each double corner brace with an M4 screw.

These will eventually mount to the 20x20 extrusions.

Mount 1 Assembly

Attach an M3 standoff in the center hole of the coupling plate using an M3 screw.

Bring in one of the assembled double corner braces, one M3 standoff and the M3x16 screw.

Using the opposite far hole on the double corner brace, attach the 3 hole coupling plate with the M3x16 through the coupling plate's left hole. Attach the standoff to the screw.

The completed assembly.

Mount 2 Assembly

Attach M3 standoffs to the 3 hole coupling plate's left and right holes with M3 screws.

Using one of the assembled double corner braces, insert an M4 screw into the open far hole.

Attach the coupling plate to the corner brace using the M4 screw and the coupling plate's center hole. Secure it with an M4 nut.

Mount 3 and 4 Assembly

With the two remaining assembled corner braces, attach an M3 standoff with an M3 screw in the open far hole.

When you're finished, the four mounts should look like the four pictured below.

Adding the Solenoid Mounts

The 20x20 aluminum extrusions will sit towards the back of each set of notes so that the solenoids will strike towards the middle of the keys for the best possible tone. Slide the assembled solenoid mounts onto the rails.

Extrusion Feet

The two 3D printed feet allow for the extrusions to sit at the correct angle across the keys and at the right height as well.

To assemble, take two slim T-Nuts and two M4 screws. Screw them loosely together through the top holes on both legs. 

After the solenoid mounts are on the aluminum extrusions, you can slide the legs on either side of the extrusions and tighten the M4 screws so that they're secured using the T-Nuts.

Attach the Proto Board Mounts

In the center of the 2nd, 3rd, 4th and 5th triple solenoid mounts, place the slim T-Nuts from proto board mounts. Turn them so that they're locked in.

Attach each mount with the M4 screw to the T-Nut. 

The electronics for the xylophone are setup identically to the Fritzing circuit diagram on the Electronics page. The ItsyBitsy will live on a 1/4 Perma-Proto and then three 1/2 Perma-Proto boards will be used for the MCP23017's and the four ULN2308's.

One board will have the two MCP32017's and then the two remaining boards will have two ULN2308's each. To begin wiring, solder the IC sockets onto the boards. Then, run the wires for power, ground and I2C. 

Mount the boards to the xylophone. The screw ends of the M3 stand-offs are facing out so that they can slot into the boards' mounting holes and then be secured with some M3 nuts. By doing this, you'll know exactly how long the wires have to be between the multiplexers and drivers, followed by the driver boards and the solenoids.

All of the outputs from the multiplexers will connect to the inputs of the drivers, which should be oriented to face towards the bottom of the xylophone. This way, the outputs of the drivers will be at the top and easier to access for the solenoids.

The solenoids will connect using a JST extension cable. Cut them to length based on the location of the solenoid and the driver output. 

Don't get rid of the other connection end though! They'll come in handy for future projects. You can never have too many JST connectors.

Strip back a few millimeters of wire on the JST extension and then tin them for soldering. Solder one wire to the driver's output and the second wire to the power rail, which will carry 5V. Solenoids do not have polarity, so it doesn't matter in this case which wire is connected to either soldering point.

Continue this for all of the solenoids.

The process of wiring up the multiplexers to the drivers can be a little tedious. To avoid any major mistakes you can wire up one driver to each multiplexer to make sure that I2C is working and that both driver boards are outputting to the solenoids as expected.

It also helps to have a cat inspect the circuit's progress.

Wiring the ItsyBitsy

The ItsyBitsy is attached to the 1/4 Perma-Proto with socket headers. Power, ground and I2C signals are run via long wires to the other Perma-Protos through the case's wiring slots at the bottom.

A toggle switch is in-line with the USB power signal from the ItsyBitsy that supplies the ULN2308's. This allows you to have control over the solenoids receiving power in case of a problem. 

This particular toggle switch also has an LED at the top that lights up when power is engaged. 

  • The toggle switch's GND is connected to the ItsyBitsy's GND
  • The toggle switch's + pin is connected to the ItsyBitsy's USB pin
  • The toggle switch's headlamp pin is connected to the ItsyBitsy's USB pin

With everything wired up, it's time for the final assembly.

Perma-Proto Back Plate

Before mounting the Perma-Proto boards, snap on their 3D printed back plates. These will help to protect the solder points from damage and shorts.

Mount the Perma-Proto boards and plug in the solenoid motors to their respective cables. 

Mounting the ItsyBitsy's Case

Attach two M3 stand-offs with two M3 screws into two of the holes in the bottom of the case as pictured. Then, with one M4 screw and a T-Nut, mount the case to the aluminum extrusion.

A 3D printed single corner brace is secured to the side of the aluminum extrusion using an M4 screw and a T-Nut. The remaining hole from the case is secured to the corner brace with an M4 screw and M4 nut.

The ItsyBitsy's 1/4 Perma-Proto can then be mounted to the M3 stand-offs with M3 nuts.

The ItsyBitsy is ready to be powered-up to run the BLE MIDI xylophone! For best results, use an external 5V USB power supply rated for at least 2A. 

The process for using this xylophone with your DAW or MIDI controller will depending on your computer's operating system. BLE MIDI is supported natively in macOS. Apple has great instructions on how to easily connect your BLE MIDI peripherals here.

Windows BLE MIDI Setup

On Windows, there is a little more setup involved. In order to have your DAW or USB MIDI controller connect, you'll need a software bridge to connect the ItsyBitsy to your desired MIDI input. There are a few available, but MIDIberry is a freeware option that is available through the Microsoft store. It allows you to select the input, output and monitor the MIDI messages being sent.

If you just want to play your xylophone live, then you can use MIDIberry on its own to control it. However, if you want to send MIDI out from a DAW for the xylophone to play for recording or live purposes, you'll want to setup a virtual MIDI loopback.

Most DAW's look for a MIDI hardware connection to assign MIDI input and outputs. By using a virtual MIDI loopback, you can trick the DAW into thinking you have different MIDI hardware devices plugged into your computer or even give your MIDI hardware extra functionality. 

One of the most common solutions for this is the loopMIDI project by Tobias Erichsen. It allows you to easily add virtual MIDI loopbacks on the fly.

In MIDIberry, you can define the virtual loopback as an input with the ItsyBitsy as an output. This means that there is a virtual cable connecting the two together, allowing the ItsyBitsy to receive MIDI data from a DAW over BLE.

After setting up your virtual loopback, you can define it as a MIDI out in your preferred DAW.

You can setup a MIDI track to send MIDI data out to the virtual loopback and your xylophone should begin playing along.

Playing the xylophone live with a MIDI keyboard is very fun and can allow you to play pieces that previously would've been impossible when limited to four mallets; especially on a bell kit/glockenspiel.

Taking in MIDI data from a DAW is really where things get interesting though, since you can record music live that previously would've been through a software synth. It could also make be a great addition to a live music setup whether it's for a traditional duet or for electro-acoustic music.

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