Raspberry Pi Pico MIDI Channel Router

Ever since I got hold of a Raspberry Pi Pico microcontroller I wanted to try out the Programmable Input Output (PIO) peripherals.  I’ve also been on the lookout for microcontrollers with more serial ports than the usual one or two.  The best I’ve found so far are ATmega2560 boards with four and the Teensy range which can have up to eight depending on which board you have.

But with the Raspberry Pi Pico PIO it should be possible to implement eight in addition to the two build-in hardware serial ports and the USB, so this has huge potential as a MIDI router, filter, merge or THRU unit.

This is the first step into exploring the Pico’s PIO for MIDI purposes.

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 interface suitable for 3.3V microcontrollers (e.g. one of the DIY MIDI Interfaces)
  • Source of multi-channel MIDI information
  • Means of examining the serial outputs (see below)
  • Breadboard and jumper wires (optional)

The Circuit

RaspberryPiPicoMIDIChannelSplitter_bb

The PIO subsystem acts on the IO pins of the Raspberry Pi Pico.  There is an example in the Pico Python SDK documentation (“3.9.3. UART TX”) that shows how to use the PIO state machines to provide eight serial port transmit functions on consecutive IO pins.  That is the approach used here, so the basic connectivity is as follows:

  • Pico hardware UART 0 RX = MIDI In (using 3.3V friendly serial port levels).
  • Pico IO pins 6-13 = MIDI Out pins (one each for MIDI channels 1 to 8).

As an aside, I’ve decided it is well worth taking a “Sharpie” to a breadboard to highlight where the key power and GND pins are on a Pico as shown below.  It makes it a lot easier when adding connecting wires.  If you want a more professional looking solution, I’ve noticed that you can now get a solderless breadboard already labelled up for the Pico (see here)!  But I digress…

IMG_5163

In terms of testing the functioning of the MIDI out pins there are several options, but first of all of course we need a way of generating MIDI signals on a range of channels to send to the Pico in the first place.  I used the following:

  • Using another Raspberry Pi Pico to play the “Bach” from MIDI, MicroPython and the Raspberry Pi Pico as a first “sanity check”, although as written this only uses MIDI channel 1.
  • Using MIDIOx to send MIDI messages manually from a PC in “KBD” mode for each of the eight channels.
  • Using MIDIBar to “play” my Arduino Tone One Year On! tune from a PC, which has individual parts for MIDI channels 1 to 9 and 11 to 14, although only channels 1 to 8 will sound.

In terms of examining the eight outputs, again there are a number of options:

  • Using my Arduino MIDI Logic Analyser to check them one at a time for the basics.
  • Using a Pro Mega 2560’s three additional serial lines to echo what they receive to the USB/Serial0 Arduino serial monitor.
  • Add some LEDs and resistors to the outputs to show when they are active (I tried this, but didn’t find it helped very much, as the LEDs weren’t really off long enough to really show what was going on).
  • Hooking up eight of my 12 Arduino Nanos from my Arduino Tone One Year On! to the eight individual output ports.

Here is a photo of me using my Pro Mega.  And another of the last option in action – getting the Pico to drive the first eight of my 12-tone Arduino Nanos.  Note that the Nano’s had to stay connected via VIN, GND and the audio out (D2) to the “back-board” using jumper wires – it was just the RX pins that had to be connected over to the Pico.  I also linked the GND from the backboard to the Pico too.

IMG_5164IMG_5169

Other options might include hooking up a MIDI interface to each in turn to hear individual channels being pulled out; or using another Arduino with a SoftwareSerial implementation to show what is going on on the serial ports. I am sure there are other options too.

The Code

This code uses Micropython, which is still the default python environment for the Raspberry Pi Pico and supports the PIO pretty well.

I wanted to do this in “Arduino C” but the Arduino Pico environment doesn’t support the PIO directly yet.  There is an outstanding issue to look at it, but for now the only option is to “assemble” the PIO using the Raspberry Pi’s “pioasm” tool (available with the official C/C++ SDK) and then manually add the resulting “h” file into your Arduino project. I might look at this at some point but for now, I went with Micropython which fully supports the PIO subsystem.  I don’t have any of the official C/C++ SDK installed at present.

As hinted at above, the idea is to take a MIDI feed from the hardware serial interface (UART 0), examine the MIDI channel number of any MIDI messages and then output it to the appropriate serial interface TX engine built using the PIO subsystem.  There will be one output per MIDI channel, so as there are 8 PIO “state machines” it is possible to have 8 serial outputs, which means 8 MIDI channels are supported.  I’ve left this configurable for now, so you can choose which 8 MIDI channels to output, but by default, I’ve split out channels 1 to 8.

