Import the Libraries

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

Turn Off the Onboard NeoPixel

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))

Setup the STEMMA OLED

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 = displayio.I2CDisplay(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.show(splash)

Create Text Objects for the OLED

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)

Setup the Blinka Tilegrid

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)

Setup MIDI

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)

Setup the Potentiometers and Switch Pins

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

Create the MIDI Note Arrays

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]

MIDI Mode Arrays

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]

Key Name Strings for the OLED

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

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

Beat Division Setup

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

Map the Analog Values

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))

Read the Potentiometers

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)

Beat Division

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

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

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

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)

Run the MIDI Melody Maker

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]

Running the Modes: Fifths, Major Triads, Minor Triads and Pedal Tone

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)

Running Scale Mode

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

Running Random Mode

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)

Stop the MIDI Melody Maker

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)

This guide was first published on Sep 29, 2020. It was last updated on Sep 29, 2020.

This page (CircuitPython Code Walkthrough) was last updated on Sep 28, 2020.

Text editor powered by tinymce.