BBC micro:bit
VS1053 Codec Breakout - MIDI

Introduction

The VS1053 Codec breakout is an Adafruit product. It is a breakout board for an integrated circuit that can be used as an audio player, recorder and MIDI player. It works with 3V logic, at least as far as the MIDI player is concerned and works with the micro:bit UART library.

This means that we can send MIDI signals to the board and hear them through speakers or headphones.

It is a relatively expensive board at over £20, more than the cost of most microcontroller boards. The programs generate MIDI signals over UART. If you wire the same pin up to a MIDI connector (look up what you need to do), you could send MIDI signals from the micro:bit to USB to MIDI cable and play them on a PC. Instructions for this are in the Arduino section of the site.

Circuit

A different circuit is needed for the different functions of this board. This is the circuit I used for MIDI over UART, following Adafruit's guide. The component at the top is a 3.5mm stereo headphone jack.

VS1053

Programming - Test

This program is based on the example Arduino sketch in Adafruit's library for the board. It just repeats a simple sequence of notes.

from microbit import *

VS1053_BANK_DEFAULT = 0x00
VS1053_BANK_DRUMS1 = 0x78
VS1053_BANK_DRUMS2 = 0x7F
VS1053_BANK_MELODY = 0x79
VS1053_GM1_OCARINA = 80

MIDI_NOTE_ON  = 0x90
MIDI_NOTE_OFF = 0x80
MIDI_CHAN_MSG = 0xB0
MIDI_CHAN_BANK = 0x00
MIDI_CHAN_VOLUME = 0x07
MIDI_CHAN_PROGRAM = 0xC0

def midiSetInstrument(chan, inst):
    if chan>15:
        return
    inst-=1
    if inst>127:
        return
    msg = bytes([MIDI_CHAN_PROGRAM | chan, inst])
    uart.write(msg)
    return

def midiSetChannelVolume(chan, vol):
  if chan>15:
        return
  if vol>127:
        return
  msg = bytes([MIDI_CHAN_MSG | chan, MIDI_CHAN_VOLUME, vol])
  uart.write(msg)
  return

def midiSetChannelBank(chan, bank):
  if chan>15:
        return
  if bank>127:
        return
  msg = bytes([MIDI_CHAN_MSG | chan, MIDI_CHAN_BANK, bank])
  uart.write(msg)
  return

def midiNoteOn(chan, n, vel):
  if chan>15:
        return
  if n>127:
        return
  if vel>127:
       return  
  msg = bytes([MIDI_NOTE_ON | chan, n, vel])
  uart.write(msg)
  return      

def midiNoteOff(chan, n, vel):
  if chan>15:
        return
  if n>127:
        return
  if vel>127:
        return  
  msg = bytes([MIDI_NOTE_OFF | chan, n, vel])
  uart.write(msg)
  return      

def Start():
    uart.init(baudrate=31250, bits=8, parity=None, stop=1, tx=pin0)
    pin2.write_digital(0)
    sleep(10)
    pin2.write_digital(1)
    sleep(10)
    midiSetChannelBank(0, VS1053_BANK_MELODY)
    midiSetInstrument(0, VS1053_GM1_OCARINA)
    midiSetChannelVolume(0, 127)
    return

Start()
while True:
    for i in range(60,69):
        midiNoteOn(0,i,127)
        sleep(100)
        midiNoteOff(0,i,127)
    sleep(1000)

The following table lists the hexadecimal values for MIDI notes.

Musical NoteHex Value
C(-1)00
C#(-1)01
D(-1)02
D#(-1)03
E(-1)04
F(-1)05
F#(-1)06
G(-1)07
G#(-1)08
A(-1)09
A#(-1)0A
B(-1)0B
C00C
C#00D
D00E
D#00F
E010
F011
F#012
G013
G#014
A015
A#016
B017
C118
C#119
D11A
D#11B
E11C
F11D
F#11E
G11F
G#120
A121
A#122
B123
C224
C#225
D226
D#227
E228
F229
F#22A
G22B
G#22C
A22D
A#22E
B22F
C330
C#331
D332
D#333
E334
F335
F#336
G337
G#338
A339
A#33A
B33B
C43C
C#43D
D43E
D#43F
E440
F441
F#442
G443
G#444
A445
A#446
B447
C548
C#549
D54A
D#54B
E54C
F54D
F#54E
G54F
G#550
A551
A#552
B553
C654
C#655
D656
D#657
E658
F659
F#65A
G65B
G#65C
A65D
A#65E
B65F
C660
C#761
D762
D#763
E764
F765
F#766
G767
G#768
A769
A#76A
B76B
C86C
C#86D
D86E
D#86F
E870
F871
F#872
G873
G#874
A875
A#876
B877
C978
C#979
D97A
D#97B
E97C
F97D
F#97E
G97F

Programming - Tune

