First, import the libraries. If you're using USB MIDI, then be sure to uncomment import usb_midi.
import time from random import randint import board import simpleio import busio import terminalio import neopixel from digitalio import DigitalInOut, Direction, Pull from analogio import AnalogIn import displayio import i2cdisplaybus import adafruit_imageload from adafruit_display_text import label import adafruit_displayio_ssd1306 # uncomment if using USB MIDI # import usb_midi from adafruit_display_shapes.rect import Rect import adafruit_midi from adafruit_midi.note_on import NoteOn from adafruit_midi.note_off import NoteOff from adafruit_midi.control_change import ControlChange
The onboard NeoPixel is turned off so that it doesn't show through the acrylic case.
#turn off on-board neopixel pixel = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0) pixel.fill((0, 0, 0))
The STEMMA OLED is setup as the display with displayio. It uses I2C for communication. The actual height of the OLED is 64 pixels, but by cutting it in half to 32 pixels, the terminalio font appears larger on the screen.
The sprite indexes for the Blinka sprite that will be animated later in the code are also setup.
# Use for I2C for STEMMA OLED i2c = board.I2C() display_bus = i2cdisplaybus.I2CDisplayBus(i2c, device_address=0x3D, reset=oled_reset) # STEMMA OLED dimensions. can have height of 64, but 32 makes text larger WIDTH = 128 HEIGHT = 32 BORDER = 0 # blinka sprite indexes EMPTY = 0 BLINKA_1 = 1 BLINKA_2 = 2 # setup for STEMMA OLED display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=WIDTH, height=HEIGHT) # create the displayio object splash = displayio.Group() display.root_group = splash
Text objects are setup for the four different parameters that can be controlled with the MIDI Melody Maker: BPM (beats per minute), key, mode and beat division. As you adjust the parameters, their values will be updated live on the screen.
# text for BPM
bpm_text = "BPM: "
bpm_text_area = label.Label(
terminalio.FONT, text=bpm_text, color=0xFFFFFF, x=4, y=6
)
splash.append(bpm_text_area)
bpm_rect = Rect(0, 0, 50, 16, fill=None, outline=0xFFFFFF)
splash.append(bpm_rect)
# text for key
key_text = "Key: "
key_text_area = label.Label(
terminalio.FONT, text=key_text, color=0xFFFFFF, x=4, y=21
)
splash.append(key_text_area)
key_rect = Rect(0, 15, 50, 16, fill=None, outline=0xFFFFFF)
splash.append(key_rect)
# text for mode
mode_text = "Mode: "
mode_text_area = label.Label(
terminalio.FONT, text=mode_text, color=0xFFFFFF, x=54, y=21
)
splash.append(mode_text_area)
mode_rect = Rect(50, 15, 78, 16, fill=None, outline=0xFFFFFF)
splash.append(mode_rect)
# text for beat division
beat_text = "Div: "
beat_text_area = label.Label(
terminalio.FONT, text=beat_text, color=0xFFFFFF, x=54, y=6
)
splash.append(beat_text_area)
beat_rect = Rect(50, 0, 78, 16, fill=None, outline=0xFFFFFF)
splash.append(beat_rect)
The Blinka sprite is setup using the adafruit_imageload library. It is setup as a tilegrid so you can iterate through the grid to create an animation of Blinka slithering.
# Blinka sprite setup
blinka, blinka_pal = adafruit_imageload.load("/spritesWhite.bmp",
bitmap=displayio.Bitmap,
palette=displayio.Palette)
# creates a transparent background for Blinka
blinka_pal.make_transparent(7)
blinka_grid = displayio.TileGrid(blinka, pixel_shader=blinka_pal,
width=1, height=1,
tile_height=16, tile_width=16,
default_tile=EMPTY)
blinka_grid.x = 112
blinka_grid.y = 0
splash.append(blinka_grid)
MIDI communication is setup using the adafruit_midi library. You can either use USB MIDI or MIDI over UART with the MIDI FeatherWing's 5-DIN ports.
# imports MIDI # USB MIDI: # midi = adafruit_midi.MIDI(midi_out=usb_midi.ports[1], out_channel=0) # UART MIDI: midi = adafruit_midi.MIDI(midi_out=busio.UART(board.TX, board.RX, baudrate=31250), out_channel=0)
The pins are setup for the five analog potentiometers and the switch.
# potentiometer pin setup key_pot = AnalogIn(board.A1) mode_pot = AnalogIn(board.A2) beat_pot = AnalogIn(board.A3) bpm_slider = AnalogIn(board.A4) mod_pot = AnalogIn(board.A5) # run switch setup run_switch = DigitalInOut(board.D5) run_switch.direction = Direction.INPUT run_switch.pull = Pull.UP
The MIDI notes for each key (C through B) are setup as arrays. These arrays are then placed into another array called keys so that they can be accessed.
# arrays of notes in each key
key_of_C = [60, 62, 64, 65, 67, 69, 71, 72]
key_of_Csharp = [61, 63, 65, 66, 68, 70, 72, 73]
key_of_D = [62, 64, 66, 67, 69, 71, 73, 74]
key_of_Dsharp = [63, 65, 67, 68, 70, 72, 74, 75]
key_of_E = [64, 66, 68, 69, 71, 73, 75, 76]
key_of_F = [65, 67, 69, 70, 72, 74, 76, 77]
key_of_Fsharp = [66, 68, 70, 71, 73, 75, 77, 78]
key_of_G = [67, 69, 71, 72, 74, 76, 78, 79]
key_of_Gsharp = [68, 70, 72, 73, 75, 77, 79, 80]
key_of_A = [69, 71, 73, 74, 76, 78, 80, 81]
key_of_Asharp = [70, 72, 74, 75, 77, 79, 81, 82]
key_of_B = [71, 73, 75, 76, 78, 80, 82, 83]
# array of keys
keys = [key_of_C, key_of_Csharp, key_of_D, key_of_Dsharp, key_of_E, key_of_F, key_of_Fsharp,
key_of_G, key_of_Gsharp, key_of_A, key_of_Asharp, key_of_B]
The different modes call on the array index location for different points in a key. This allows for you to play these different patterns in different keys automatically. These arrays are referencing note indexes ranging from 0 to 7.
You can create your own patterns by creating your own arrays of note indexes.
# array of note indexes for modes fifths = [0, 4, 3, 7, 2, 6, 4, 7] major = [4, 2, 0, 3, 5, 7, 6, 4] minor = [5, 7, 2, 4, 6, 5, 1, 3] pedal = [5, 5, 5, 6, 5, 5, 5, 7]
Variables are created for the strings of key names. These variables are put into an array called key_names so that they can be displayed on the OLED.
# defining variables for key name strings
C_name = "C"
Csharp_name = "C#"
D_name = "D"
Dsharp_name = "D#"
E_name = "E"
F_name = "F"
Fsharp_name = "F#"
G_name = "G"
Gsharp_name = "G#"
A_name = "A"
Asharp_name = "A#"
B_name = "B"
# array of strings for key names for use with the display
key_names = [C_name, Csharp_name, D_name, Dsharp_name, E_name, F_name, Fsharp_name,
G_name, Gsharp_name, A_name, Asharp_name, B_name]
States and default array indexes are setup to be referenced later in the loop.
# comparitors for pots' values mod_val2 = 0 beat_val2 = 0 bpm_val2 = 120 key_val2 = 0 mode_val2 = 0 # time.monotonic for running the modes run = 0 # state for being on/off run_state = False # indexes for modes r = 0 b = 0 f = 0 p = 0 maj = 0 mi = 0 random = 0 # mode states play_pedal = False play_fifths = False play_maj = False play_min = False play_rando = False play_scale = True # state for random beat division rando = False # comparitors for states last_r = 0 last_f = 0 last_maj = 0 last_min = 0 last_p = 0 last_random = 0 # index for random beat division hit = 0 # default tempo tempo = 60 # beat division sixteenth = 15 / tempo eighth = 30 / tempo quarter = 60 / tempo half = 120 / tempo whole = 240 / tempo # time.monotonic for blinka animation slither = 0 # blinka animation sprite index g = 1
Arrays are created for beat division. These values allow the MIDI Melody Maker to divide the BPM to play different note values. The possible values are whole notes, half notes, quarter notes, eighth notes and sixteenth notes.
There is also an array of strings so that the beat division can be displayed on the OLED.
# array for random beat division values rando_div = [240, 120, 60, 30, 15] # array of beat division values beat_division = [whole, half, quarter, eighth, sixteenth] # strings for beat division names beat_division_name = ["1", "1/2", "1/4", "1/8", "1/16", "Random"]
The loop begins by mapping the analog values from the potentiometers to the values needed for the different parameters.
# mapping analog pot values to the different parameters
# MIDI modulation 0-127
mod_val1 = round(simpleio.map_range(val(mod_pot), 0, 65535, 0, 127))
# BPM range 60-220
bpm_val1 = simpleio.map_range(val(bpm_slider), 0, 65535, 60, 220)
# 6 options for beat division
beat_val1 = round(simpleio.map_range(val(beat_pot), 0, 65535, 0, 5))
# 12 options for key selection
key_val1 = round(simpleio.map_range(val(key_pot), 0, 65535, 0, 11))
# 6 options for mode selection
mode_val1 = round(simpleio.map_range(val(mode_pot), 0, 65535, 0, 5))
All of the parameters that are defined by analog potentiometers compare the last reading of the pot to the current reading to define whether the state has changed.
If the state has changed, then values are updated to affect how the MIDI Melody Maker is generating data. The display on the OLED is updated with the new value.
For modulation, a modulation MIDI CC message is sent.
# sending MIDI modulation
if abs(mod_val1 - mod_val2) > 2:
# updates previous value to hold current value
mod_val2 = mod_val1
# MIDI data has to be sent as an integer
# this converts the pot data into an int
modulation = int(mod_val2)
# int is stored as a CC message
modWheel = ControlChange(1, modulation)
# CC message is sent
midi.send(modWheel)
print(modWheel)
# delay to settle MIDI data
time.sleep(0.001)
For beat division, the text is updated and if the beat division is going to be randomized, the rando state is updated to True. The index for the beat division array is also stored in beat_val2 and is referenced later in the loop.
# sets beat division
if abs(beat_val1 - beat_val2) > 0:
# updates previous value to hold current value
beat_val2 = beat_val1
print("beat div is", beat_val2)
# updates display
beat_text_area.text = "Div:%s" % beat_division_name[beat_val2]
# sets random beat division state
if beat_val2 == 5:
rando = True
else:
rando = False
time.sleep(0.001)
Mode selection sets the different mode states to True depending on the array index and also updates the OLED's text.
# mode selection
if abs(mode_val1 - mode_val2) > 0:
# updates previous value to hold current value
mode_val2 = mode_val1
# scale mode
if mode_val2 == 0:
play_scale = True
play_maj = False
play_min = False
play_fifths = False
play_pedal = False
play_rando = False
# updates display
mode_text_area.text = "Mode:Scale"
print("scale")
# major triads mode
if mode_val2 == 1:
play_scale = False
play_maj = True
play_min = False
play_fifths = False
play_pedal = False
play_rando = False
print("major chords")
# updates display
mode_text_area.text = "Mode:MajorTriads"
# minor triads mode
if mode_val2 == 2:
play_scale = False
play_maj = False
play_min = True
play_fifths = False
play_pedal = False
play_rando = False
print("minor")
# updates display
mode_text_area.text = "Mode:MinorTriads"
# fifths mode
if mode_val2 == 3:
play_scale = False
play_maj = False
play_min = False
play_fifths = True
play_pedal = False
play_rando = False
print("fifths")
# updates display
mode_text_area.text = "Mode:Fifths"
# pedal tone mode
if mode_val2 == 4:
play_scale = False
play_maj = False
play_min = False
play_fifths = False
play_pedal = True
play_rando = False
print("play random")
# updates display
mode_text_area.text = 'Mode:Pedal'
# random mode
if mode_val2 == 5:
play_scale = False
play_maj = False
play_min = False
play_fifths = False
play_pedal = False
play_rando = True
print("play random")
# updates display
mode_text_area.text = 'Mode:Random'
time.sleep(0.001)
Key selection defines which key is selected from the keys array with key_val2 holding the index. octave will be used later in the loop to access the key's MIDI note array. The text for the key's name is also updated for the OLED.
# key selection
if abs(key_val1 - key_val2) > 0:
# updates previous value to hold current value
key_val2 = key_val1
# indexes the notes in each key array
for k in keys:
o = keys.index(k)
octave = keys[o]
# updates display
key_text_area.text = 'Key:%s' % key_names[key_val2]
print("o is", o)
time.sleep(0.001)
BPM is adjusted with the sliding potentiometer. It's value is stored as an integer in tempo. The tempo is divided in the beat division formulas to get the new beat division values. These values are updated in the beat_division array. The BPM's text is updated for the OLED.
# BPM adjustment
if abs(bpm_val1 - bpm_val2) > 1:
# updates previous value to hold current value
bpm_val2 = bpm_val1
# updates tempo
tempo = int(bpm_val2)
# updates calculations for beat division
sixteenth = 15 / tempo
eighth = 30 / tempo
quarter = 60 / tempo
half = 120 / tempo
whole = 240 / tempo
# updates array of beat divisions
beat_division = [whole, half, quarter, eighth, sixteenth]
# updates display
bpm_text_area.text = "BPM:%d" % tempo
print("tempo is", tempo)
time.sleep(0.05)
If the run_switch is pressed, then the run_state is True and the MIDI Melody Maker begins generating MIDI data.
divide holds the value of the beat division data and determines when a note is played. If the beat division is going to be randomized, then hit (which holds a random integer) acts as the index for beat_division. Otherwise, beat_val2 (which holds the value of the beat division pot) defines the index.
The Blinka animation advances every time a note is played by comparing time.monotonic() to divide.
octave allows access to the different keys' MIDI note indexes so that the MIDI notes can be played in the different modes.
# if the run switch is pressed:
if run_switch.value:
run_state = True
# if random beat division, then beat_division index is randomized with index hit
if rando:
divide = beat_division[hit]
# if not random, then beat_division is the value of the pot
else:
divide = beat_division[beat_val2]
# blinka animation in time with BPM and beat division
# she will slither every time a note is played
if (time.monotonic() - slither) >= divide:
blinka_grid[0] = g
g += 1
slither = time.monotonic()
if g > 2:
g = 1
# holds key index
octave = keys[key_val2]
The modes with defined arrays (fifths, major triads, minor triads and pedal tone) work in the same way. If their state is True, then time.monotonic() is compared to divide to determine when the next note should be played.
A variable holds the index of the mode's array. That variable is then used as the index for the key's array to be able to play specific notes.
The next note is sent as a MIDI NoteOn message and the previous note is turned off with a MIDI NoteOff message.
Once the end of the array is reached, index positions are reset to the beginning to that the modes can continue playing.
# fifths mode
if play_fifths:
# tracks time divided by the beat division
if (time.monotonic() - run) >= divide:
# note index from mode, r counts index position
f = fifths[r]
# sends NoteOn
midi.send(NoteOn(octave[f]))
# turns previous note off
midi.send(NoteOff(octave[last_f]))
# print(octave[r])
run = time.monotonic()
# go to next note
r += 1
# updates previous value to hold current value
if r > 0:
last_r = r
last_f = f
hit = randint(2, 4)
# resets note index position
if r > 7:
r = 0
last_r = r
last_f = f
hit = randint(2, 4)
For scale mode, rather than referencing an array of defined notes, the keys' array of MIDI note numbers is played in ascending order. r holds the index value and increases by 1. r is reset when it is greater than 7, which is the end of the array.
# scale mode
if play_scale:
# tracks time divided by the beat division
if (time.monotonic() - run) >= divide:
# sends NoteOn
midi.send(NoteOn(octave[r]))
# turns previous note off
midi.send(NoteOff(octave[last_r]))
# print(octave[r])
run = time.monotonic()
# go to next note
r += 1
# updates previous value to hold current value
if r > 0:
last_r = r
hit = randint(2, 4)
# resets note index position
if r > 7:
r = 0
last_r = r
For random mode, the notes are played in a randomized order. In this case, r holds a random integer that is updated every time a note is played. r does not need to be reset since the random integer is constantly updated and is not constrained by an array.
# random note mode
if play_rando:
# randomizes note indexes in key
r = randint(0, 7)
# tracks time divided by the beat division
if (time.monotonic() - run) >= divide:
# sends NoteOn
midi.send(NoteOn(octave[r]))
# turns previous note off
midi.send(NoteOff(octave[last_r]))
# print(octave[r])
run = time.monotonic()
# updates previous value to hold current value
if r > 0:
last_r = r
r = randint(0, 7)
hit = randint(2, 4)
If you turn off the MIDI Melody Maker by pressing the run_switch, then an all notes off message is sent with MIDI CC message 123. This prevents any notes from accidentally hanging on, which can happen with some DAWs and hardware synths.
run_state is then set to False to reset for the next time you turn the MIDI Melody Maker on.
if not run_switch.value:
if run_state:
all_note_off = ControlChange(123, 0)
# CC message is sent
midi.send(all_note_off)
run_state = False
time.sleep(0.001)
Page last edited February 14, 2025
Text editor powered by tinymce.