I’m not going to go over the details of what the PIO system is all about – the tutorials referenced at the start of the post do that very well already.  There is a C/C++ SDK implementation of a single serial TX output and a Micropython implementation of eight TX outputs, so I started with those, which you can find yourself on GitHub in the following places:

Decoding MIDI

One problem with using Micropython over Adafruit’s Circuitpython is the lack of MIDI support “out of the box”.  But I’ve already written a simple MIDI decoder for a previous project, so I took some time to refactor that out into a simple Python “class” of its own.  I use quotes meaningfully here – I am not really an OO programmer – my background is embedded C, not C++, but I’ve followed some examples and I can recommend “Python Crash Course” by Eric Matthes!  If you have programming suggestions for how to do this properly, feel free to drop them in the comments.  You can see the resulting “SimpleMIDIDecoder.py” in the GitHub repository.

I designed my “class” on the following principles:

  • The main “read” function will take a byte at a time and maintain the state internally as to where in the MIDI message it is.
  • When a complete MIDI message is found, it will invoke a “callback” function to decide what to do with it.
  • It should (if I got my sums right) correctly handle MIDI “Running Status” following the algorithm suggested here.
  • Any instance of the class should run independently, allowing the parallel decoding of several MIDI streams at the same time (in theory – this is, as yet, untested).

The most basic usage uses built-in callback functions to just print out MIDI messages.  Here is an example that just sends any bytes received over the serial port to the decoder, one byte at a time.  The decoder will print out any complete MIDI messages.

import machine
import SimpleMIDIDecoder

uart = machine.UART(0,31250)
md = SimpleMIDIDecoder.SimpleMIDIDecoder()

while True:
if (uart.any()):
md.read(uart.read(1)[0])

Here is a slightly more complete example using user-provided callbacks instead of the built-in ones.  I still only dump things to the serial console though, but you get the idea.

import machine
import SimpleMIDIDecoder

uart = machine.UART(0,31250)
md = SimpleMIDIDecoder.SimpleMIDIDecoder()

def doMidiNoteOn(ch,cmd,note,vel):
print("Note On \t", note, "\t", vel)

def doMidiNoteOff(ch,cmd,note,vel):
print("Note Off\t", note, "\t", vel)

def doMidiThru(ch,cmd,data1,data2):
print("Thru\t", cmd, "\t", data1, "\t", data2)

md = SimpleMIDIDecoder.SimpleMIDIDecoder()
md.cbNoteOn (doMidiNoteOn)
md.cbNoteOff (doMidiNoteOff)
md.cbThru (doMidiThru)

while True:
if (uart.any()):
md.read(uart.read(1)[0])

As you can see, at the time of writing there are just three functions: NoteOn, NoteOff, and a default “Thru” for everything else.  If the Thru function is called for a Program Change or Channel Pressure message (0xCn or 0xDn respectively) then the d2 parameter will be set to -1 as these are single data byte messages.

The decoder just ignores any System Common or System Real-time messages – basically anything with a command in the 0xF0 to 0xFF range is just dropped.  It is only a simple decoder after all.

The decoder also probably won’t handle deformed MIDI messages very well – I’ve only really tested it with note on/note off messages in anger.

Consider it a “work in progress” at present and expect it to keep getting updated as I use it more myself.

PIO MIDI Splitting

To tie the two things together I’ve created a “pio_midi_send” function that takes a MIDI command, channel and two data bytes and chooses which PIO output channel to send them to based on the MIDI channel.

def pio_midi_send(cmd, ch, b1, b2):
if ch < MIDI_CH_BASE:
return

if (ch > MIDI_CH_BASE+7):
return

midiuart = ch - MIDI_CH_BASE
sm = tx_uarts[midiuart]
b0 = cmd + ch-1
sm.put(b0)
sm.put(b1)
sm.put(b2)

The channel number is 1-indexed, i.e. 1 to 16, so needs to be translated to a zero-index, i.e. 0 to 15, to index into the tx_uarts list of pio statemachines, and also to be used “on the wire” in the MIDI message.

MIDI_CH_BASE defines the first MIDI channel to be mapped to the PIO.  I’ve set it at 1 by default to map MIDI channels 1 to 8 to the eight PIO output channels.  Notice how the code does nothing if we get a MIDI channel that is not mapped onto a PIO channel.

Linking the two parts together is therefore just a case of providing callback routines that call the pio_midi_send function with the right parameters:

def doMidiNoteOn(ch,cmd,note,vel):
pio_midi_send(cmd, ch, note, vel)

def doMidiNoteOff(ch,cmd,note,vel):
pio_midi_send(cmd, ch, note, vel)

