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.