Raspberry Pi Pico I2C MIDI Interface – Part 4

For this final (for now) part of my experimenting with my Raspberry Pi Pico I2C MIDI Interface, I’ve set it up to talk to multiple Arduino’s, each with a speaker to act as an I2C MIDI “tone module”.

  • In part 1, I tested the concept of a Pico I2C MIDI interface.
  • In part 2, I wrote an I2C MIDI “driver” for the Adafruit MIDI library.
  • In part 3, I looked at measuring the latency of the links.
  • And now, I’m talking to eight Arduino’s!

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

  • 8x Arduino Uno, Nano, or similar
  • Raspberry Pi Pico
  • 3.3V to 5V level shifter
  • 2x 10kΩ resistors
  • 8x old 4Ω or 8Ω loudspeakers
  • 8x 220Ω resistors (for the loudspeakers)
  • Solderless breadboards and jumper wires

The Circuit

RaspberryPiPicoUSBi2cMIDIRouter_bb

The concept I’m aiming for, is to have the Pico acting as a USB MIDI interface to 8 Arduino’s via I2C MIDI.  The Arduinos are all chained so that their I2C and 5V power lines are connected together.  One of the Arduinos is powered via USB.  Each Arduino has a speaker connected via a 220Ω resistor to D8.  If you are using Uno’s then you can use both A4/A5 and SDA/SCL to connect them up. I’ve also used some solderless breadboards to help where necessary too.

The Pico is connected over USB to a PC for MIDI data and power – when powered on it will appear as “CircuitPython Audio” device.  It is connected to the first Arduino’s I2C bus via a level shifter as shown.  It also has two pull-ups on its own I2C bus.

In my test system, I have a mix of Arduino Uno and Nano clones (alongside my original Uno and Nano).  I popped the whole thing onto a tray to make it easy to move around!

IMG_5713

The Code: Pico I2C MIDI Router

The Pico will be the I2C CONTROLLER and the Arduinos will all be set up as PERIPHERALS.

I am using my i2cmidi.py “driver” for the Adafruit MIDI library, but I need to create an instance of it for each remote endpoint it has to talk to.  I do this using an array of both I2C MIDI instances and Adafruit MIDI decoders, as follows.

addr = 0x40
i2c = busio.I2C(board.GP13, board.GP12)

imidi = []
for i in range (8):
  im = i2cmidi.I2CMIDI(i2c, addr+i*2, i2cmidi.I2CMIDI_CTRL)
  imidi.append(im)

midi = []
for m in range(8):
  mid = adafruit_midi.MIDI(midi_out=imidi[m], out_channel=m)
  midi.append(mid)

This creates a series of MIDI OUT ports (in the midi[] array) for channels 0 to 7 (8 devices), using I2C addresses 0x40 to 0x4E.  I’m using the formula:

  • I2C Address = 0x40 + MIDI CHANNEL (0-indexed) * 2

I then just need a USB MIDI device for the input.

usbmidi = adafruit_midi.MIDI(midi_in=usb_midi.ports[0])

and everything is ready to go.  Note that the USB MIDI port is listening on all MIDI channels.

The actual routing proceeds as follows:

While True:
  Check for MIDI messages over USB
  IF message found THEN
    Grab the MIDI channel
    Send it to the right board over I2C MIDI

Note that to use serial MIDI rather than USB MIDI as the input, simply requires a different initialisation as follows:

uart = busio.UART(tx=board.GP0, rx=board.GP1, baudrate=31250, timeout=0.001)
uartmidi = adafruit_midi.MIDI(midi_in=uart)

Of course, this would then require a physical serial MIDI interface connecting to UART 0 (in this case) to work.

Here is the complete code for the Pico for the USB MIDI version talking to 8 Arduinos.

import board
import busio
import usb_midi
import adafruit_midi
import i2cmidi

from adafruit_midi.note_off import NoteOff
from adafruit_midi.note_on import NoteOn
from adafruit_midi.midi_message import MIDIUnknownEvent

I2CMIDIDEVICES = 8

