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.
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 Note | Hex 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 |
C0 | 0C |
C#0 | 0D |
D0 | 0E |
D#0 | 0F |
E0 | 10 |
F0 | 11 |
F#0 | 12 |
G0 | 13 |
G#0 | 14 |
A0 | 15 |
A#0 | 16 |
B0 | 17 |
C1 | 18 |
C#1 | 19 |
D1 | 1A |
D#1 | 1B |
E1 | 1C |
F1 | 1D |
F#1 | 1E |
G1 | 1F |
G#1 | 20 |
A1 | 21 |
A#1 | 22 |
B1 | 23 |
C2 | 24 |
C#2 | 25 |
D2 | 26 |
D#2 | 27 |
E2 | 28 |
F2 | 29 |
F#2 | 2A |
G2 | 2B |
G#2 | 2C |
A2 | 2D |
A#2 | 2E |
B2 | 2F |
C3 | 30 |
C#3 | 31 |
D3 | 32 |
D#3 | 33 |
E3 | 34 |
F3 | 35 |
F#3 | 36 |
G3 | 37 |
G#3 | 38 |
A3 | 39 |
A#3 | 3A |
B3 | 3B |
C4 | 3C |
C#4 | 3D |
D4 | 3E |
D#4 | 3F |
E4 | 40 |
F4 | 41 |
F#4 | 42 |
G4 | 43 |
G#4 | 44 |
A4 | 45 |
A#4 | 46 |
B4 | 47 |
C5 | 48 |
C#5 | 49 |
D5 | 4A |
D#5 | 4B |
E5 | 4C |
F5 | 4D |
F#5 | 4E |
G5 | 4F |
G#5 | 50 |
A5 | 51 |
A#5 | 52 |
B5 | 53 |
C6 | 54 |
C#6 | 55 |
D6 | 56 |
D#6 | 57 |
E6 | 58 |
F6 | 59 |
F#6 | 5A |
G6 | 5B |
G#6 | 5C |
A6 | 5D |
A#6 | 5E |
B6 | 5F |
C6 | 60 |
C#7 | 61 |
D7 | 62 |
D#7 | 63 |
E7 | 64 |
F7 | 65 |
F#7 | 66 |
G7 | 67 |
G#7 | 68 |
A7 | 69 |
A#7 | 6A |
B7 | 6B |
C8 | 6C |
C#8 | 6D |
D8 | 6E |
D#8 | 6F |
E8 | 70 |
F8 | 71 |
F#8 | 72 |
G8 | 73 |
G#8 | 74 |
A8 | 75 |
A#8 | 76 |
B8 | 77 |
C9 | 78 |
C#9 | 79 |
D9 | 7A |
D#9 | 7B |
E9 | 7C |
F9 | 7D |
F#9 | 7E |
G9 | 7F |
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
- Getting a MIDI tune playing nicely is a good start.
- 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.
- 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.
- 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.
- 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.
- 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.