Extending the principle a little, we can play a tune.

from microbit import *

VS1053_BANK_DEFAULT = 0x00
VS1053_BANK_DRUMS1 = 0x78
VS1053_BANK_DRUMS2 = 0x7F
VS1053_BANK_MELODY = 0x79
VS1053_GM1_OCARINA = 80

MIDI_NOTE_ON  = 0x90
MIDI_NOTE_OFF = 0x80
MIDI_CHAN_MSG = 0xB0
MIDI_CHAN_BANK = 0x00
MIDI_CHAN_VOLUME = 0x07
MIDI_CHAN_PROGRAM = 0xC0

tune = [0x3C, 0x3C, 0x43, 0x43, 0x45, 0x45, 0x43, 0x41, 0x41, 0x40, 0x40, 0x3E, 0x3E, 0x3C]
beats = [1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 2]
tunelength = 14
paws = 300


def midiSetInstrument(chan, inst):
    if chan>15:
        return
    inst-=1
    if inst>127:
        return
    msg = bytes([MIDI_CHAN_PROGRAM | chan, inst])
    uart.write(msg)
    return

def midiSetChannelVolume(chan, vol):
  if chan>15:
        return
  if vol>127:
        return
  msg = bytes([MIDI_CHAN_MSG | chan, MIDI_CHAN_VOLUME, vol])
  uart.write(msg)
  return

def midiSetChannelBank(chan, bank):
  if chan>15:
        return
  if bank>127:
        return
  msg = bytes([MIDI_CHAN_MSG | chan, MIDI_CHAN_BANK, bank])
  uart.write(msg)
  return

def midiNoteOn(chan, n, vel):
  if chan>15:
        return
  if n>127:
        return
  if vel>127:
       return  
  msg = bytes([MIDI_NOTE_ON | chan, n, vel])
  uart.write(msg)
  return      

def midiNoteOff(chan, n, vel):
  if chan>15:
        return
  if n>127:
        return
  if vel>127:
        return  
  msg = bytes([MIDI_NOTE_OFF | chan, n, vel])
  uart.write(msg)
  return      

def Start():
    uart.init(baudrate=31250, bits=8, parity=None, stop=1, tx=pin0)
    pin2.write_digital(0)
    sleep(10)
    pin2.write_digital(1)
    sleep(10)
    midiSetChannelBank(0, VS1053_BANK_MELODY)
    midiSetInstrument(0, VS1053_GM1_OCARINA)
    midiSetChannelVolume(0, 127)
    return

Start()

while True:
    for i in range(0, tunelength):
        midiNoteOn(0, tune[i], 127)
        sleep(beats[i]*paws)
        midiNoteOff(0, tune[i], 127)
        sleep(paws/10)
    sleep(3000)
    

Programming - Drum Kit

This program is the beginning of a MIDI drum kit. You can extend the principle by thinking of other inputs you could use. With drum banks, the note is the instrument. You play a note to get the different percussion item.

from microbit import *

VS1053_BANK_DEFAULT = 0x00
VS1053_BANK_DRUMS1 = 0x78
VS1053_BANK_DRUMS2 = 0x7F
VS1053_BANK_MELODY = 0x79
VS1053_SNARE = 38
VS1053_BASS = 36


MIDI_NOTE_ON  = 0x90
MIDI_NOTE_OFF = 0x80
MIDI_CHAN_MSG = 0xB0
MIDI_CHAN_BANK = 0x00
MIDI_CHAN_VOLUME = 0x07
MIDI_CHAN_PROGRAM = 0xC0

def midiSetInstrument(chan, inst):
    if chan>15:
        return
    inst-=1
    if inst>127:
        return
    msg = bytes([MIDI_CHAN_PROGRAM | chan, inst])
    uart.write(msg)
    return

def midiSetChannelVolume(chan, vol):
  if chan>15:
        return
  if vol>127:
        return
  msg = bytes([MIDI_CHAN_MSG | chan, MIDI_CHAN_VOLUME, vol])
  uart.write(msg)
  return

def midiSetChannelBank(chan, bank):
  if chan>15:
        return
  if bank>127:
        return
  msg = bytes([MIDI_CHAN_MSG | chan, MIDI_CHAN_BANK, bank])
  uart.write(msg)
  return

def midiNoteOn(chan, n, vel):
  if chan>15:
        return
  if n>127:
        return
  if vel>127:
       return  
  msg = bytes([MIDI_NOTE_ON | chan, n, vel])
  uart.write(msg)
  return      

def midiNoteOff(chan, n, vel):
  if chan>15:
        return
  if n>127:
        return
  if vel>127:
        return  
  msg = bytes([MIDI_NOTE_OFF | chan, n, vel])
  uart.write(msg)
  return      