addr = 0x40
i2c = busio.I2C(board.GP13, board.GP12)

imidi = []
for i in range (I2CMIDIDEVICES):
  im = i2cmidi.I2CMIDI(i2c, addr+i*2, i2cmidi.I2CMIDI_CTRL)
  imidi.append(im)

midi = []
for m in range(I2CMIDIDEVICES):
  mid = adafruit_midi.MIDI(midi_out=imidi[m], out_channel=m)
  midi.append(mid)

usbmidi = adafruit_midi.MIDI(midi_in=usb_midi.ports[0])

while True:
  msg = usbmidi.receive()
  if (isinstance(msg, MIDIUnknownEvent)):
    # Ignore unknown MIDI events
    pass
  #elif (isinstance(msg, NoteOff)):
    # Do something special with noteOff
  #elif (isinstance(msg, NoteOn)):
    # Do something special with noteOn
  elif msg is not None:
    ch = msg.channel
    if (ch < I2CMIDIDEVICES):
      midi[ch].send(msg)
  else:
    # Ignore "None"
    pass

Additional points to note:

  • I’m filtering out unknown MIDI events, partly as my Roland UM-ONE MIDI interface continually uses MIDI Active Sensing, which means that there is a continuous stream of single byte MIDI 0xFE system messages, which the Adafruit library doesn’t decode. I see no need to forward these to all the Arduinos!
  • I’ve left in some commented out lines for NoteOn and NoteOff to show how it could be used for some additional filtering and processing, but I’m not using these – I’m just sending any valid MIDI message on to the appropriate channel.
  • MIDI channels have to be contiguous and pair up with the I2C addresses according to the formula described above.

Note that if any of the Arduinos are not powered up when the Pico starts running the code, then there will be errors initialising the corresponding I2C MIDI instances on the Pico.

The Code: Arduino I2C MIDI Tone Module

This is a variant of the example I2C MIDI Receive code used in my Arduino I2C experiments.  It combines the Arduino MIDI tone module functionality with the I2C MIDI interface.

It needs to be set up for each Arduino with the appropriate MIDI channel, and then the I2C peripheral address is calculated accordingly.  These need to be contiguous, so you’ll need to build this for MIDI channels 1 to 8.

#define MIDI_CHANNEL 1 // MIDI Receive channel choose 1-16
#define I2CMIDIBASE 0x40
#define I2CMIDIADDR (I2CMIDIBASE + (MIDI_CHANNEL-1)*2)
I2CMIDI_CREATE_INSTANCE(I2CMIDIPERIPHERAL, I2CMIDIADDR, I2CMIDI);

The rest of the code is pretty much as for the Arduino MIDI Tone Module, using callback functions to start and stop the tones in response to MIDI NoteOn and NoteOff messages received.

It requires the “pitches.h” file from the built-in Arduino Tone Melody example. The speaker is configured for D8.

Find it on GitHub here.

Closing Thoughts

In the video I’ve played the MIDI file from my Arduino Tone One Year On! 12-tone Arduino set up.  Of course in this version you can only hear MIDI channels 1 to 8, but it gives you an idea. In particular towards the end of the demo, you can hear some triplets articulated very cleanly which gives me confidence that the performance of the system in general is actually pretty good.

There are major limitations of course (some of which I mentioned last time) so really I’m still treating this as an “advanced” project and a “bit of fun”.

In terms of future enhancements, there are several options:

  • It is a shame there is no “broadcast” – the system effectively replaces MIDI channels with I2C addresses, but there is no reason why a single address can’t receive many MIDI channels.
  • The Pico end ought to be more forgiving of missing I2C nodes. I’ll need to look into that (I think there might be an option).
  • I’d like to try a full 16-channel system at some point from a single Pico, but I’m not sure I have the hardware (or patience) to do it!
  • I think there is might be a future activity to re-build the Lo-Fi Orchestra to use I2C MIDI with a Pico router…

So, I’m probably leaving it there for now.  If you fancy having a go and do anything interesting with any of this, do please let me know!  Just don’t use it for anything critical 🙂

Kevin

Leave a comment