def doMidiThru(ch,cmd,d1,d2):
if (d2 == -1):
pio_midi_send(cmd, ch, d1, 0)
else:
pio_midi_send(cmd, ch, d1, d2)

md = SimpleMIDIDecoder.SimpleMIDIDecoder()
md.cbNoteOn (doMidiNoteOn)
md.cbNoteOff (doMidiNoteOff)
md.cbThru (doMidiThru)

Notice that I check for single-data-byte commands in doMidiThru by checking if d2 is -1 before deciding what to output.  If there is only a single data byte then I send 0 as the third byte in the message.  I think this is an ok thing to do…

Premature Optimisation

I’ve also experimented with changing the PIO routine to take a three-byte MIDI message in one go as a 32-bit value into the PIO “FIFO” queue.  This basically required wrapping the existing pio_uart_tx routine in another loop to take the three bytes from the “output status register” (OSR).  Something like the following:

@asm_pio(sideset_init=PIO.OUT_HIGH, out_init=PIO.OUT_HIGH, out_shiftdir=PIO.SHIFT_RIGHT)
def uart_tx():
pull()
set(y,2)
label("wordloop")

(rest of the uart_tx_pio code for a single byte here)

jmp(y_dec, "wordloop")

Which I think worked ok, but I’m still learning about PIO so wasn’t sure if I’d got the timing exactly right.  It also means that what was a generic PIO routine is starting to become MIDI specific, but I did wonder if it would help the throughput.  But thinking on it some more, the PIO routines are slowed down from the system clock rate (typically 125MHz by default I believe) to the MIDI baud (31250 bits per second) which is quite slow by serial port and CPU standards anyway, so that is almost certainly the limiting factor rather than any Pico CPU to PIO “FIFO” queues or processor time.  So in the end I kept with the standard published single-byte solution and decided I’d see how it works before trying anything more fancy.

Pro Mega 2560 Serial MIDI Dumper

Part of my testing used one of the Pro Mega 2560 boards I mentioned previously, as these have four serial ports by default.  I had one wired up as follows:

  • Pro Mega GND to Pico GND
  • Pro Mega pin 14 to Serial 3 input
  • Pro Mega pin 16 to Serial 2 input
  • Pro Mega pin 18 to Serial 1 input

Connecting the serial inputs to three of the Pico’s PIO TX outputs at a time.  This works ok as although the Mega is a 5V device and the Pico is a 3V device, having 3V TX connected to 5V RX works fine.  Doing it the other way round (5V into a 3V pin) will likely damage the 3V device if you don’t take special precautions.

The Pro Mega was connected using its micro-USB port to the PC providing power and the serial console output.

The following Arduino code provides a simple “MIDI data dump” on three serial ports, outputting the information to the default “Serial 0” port – i.e. over USB to the Arduino’s serial monitor.

#include <MIDI.h>
MIDI_CREATE_INSTANCE(HardwareSerial, Serial1, MIDI1);
MIDI_CREATE_INSTANCE(HardwareSerial, Serial2, MIDI2);
MIDI_CREATE_INSTANCE(HardwareSerial, Serial3, MIDI3);

void setup()
{
Serial.begin(9600);
MIDI1.begin(MIDI_CHANNEL_OMNI);
MIDI2.begin(MIDI_CHANNEL_OMNI);
MIDI3.begin(MIDI_CHANNEL_OMNI);
}

void loop()
{
if (MIDI1.read()) {
decodeMIDI(1, MIDI1.getType(), MIDI1.getData1(), MIDI1.getData2(), MIDI1.getChannel());
}
if (MIDI2.read()) {
decodeMIDI(2, MIDI2.getType(), MIDI2.getData1(), MIDI2.getData2(), MIDI2.getChannel());
}
if (MIDI3.read()) {
decodeMIDI(3, MIDI3.getType(), MIDI3.getData1(), MIDI3.getData2(), MIDI3.getChannel());
}
}

void decodeMIDI(int iface, int cmd, int d1, int d2, int ch) {
Serial.print(iface);
Serial.print(": 0x");
Serial.print(cmd>>4, HEX); Serial.print("\t");
Serial.print(ch); Serial.print("\t");
Serial.print(d1); Serial.print("\t");
Serial.print(d2); Serial.print("\n");
}

More complex MIDI “dumping” or decoding is possible of course, but I just wanted to see the raw data to know something was happening.  It might also be possible to add in some “SoftwareSerial” ports to get more coverage but this was enough to do what I needed.

By moving the three hook-up wires from the Mega to the different output pins from the Pico, I can get a rough idea that the Pico is doing its job across all eight output channels.