def Start():
    uart.init(baudrate=31250, bits=8, parity=None, stop=1,tx=pin0)
    pin2.write_digital(0)
    sleep(10)
    pin2.write_digital(1)
    sleep(10)
    midiSetChannelBank(0, VS1053_BANK_DRUMS1)
    midiSetInstrument(0, 80)
    midiSetChannelVolume(0, 127)
    return

Start()
lastA = False
lastB = False

while True:
    a = button_a.is_pressed()
    b = button_b.was_pressed()
    if a==True and lastA==False:
        midiNoteOn(0,VS1053_BASS,127)
    elif a==False and lastA==True:
        midiNoteOff(0,VS1053_BASS,127)
    if b==True and lastB==False:
        midiNoteOn(0,VS1053_SNARE,127)
    elif b==False and lastB==True:
        midiNoteOff(0,VS1053_SNARE,127)   
    lastA = a
    lastB = b
    sleep(10)

Finally, because it couldn't be resisted, an accelerometer-based instrument. More of a sound effect the way it is programmed here though.

from microbit import *

VS1053_BANK_DEFAULT = 0x00
VS1053_BANK_DRUMS1 = 0x78
VS1053_BANK_DRUMS2 = 0x7F
VS1053_BANK_MELODY = 0x79

MIDI_NOTE_ON  = 0x90
MIDI_NOTE_OFF = 0x80
MIDI_CHAN_MSG = 0xB0
MIDI_CHAN_BANK = 0x00
MIDI_CHAN_VOLUME = 0x07
MIDI_CHAN_PROGRAM = 0xC0

def midiSetInstrument(chan, inst):
    if chan>15:
        return
    inst-=1
    if inst>127:
        return
    msg = bytes([MIDI_CHAN_PROGRAM | chan, inst])
    uart.write(msg)
    return

def midiSetChannelVolume(chan, vol):
  if chan>15:
        return
  if vol>127:
        return
  msg = bytes([MIDI_CHAN_MSG | chan, MIDI_CHAN_VOLUME, vol])
  uart.write(msg)
  return

def midiSetChannelBank(chan, bank):
  if chan>15:
        return
  if bank>127:
        return
  msg = bytes([MIDI_CHAN_MSG | chan, MIDI_CHAN_BANK, bank])
  uart.write(msg)
  return

def midiNoteOn(chan, n, vel):
  if chan>15:
        return
  if n>127:
        return
  if vel>127:
       return  
  msg = bytes([MIDI_NOTE_ON | chan, n, vel])
  uart.write(msg)
  return      

def midiNoteOff(chan, n, vel):
  if chan>15:
        return
  if n>127:
        return
  if vel>127:
        return  
  msg = bytes([MIDI_NOTE_OFF | chan, n, vel])
  uart.write(msg)
  return      

def Start():
    uart.init(baudrate=31250, bits=8, parity=None, stop=1, tx=pin0)
    pin2.write_digital(0)
    sleep(10)
    pin2.write_digital(1)
    sleep(10)
    midiSetChannelBank(0, VS1053_BANK_MELODY)
    # electric guitar clean
    midiSetInstrument(0, 28)
    midiSetChannelVolume(0, 127)
    return

Start()
lastNote = 0
note = 0

while True:
    if button_a.is_pressed():
        midiNoteOff(0, lastNote, 127)
        a = (accelerometer.get_x() + 1000) // 20
        lastNote = a
        midiNoteOn(0, a, 127)
    else:
        midiNoteOff(0, lastNote, 127)   
    sleep(10)

Datasheet

The datasheet for the chip is a really useful source of information. The MIDI stuff is on pages 31 and 32. You can find a list of the control codes that you can send (not all are covered with these functions) and a list of instruments.

Challenges

  1. Getting a MIDI tune playing nicely is a good start.
  2. Using the 3 touch inputs, perhaps with some play-doh, you could make more of a drum kit. Add some external arcade buttons on the input pins and you are getting there.
  3. You can play more than one instrument more easily if you set them up on different channels. Try this out, maybe by working out how to play something sounding like a chord.
  4. Improve the accelerometer-based instrument by using a smaller range of notes, all from the same scale. Look up the blues scale and map your accelerometer readings onto two or three octaves from this scale.
  5. Anything that gives you analog readings can be used as the note selector for your instruments. The level of precision in your device will impact upon your instrument.
  6. You could make a tool to make drum loops, using the matrix as a display. Let the user navigate a portion of the LED matrix selecting and deselecting dots to indicate whether or not the instrument is played on that beat. This is quite a complex task, you have two modes of operation to consider, editing and playing. You can do these things at the same time if you want. The editing requires a fair bit of work to make a user interface. Let's say, you do 16 beats. The user makes a pattern of 16 on and off pixels to indicate which beats should be played by the instrument. This can be stored as a binary integer. Each time the 16 beat sequence is played, a binary place value will tell you whether or not the instrument should be played.