I was going to leave things at Part 3 blog-wise, and just get on with filling in the gaps in code now, but I’ve come back to add a few more notes. But this is likely to be the final part now.
Recall so far, I have:
- Part 1 where I work out how to build Synth_Dexed using the Pico SDK and get some sounds coming out.
- Part 2 where I take a detailed look at the performance with a diversion into the workings of the pico_audio library and floating point maths on the pico, on the way.
- Part 3 where I managed to get up to 16-note polyphony, by overclocking, and some basic serial MIDI support.
This is building on the last part and includes notes on how I’ve implemented the following:
- Fuller MIDI support, including control change, program change and pitch bend messages.
- Voice and voice banks, selectable over MIDI.
- MIDI SysEx messages for voice parameters.
- USB MIDI device support.
The latest code can be found on GitHub here: https://github.com/diyelectromusic/picodexed
Warning! I strongly recommend using old or second hand equipment for your experiments. I am not responsible for any damage to expensive instruments!
If you are new to microcontrollers, see the Getting Started pages.
MIDI Support
I’m not going to walk through all the details of how I’ve added MIDI but suffice to say that once again the implementation owes a lot to MiniDexed and the Arduino MIDI Library.
At the time of writing the following are all supported as they were already supported in Synth_Dexed, so I just needed to glue the bits together.
Channel Voice Messages (only channel 1 at present)
0x80 | MIDI Note Off | note=0..127, vel=0..127 |
0x90 | MIDI Note On | note=0..127, vel=0..127 |
0xA0 | Channel Aftertouch | note=0..127, val=0..127 |
0xB0 | Control Change | See below |
0xC0 | Program Change | 0..31 (If used with BANKSEL) 0..127 (if used independently) |
0xE0 | Pitch Bend | 0..16383 (in LSB/MSB 2×7-bit format) |
Channel Control Change Messages
0 | Bank Select (MSB) | 0 |
1 | Modulation | 0..127 |
2 | Breath Control | 0..127 |
4 | Foot Control | 0..127 |
7 | Channel Volume | 0..127 |
32 | Bank Select (LSB) | 0..8 |
64 | Sustain | <=63 Off, 64=> On |
65 | Portamento | <=63 Off, 64=> On |
95 | Master Tune | 0..127 * |
120 | All Sound Off | 0 |
123 | All Notes Off | 0 |
126 | Mono Mode | 0 ** |
127 | Poly Mode | 0 |
* There is a bug with the master tuning. It ought to accept -99 to 99 I believe, but only 0..99 will actually register and there is no way to send -99 via MIDI at the moment. I need to read up on what is going on here and what it ought to do!
** The Mono Mode parameter has the option for specifying how many of the playable voices can be dedicated to mono mode (at least I think that is what it is saying). I only support a value of 0 which I believe is meant to mean “all available voices”.
System Messages
0xF0..0xF7 | Start/End System Exclusive | See below |
0xFE | Active Sensing | Filtered out |
0xFn | Other system messages | Ignored |
System Exclusive Messages
Any valid Yamaha (DX) system exclusive messages are passed straight into Synth_Dexed. A Yamaha (DX) message has the following format (see the “DX7IIFD/D Supplemental Booklet: Advanced MIDI Data and Charts”):
F0 - start SysEx message
43 - Yamaha manufacturer ID
sd - s=substatus (command class:0,1,2); d=device ID (0..F)
.. data ..
F7 - end SysEx message
The device ID can be set using the UI on a real DX7 to a value between 1 and 16, which becomes a value between 0 and 15 (0..F) as part of the SysEx message (see “DX7IIFD/D Supplemental Booklet: Advanced MIDI Applications, Section 8”). It is a Systems Exclusive value analogous to the MIDI channel for regular channel messages.
There are a range of Sys Ex parameter settings that have been passed onto Synth_Dexed as follows:
Mono Mode | 0..1 |
Pitch Bend Range | 0..12 |
Pitch Bend Step | 0..12 |
Portamento Mode | 0..1 |
Portamento Glissando | 0..1 |
Portamento Time | 0..99 |
Mod Wheel Range | 0..99 |
Mod Wheel Target | 0..7 |
Foot Control Range | 0..99 |
Foot Control Target | 0..7 |
Breath Control Range | 0..99 |
Breath Control Target | 0..7 |
Aftertouch Range | 0..99 |
Aftertouch Target | 0..7 |
Voice Dump Load | <156 bytes of voice data> |
Voice Parameter Set | Parameter=0..155; Data=0..99 |
At this stage, all of the MIDI support is on a “it’s probably something like this” basis, so it will evolve as I find out what it is meant to be doing!
Voice and Bank Loading
Banks of voices are programmed directly into the code. There is a python script from Synth_Dexed that will take a .syx format voice bank and generate a block of C code. I’ve included a script to download the main 8 banks of standard DX voices and run the script:
#!/bin/sh
# Get voices from
# https://yamahablackboxes.com/collection/yamaha-dx7-synthesizer/patches/
mkdir -p voices
DIR="https://yamahablackboxes.com/patches/dx7/factory"
wget -c "${DIR}"/rom1a.syx -O voices/rom1a.syx
wget -c "${DIR}"/rom1b.syx -O voices/rom1b.syx
wget -c "${DIR}"/rom2a.syx -O voices/rom2a.syx
wget -c "${DIR}"/rom2b.syx -O voices/rom2b.syx
wget -c "${DIR}"/rom3a.syx -O voices/rom3a.syx
wget -c "${DIR}"/rom3b.syx -O voices/rom3b.syx
wget -c "${DIR}"/rom4a.syx -O voices/rom4a.syx
wget -c "${DIR}"/rom4b.syx -O voices/rom4b.syx
./synth_dexed/Synth_Dexed/tools/sysex2c.py voices/* > src/voices.h
This only needs to be run once to create the src/voices.h file which is then included in the build.
Voices have the following format:
uint8_t progmem_bank[8][32][128] PROGMEM =
{
{ // Bank 1
{<--128 bytes of packed voice data-->} // Voice 1
...
{<--128 bytes of packed voice data-->} // Voice 32
}
{ // Bank 2
...
}
...
{ // Bank 8
{<--128 bytes of packed voice data-->} // Voice 1
...
{<--128 bytes of packed voice data-->} // Voice 32
}
}
The system assumes 8 banks of 32 voices each, in the “packed” SYX header format, meaning each voice consists of 128 bytes.
MIDI Bank and Voice Selection
As there are only 8 banks, only BANKSEL (LSB) values 0..7 are valid. Program Change will work in two ways however:
- 0..31 will select voices 1 to 32 in the current bank.
- 31..127 will select voices from the following three adjacent banks.
To select any voice in all 8 banks thus requires the following sequence:
BANKSEL MSB = 0
BANKSEL LSB = 0..7
PROG CHANGE = 0..31
But if bank selection is skipped, then Program Change messages can still be used to select one of the first 128 voices across four consecutive banks.
USB MIDI
The Raspberry Pi Pico SDK uses the TinyUSB protocol stack to implement USB device or host modes and there is an additional option to implement a second USB host port using the Pico’s PIO.
However, USB MIDI appears to only be supported for USB devices at the time of writing, so I’m just using the built-in USB port as a USB device, based on the code provided as part of the TinyUSB examples (more details of how to get basic USB MIDI running here).
TinyUSB MIDI supports two interfaces for reading data, and this wasn’t immediately obvious from the example as that is only sending data and ignores anything coming in.
- USB MIDI Stream mode: this will fill a provided buffer with MIDI data received over USB.
- USB MIDI Packet mode: this will return each 4-byte USB packet individually.
From what I can see of the USB MIDI Spec, all MIDI messages are turned into 4-byte packets for transferring over USB. All normal MIDI messages will consist of 1, 2 or 3 byte messages, and so will fit in a packet each – any unused bytes are padded with 0.
However SysEx messages are a little more complicated and have to be split across multiple packets.
This is the format for a USB MIDI Event Packet (see the “Universal Serial Bus Device Class Definition for MIDI Devices”, Release 1.0):
The code index number is an indication of the contents of each packet. For channel messages, this is basically a repeat of the MIDI command, so a MIDI Note On message might look something like the following:
09 92 3C 64
Cable 0
Code Index Number 9
MIDI Cmd 0x90 (Note On)
MIDI Channel 3 (0x0=1; 0x1=2; 0x2=3; ... 0xF=16)
Note 0x3C (60 = C4)
Velocity 0x64 (100)
But things get a little more complex with System Common or System Exclusive messages which have their own set of codes, depending on the chunking of the packets required.
The critical ones for SysEx are CIN=4,5,6,7 which correspond to SysEx start and then various versions of continuation or end packets. So a larger SysEx message might look something like the following
04 F0 43 10 -- SysEx Start or Continuation
04 34 44 4D -- SysEx Start or Continuation
06 3E F7 00 -- SysEx End after two bytes
Complete message: F0 43 10 34 44 4D 3E F7
So, if I opt to use the packet interface to TinyUSB MIDI then all this has to be sorted out in user code myself. However, the streaming interface will take care of all this for me and just return a buffer full of “traditional” MIDI messages.
Note that there is no concept of Running Status in USB MIDI. Even the oldest USB standard protocol speeds are an order of magnitude, or more, higher than serial MIDI so it isn’t necessary. Every MIDI message will either be a complete 1,2,3 byte message in a single USB packet, or a SysEx multi-packet message as described above.
The basic structure of the USB MIDI handler is as follows:
Init:
Initialise TinyUSB MIDI stack
Process:
Run the TinyUSB MIDI task
IF TinyUSB says MIDI data available:
Call the stream API to fill our RX buffer
WHILE data in the RX buffer:
Call the MIDIParser which reads from the RX buffer
IF MIDI messages found:
Call the MIDI Message Handler
Read:
Grab the next byte from the RX buffer
I’ve actually split this over two files: usbmidi.cpp is the companion to serialmidi.cpp and provides the class that inherits from MIDIDevice (which provides the parser and message handler); usbtask.c provides the interface into the TinyUSB C driver code.
I haven’t done anything special with a USB manufacturer/vendor and device ID yet – so at some point I should see what TinyUSB is using by default and find something unique to PicoDexed (assuming I take it forward in any useful way).
Closing Thoughts
I have a fairly complete implementation now, which is quite nice. I do need to find some way to properly exercise the voice loading over SysEx and it would be good to get some idea of the performance when I throw a MIDI file at it over USB!
I’ve tested some of the parameter changes using the PC version of Dexed. When configured correctly, this can be used to send voice parameter changes to PicoDexed, but I haven’t found a way to download the entire voice as yet.
It’s a shame I can’t just plug in a USB MIDI controller and play it now, but I’ll work on some kind of interface board that should allow me to do it. It will need to be independently powered to act as a USB host anyway.
This is probably going to be my last blog post on PicoDexed for now, but I plan to keep tinkering away at the GitHub repository to see how things go. There are still a couple of limitations, the main one being that everything has to be hard-coded in at present. It would be nice to be able to have some kind of system configuration facility for the MIDI channel if nothing else.
At some point it would also be nice to have a build on the GitHub so others can try it too. And I still need to decide how best to manage the changes I needed to make to Synth_Dexed.
Kevin