Summary

The system seems to work quite well but I need to validate it with a high through-put MIDI stream.  At present I don’t know if the Micropython environment and interpreted code has a significant overhead when handling a full MIDI stream or not.  I’m relatively comfortable with the output side – at the end of the day it will be more limited on the input side and processing really.

But as you can hear in the video, it seemed to cope with eight of the 12 channels from the Arduino Tone One Year On! so I’m working on the assumption it is now probably fine.

Other options I’ve considered include:

  • Hooking the PIO directly to the RX line to literally duplicate the incoming bit stream to output IO pins.  This would be fast but gives no option for doing any active filtering or processing based on the data.
  • Using some of the PIO as RX pins to give more of a “matrix” of input and output channels.  But the Pico has two hardware UARTs anyway, so a 2-in, 8-out mix shouldn’t be too much of a stretch.

Find it on GitHub here.  Note that you need both the SimpleMIDIChannelRouter.py and SimpleMIDIDecoder.py.

Closing Thoughts

One enhancement I’d like to look into is to send the information on non-routed channels to the hardware UART TX pin.  This would allow me to hook up a second Pico to handle the other 8 MIDI channels.  Two Picos should be able to provide a full 1 to 16 MIDI channel splitter from a single MIDI feed.

Having said that, I might be able to get away with having two Picos hanging off the same MIDI IN signal anyway, without any routing via the first Pico.  I’ll have to experiment.

Another great option would be being able to take the MIDI In via the USB link of the Pico. If it can support USB Host functionality (I know the hardware can) then you could plug a keyboard straight in. But it would really shine in USB device mode where you can use it as a PC MIDI interface with eight independent MIDI output lines.  That would be a really useful device to have, especially for my Lo-Fi Orchestra.

In principle as it stands, there could be three inputs – USB and two serial – to the eight outputs.  There are also a large number of unused IO pins on the Pico, so it may be possible to get some kind of intelligent switching/routing going on with a hardware “user interface” of sorts using the free IO pins.

If the performance seems to be ok, then there are many possibilities with a Micropython-programmable MIDI splitter/router/merger/filter hardware unit.  Ultimately it would be really nice to be able to make a Pi Pico version of some of these boards you can find on Tindie for the Teensy.

But in the short term, I’ll probably try to make some kind of MIDI routing panel for my CD Rack synth.

Kevin

15 thoughts on “Raspberry Pi Pico MIDI Channel Router

  1. Very nice …
    This will make me buy a 2040 …
    Just the theme i’m working on these days … thank you so much for sharing!

    Like

    1. You are most welcome! Do let me know how you get on. At least you might have fighting chance of actually being able to purchase a RP2040 based device at the moment. Other MCUs are becoming rather scarce!

      Like

  2. When you say that I will need both the simple midi decoder and the midi channel router, does it mean that I need to combine the two pieces of code? If I run the midi channel router code on its own, will it work? Stupid questions, I know, but I have next to zero experience with python. Really looking forward to getting it working. Thanks!

    Like

    1. There are no stupid questions! Only questions that don’t get asked 🙂

      You will need both files saved to the Pico separately. SImpleMIDIDecoder.py just needs to exist so it can be used by the Router code. To make the Router code run automatically the easiest way is to simply save it as main.py. Micropython will run main.py automatically on power up.

      So you should have two files on your Pico:
      * SimpleMIDIDecoder.py – just “as is”
      * main.py – actually the contents of SimpleMIDIChannelRouter.py – just save it as main.py instead

      Good luck!

      Like

    1. I’m sorry, I’m note quite sure what you’re asking here? I’ve no experience of the Orange Pi devices I’m afraid.

      Kevin

      Like

  3. Hi
    thank you for sharing this nice project

    I have a question if is possible

    Is it possible to sequence the outputs with the incomming midi notes?

    example sequence in/outs

    INPUT
    note 1
    note 2
    note 3

    OUTPUT
    note 1 chanel 1
    note 2 chanel 2
    note 3 chanel 3

    I mean every note in outputs the same note but in diferent chanels

    my intention is to make some kind of polyphonic multi midi device interface

    thank you

    Like

    1. I’m sure it is, but I must admit I’m not entirely clear on what you are trying to do. Are you re-writing channels here? How do the channels related to physical MIDI ports?

      Like

  4. Re: your “premature optimisation” – yes, this would absolutely be premature, and ultimately counter productive.

    The reason is that if you do this you can’t route a message from the hardware MIDI to the correct PIO channel until *all three bytes have been received*. The delay won’t be massive, but it’ll definitely take longer than just making the routing decision based on the first command byte.

    Like

Leave a comment