Raspberry Pi Pico MIDI Channel Merger

This is my second try at using the Raspberry Pi Pico Programmable Input Output (PIO) peripherals.  My previous attempt was for a MIDI channel router.

This time, I’m building a MIDI channel merger.  The basic idea is to use the Raspberry Pi PIO to create 8 MIDI IN ports and merge them all into a single MIDI stream that is presented on the Pico’s hardware UART TX pin.

IMG_5629

Warning! I strongly recommend using old or second hand equipment for your experiments.  I am not responsible for any damage to expensive instruments!

These are the key tutorials for the main concepts used in this project:

If you are new to microcontrollers, see the Getting Started pages.

Parts list

  • Raspberry Pi Pico
  • MIDI interfaces suitable for 3.3V microcontrollers (e.g. one of the DIY MIDI Interfaces)
  • Several sources of MIDI information (optional: additional Raspberry Pi Picos!)
  • Breadboard and jumper wires (optional)

The Circuit

PiPicoMIDIMerger_bb

The test circuit shows a Pico with a 3.3V appropriate MIDI OUT circuit (as first described here) with four additional Pi Pico’s acting as 3.3V level “MIDI signals” just for testing. I’m using both UARTs on each of the four test Pico’s to allow me to check all 8 input channels for the merge.  UART 0 is shown in blue and UART 1 in green.

Note that the merger Pico is expecting to receive MIDI IN signals on GP6 to GP13.

Also note that all of the test Picos need to be powered separately, although I’ve connected all the GNDs together.

The Code

The basic PIO receiver code is based on the Micropython version of one of the Raspberry Pi Pico examples, as found here: https://github.com/micropython/micropython/blob/master/examples/rp2/pio_uart_rx.py

I’ve created eight instances of the “more fleshed out 8n1 UART receiver” and stored them all in a list.

rx_uarts = []
for i in range(NUM_UARTS):
sm = rp2.StateMachine(
i, uart_rx, freq=8 * UART_BAUD,
in_base=machine.Pin(PIN_BASE + i),
jmp_pin=machine.Pin(PIN_BASE + i)
)
sm.active(1)
rx_uarts.append(sm)

I’m using my SimpleMIDIDecoder once again (more details here) but this time I need eight instances of this too: one for each serial RX chain.  This is so that each can maintain the state of each MIDI IN interface independently and only pass on complete messages at a time.  Each will be configured to use the same three callbacks: for NoteOn; NoteOff; and THRU.

md = []
for i in range(NUM_UARTS):
md_t = SimpleMIDIDecoder.SimpleMIDIDecoder()
md_t.cbNoteOn (doMidiNoteOn)
md_t.cbNoteOff (doMidiNoteOff)
md_t.cbThru (doMidiThru)
md.append(md_t)

All callbacks will use the same midi_send function to transmit a complete MIDI message over the hardware serial port.  Note that it has to know about both two-byte and three-byte MIDI messages (recall that the MIDI decoder will set the second byte to -1 if it is a two-byte message).

def midi_send(cmd, ch, b1, b2):
if (b2 == -1):
tx_uart.write(ustruct.pack("bb",cmd+ch,b1))
else:
tx_uart.write(ustruct.pack("bbb",cmd+ch,b1,b2))

Then the whole thing will run in an infinite loop checking for any data on any of the PIO FIFO queues and if found, passing it off to the MIDI decoder for that serial (PIO) channel.

while True:
for i in range(NUM_UARTS):
if (rx_uarts[i].rx_fifo()):
md[i].read(rx_uarts[i].get() >> 24)

Note that the PIO FIFO queues are 32-bits in size but we are only receiving a byte (8-bits) at a time, so when it comes to retrieving the value (via the .get() method) it has to be shifted by 24 bits to give us our single byte.

Find it on GitHub here.  Remember you’ll need the SimpleMIDIDecoder too.

MIDI Tester

Once again, testing this is a little tricky as I don’t have eight 3.3V compliant MIDI IN interfaces (at the moment) or source of MIDI data, so I’ve cheated.  I’ve use four other Pico’s to send MIDI data directly to the merger. I’ve written a simple piece of code that just plays a sequence of notes using a randomly selected tempo on a specified MIDI channel over the first hardware serial port, and the same set of notes an octave higher, after a small random delay, on the MIDI channel that is one higher than the configured channel.

I’m hoping that by using four different Pico’s, all selecting a tempo at random, it will exercise the merger code in a sufficient manner!

import machine
import utime
import ustruct
import random

CH = 1
pin = machine.Pin(25, machine.Pin.OUT)
uart1 = machine.UART(0,31250)
uart2 = machine.UART(1,31250)
notes = [48, 50, 52, 53, 55, 57, 59, 60]
tempo = random.randint(20,1000)
diff = random.randint(100,500)

while True:
for x in notes:
pin.value(1)
uart1.write(ustruct.pack("bbb",0x90|(CH-1),x,127))
utime.sleep_ms(diff)
uart2.write(ustruct.pack("bbb",0x90|(CH),x+12,127))
utime.sleep_ms(120)
pin.value(0)
utime.sleep_ms(tempo)
uart1.write(ustruct.pack("bbb",0x80|(CH-1),x,0))
utime.sleep_ms(diff)
uart2.write(ustruct.pack("bbb",0x80|(CH),x+12,0))

You can also find the tester code on GitHub here.

Closing Thoughts

I’ve really not given this any kind of robust testing.  I believe it appears to be working, but I’ve only really tried it with NoteOn and NoteOff messages!  If you give it a try, do let me know how you get on!

You also have to remember that all the MIDI IN links are running at the same speed as the single MIDI OUT link, so if all eight inputs are running at full speed, then it will not be possible for the single output to keep up.

It would be really good to have some kind of configurable merger/router system with a configurable number of MIDI IN and OUT ports also linked to the two hardware UART ports and the USB link.

I’d also like to build a hardware 3.3V friendly “multi-MIDI IN” module of some description.

Kevin

Leave a comment