I've seen a few dedicated units for sending and managing patches for older synths and whilst they look really neat, I thought it would be worth me having a go and seeing what I could do myself.
This is my first attempt at pretty much the simplest hardware "patch librarian" I could imagine - it will allow you to choose from a list of stored patches and send them over MIDI.

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.
- Pimoroni Pico Display (or another ST7789 display and some buttons).
- Raspberry Pi Pico compatible 5-pin serial MIDI OUT.
- Optional: Pimoroni "dual expander".
- A synth to control of course.
The Circuit

As this is using "off the shelf" components, there is no circuit diagram as such. I'm using a Pimoroni Pico display (details here) and a dual expander (details here). There is a strong possibility that any Pico display would work if you included the right modules and provided the correct pin configuration.
A serial (5-pin DIN) MIDI OUT interface is also required. That could be as simple as a MIDI socket and two resistors (as described here) or an off-the-shelf (3.3V compatible) module or a home made unit. I'm using my "reversible" MIDI OUT module from here in the above photo.
In terms of other connections, micro USB is required for power.
The Code
In terms of code, these were my main thoughts:
- I wanted to be able to store ".syx" files on the built-in disk with each file being a single patch.
- I wanted the display to give me a list of patches and give me basic navigation up and down the list.
- I wanted to be able to select one of the patches and have it sent out over MIDI directly from the corresponding .syx file.
- Ideally I'd support both USB MIDI and serial MIDI, but serial MIDI would be fine for a "first attempt".
The Pimoroni display is very well supported in MicroPython, but I've opted for CircuitPython as I wanted to use the "Pico as a filesystem" element of it (the CIRCUITPY "disk" bit) to make it easy to load files onto the Pico. The Adafruit SD7789 display driver supports the Pimoroni Display and there is some example code to get you going: see the "st7789_240x135_simpletest_Pimoroni_Pico_Display_Pack.py" Adafruit example.
The following Adafruit CircuitPython libraries are required:
- adafruit_st7789_display.mpy
- adafruit_display_text
I initially thought I'd want to use the Adafruit MIDI library too, but in the end I opted for a simple "grab the bytes from the file and send them over the serial port" approach, which makes things a lot simpler.
One surprise for me was finding out that by using the Adafruit displayio subsystem, it automatically attaches a Python console to your display whether you want it to or not! This means that you might end up with all sorts of junk "printed" to your display. There doesn't seem to be a way to turn this off, but adding a displayio.release_display() at the end of your code at least allows you to see the last thing you put on the display yourself during development.
This is how the Pimoroni display is setup. Notice it is an SPI display:
displayio.release_displays() tft_cs = board.GP17 tft_dc = board.GP16 spi_mosi = board.GP19 spi_clk = board.GP18 spi = busio.SPI(spi_clk, spi_mosi) display_bus = displayio.FourWire(spi, command=tft_dc, chip_select=tft_cs) display = ST7789( display_bus, rotation=270, width=240, height=135, rowstart=40, colstart=53 )
The four buttons on the display board were set up in an array as follows:
SW_A = 0 SW_B = 1 SW_X = 2 SW_Y = 3 # Pimoroni Switches: A, B, X, Y pimSw = [board.GP12, board.GP13, board.GP14, board.GP15] sws = [] lastsws = [] for sw in range(len(pimSw)): sw = digitalio.DigitalInOut(pimSw[sw]) sw.direction = digitalio.Direction.INPUT sw.pull = digitalio.Pull.UP sws.append(sw) lastsws.append(True)
I'll be using lastsws[] to look for HIGH to LOW transitions - the switches are pulled-up so when pressed will results in a LOW signal.
In terms of accessing the CIRCUITPY disk, I went for a simple approach of using some of the "os" functions (more here) to check for directories and files as follows.
try: os.chdir("/syx") except OSError as exec: if exec.args[0] == errno.ENOENT: print ("ERROR: No /syx directory found for patches") files = os.listdir("/syx") fnregex = re.compile("\.syx$") fnames = [] for f in files: fname = fnregex.split(f) fnames.append(fname[0]) This also uses the regular expressions support to pick out all files with a .syx extension and grab their names for the fnames[] array, although it is fairly crude. There is a large onus on us to name files correctly at the moment.
When it comes to opening a file to stream the bytes out over MIDI, I can use a basic "open" in "binary" module (i.e. don't interpret ASCII control characters) and sent that out over the serial port.
def sendSysExFromFile (fileidx): if fileidx >= len(fnames): return filename = fnames[fileidx] + ".syx" try: os.stat(filename) except OSError as exec: if exec.args[0] == errno.ENOENT: print ("ERROR: Cannot open filename: ", filename) else: print ("ERROR: Unknown error: ", exec.args[0]) return print ("Opening: ", filename) with open (filename, "rb") as fh: sysex = fh.read() written = uart.write(sysex) Notice how the open command uses the mode "rb" - i.e. read only, treat as a binary file, then it reads the whole file into memory before sending it out over the serial port.
When it comes to mapping names to the display, there are several key structures, lists and principles to be aware of:
- page is the current page of names we're currently working with.
- name is the current index into the list of names we are working with.
- There are 6 names per page, so the number of pages is the number of names / 6 (plus one to ensure we catch incomplete pages at the end).
- The name is just the filename without the ".syx" extension. The names are stored in the fnames[] list.
- There are just 6 name labels defined and displayed. When a page changes, the text in these six labels is updated. Each label is initialised as 20 spaces and from that point onwards text is just updated when required.
- The six labels are treated as a "Group" as far as the display is concerned so they get updated together.
The buttons are used as follows:
- SW A - Scroll "up", including going off one page onto the previous one.
- SW B - Scroll "down", including going off on page onto the next one.
- SW X - "send" the selected patch over MIDI from the corresponding ".syx" file.
- SW Y - Page "down" - to allow quicker navigation through lots of files.
The text is displayed with a "scale = 2" which makes it easier to see on a smaller display but obviously means you can't get as much on the screen.
There are many limitations though:
- Names are simply taken from the filenames.
- There are no "librarian" functions - it is assumed you'll do all that via the filing system directly.
- It is serial MIDI only.
- It is "send only" - there is no option to pull data off your synth.
- I have no idea what the capacity is or how it behalves with 100s of patches!
- It will only read patches from a single directory "/syx". There is no further structure.
But most importantly - there is no validation of the data. If it exists in a file and you tell it to send it to your synth, it will send it regardless - even if it is for a different manufacturer or could harm your synth! In fact it would even send a random blob of binary data if you called it a ".syx" file and selected it for sending.
You have to take all responsibility for ensuring the .syx files you obtain or create are suitable for use.
Find it on GitHub here.
Closing Thoughts
As always with Python, there isn't a lot of code, but what there is has taken me quite a while to work out! That is the problem with not being so personally familiar with the language and environment.
I had initially thought about how to do this in Arduino, but then decided that the memory constraints and additional requirements for some kind of filing system made the Pico a much more sensible choice. I still think that was the right choice.
I'm really pleased with the result as a "first go". There are many opportunities for improvements now:
- Allow the use of a rotary encoder.
- Support bigger displays.
- Allowing different directories of .syx files to support different synths.
- Possibly supporting a numeric keypad to allow entry "by number".
- A slicker "user interface" with more functions.
I'd like it to support USB MIDI too, but I've stayed away from the Adafruit MIDI library for this one to keep things simple. I don't know (yet) if there is an equivalent option for USB MIDI to just send it a block of data to output "raw".
In the video you can see a Pico loaded with the voices from "CoffeeShopped" for my Casio CZ-3000 synth.
And of course it would be really, really neat to put this all into a nice little stand-alone box with a nice case!
Kevin
No comments:
Post a Comment