Once you've finished setting up your Metro RP2350 with CircuitPython, you can access the code, MIDI files and necessary libraries by downloading the Project Bundle.
To do this, click on the Download Project Bundle button in the window below. It will download to your computer as a zipped folder.
# SPDX-FileCopyrightText: 2025 Liz Clark for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import os
from random import randint
import board
import usb_midi
import keypad
from adafruit_mcp230xx.mcp23017 import MCP23017
import adafruit_midi
from adafruit_midi.note_on import NoteOn
from adafruit_midi.note_off import NoteOff
import adafruit_midi_parser
# music_box plays back MIDI files on CP drive
# set to false for live MIDI over USB control
music_box = True
# define the notes that correspond to each solenoid
notes = [48, 50, 52, 53, 55, 57, 59, 60]
key = keypad.Keys((board.BUTTON,), value_when_pressed=False, pull=True)
i2c = board.STEMMA_I2C()
mcp = MCP23017(i2c)
noids = []
for i in range(8):
noid = mcp.get_pin(i)
noid.switch_to_output(value=False)
noids.append(noid)
# pylint: disable=used-before-assignment, unused-argument, global-statement, no-self-use
if not music_box:
midi = adafruit_midi.MIDI(
midi_in=usb_midi.ports[0], in_channel=0, midi_out=usb_midi.ports[1], out_channel=0
)
else:
midi_files = []
for filename in os.listdir('/'):
if filename.lower().endswith('.mid') and not filename.startswith('.'):
midi_files.append("/"+filename)
print(midi_files)
class Custom_Player(adafruit_midi_parser.MIDIPlayer):
def on_note_on(self, note, velocity, channel): # noqa: PLR6301
for z in range(len(notes)):
if notes[z] == note:
print(f"Playing note: {note}")
noids[z].value = True
def on_note_off(self, note, velocity, channel): # noqa: PLR6301
for z in range(len(notes)):
if notes[z] == note:
noids[z].value = False
def on_end_of_track(self, track): # noqa: PLR6301
print(f"End of track {track}")
for z in range(8):
noids[z].value = False
def on_playback_complete(self): # noqa: PLR6301
global now_playing
now_playing = False
for z in range(8):
noids[z].value = False
parser = adafruit_midi_parser.MIDIParser()
parser.parse(midi_files[randint(0, (len(midi_files) - 1))])
player = Custom_Player(parser)
new_file = False
now_playing = False
while True:
if music_box:
event = key.events.get()
if event:
if event.pressed:
now_playing = not now_playing
if now_playing:
new_file = True
if new_file:
parser.parse(midi_files[randint(0, (len(midi_files) - 1))])
print(f"Successfully parsed! Found {len(parser.events)} events.")
print(f"BPM: {parser.bpm:.1f}")
print(f"Note Count: {parser.note_count}")
new_file = False
if now_playing:
player.play(loop=False)
else:
msg = midi.receive()
if msg is not None:
for i in range(8):
noid_output = noids[i]
notes_played = notes[i]
if isinstance(msg, NoteOn) and msg.note == notes_played:
noid_output.value = True
elif isinstance(msg, NoteOff) and msg.note == notes_played:
noid_output.value = False
Upload the Code and Libraries to the Metro RP2350
After downloading the Project Bundle, plug your Metro RP2350 into the computer's USB port with a known good USB data+power cable. You should see a new flash drive appear in the computer's File Explorer or Finder (depending on your operating system) called CIRCUITPY. Unzip the folder and copy the following items to the Metro RP2350's CIRCUITPY drive.
- lib folder
- code.py
- song_1.mid
- song_2.mid
- song_3.mid
- song_4.mid
Your Metro RP2350 CIRCUITPY drive should look like this after copying the lib folder, MIDI files and code.py file:
How the Code Works
At the top of the code is the music_box variable. This determines the mode for the robot xylophone. If music_box is set to True, then it will playback MIDI files (.mid) files on the CIRCUITPY drive. If it is set to False, then it will act as a USB MIDI output device. The MIDI note numbers assigned to each solenoid are defined in the notes array.
# music_box plays back MIDI files on CP drive # set to false for live MIDI over USB control music_box = True # define the notes that correspond to each solenoid notes = [48, 50, 52, 53, 55, 57, 59, 60]
Button and I2C
The BOOT button is passed as a keypad button. Then, the MCP23017 is instantiated over I2C and the 8 solenoid pins are created and added to the noids array.
key = keypad.Keys((board.BUTTON,), value_when_pressed=False, pull=True)
i2c = board.STEMMA_I2C()
mcp = MCP23017(i2c)
noids = []
for i in range(8):
noid = mcp.get_pin(i)
noid.switch_to_output(value=False)
noids.append(noid)
if not music_box:
midi = adafruit_midi.MIDI(
midi_in=usb_midi.ports[0], in_channel=0, midi_out=usb_midi.ports[1], out_channel=0
)
Music Box Mode
If music_box is True, then the Metro is setup to playback MIDI files on the CIRCUITPY drive. First, all of the .mid files on the drive are found and added to the midi_files array. Then, a MIDIPlayer class is created to define what is done in the code when certain MIDI messages are read from a MIDI file. In the case of the xylophone, if a NoteOn message is received, the solenoid is triggered. When a NoteOff message is received, the solenoid retracts. At the end of the MIDI file, all solenoids are set to False to make sure none of them are stuck on.
else:
midi_files = []
for filename in os.listdir('/'):
if filename.lower().endswith('.mid') and not filename.startswith('.'):
midi_files.append("/"+filename)
print(midi_files)
class Custom_Player(adafruit_midi_parser.MIDIPlayer):
def on_note_on(self, note, velocity, channel): # noqa: PLR6301
for z in range(len(notes)):
if notes[z] == note:
print(f"Playing note: {note}")
noids[z].value = True
def on_note_off(self, note, velocity, channel): # noqa: PLR6301
for z in range(len(notes)):
if notes[z] == note:
noids[z].value = False
def on_end_of_track(self, track): # noqa: PLR6301
print(f"End of track {track}")
for z in range(8):
noids[z].value = False
def on_playback_complete(self): # noqa: PLR6301
global now_playing
now_playing = False
for z in range(8):
noids[z].value = False
parser = adafruit_midi_parser.MIDIParser()
parser.parse(midi_files[randint(0, (len(midi_files) - 1))])
player = Custom_Player(parser)
new_file = False
now_playing = False
The Loop
When you press the BOOT button in music box mode, a MIDI file on the CIRCUITPY drive is chosen and queued up to be parsed. As it is read back, the file is played on the xylophone.
while True:
if music_box:
event = key.events.get()
if event:
if event.pressed:
now_playing = not now_playing
if now_playing:
new_file = True
if new_file:
parser.parse(midi_files[randint(0, (len(midi_files) - 1))])
print(f"Successfully parsed! Found {len(parser.events)} events.")
print(f"BPM: {parser.bpm:.1f}")
print(f"Note Count: {parser.note_count}")
new_file = False
if now_playing:
player.play(loop=False)
In live MIDI mode, whenever a NoteOn or NoteOff message is received, the solenoids strike a note.
else:
msg = midi.receive()
if msg is not None:
for i in range(8):
noid_output = noids[i]
notes_played = notes[i]
if isinstance(msg, NoteOn) and msg.note == notes_played:
noid_output.value = True
elif isinstance(msg, NoteOff) and msg.note == notes_played:
noid_output.value = False
Page last edited May 20, 2025
Text editor powered by tinymce.