Overview
Ever wanted to plug your modern, USB-only MIDI keyboard into a synth that only has a classic DIN-5 or TRS 3.5mm MIDI input but you can't? Shouldn't there be a simple cable to convert USB to classic MIDI? It turns out, hosting a USB device is difficult, but you can build the USB MIDI Host Messenger to solve this key problem!
The USB MIDI Host Messenger solves the problem of how to connect USB MIDI keyboards and other controllers with classic synths and drum machines without inserting a whole dang computer in the middle. Plus, you can use it to filter MIDI messages in order to re-map MIDI channels, expand your Program Change choices, and spy on CC values.
The USB Host Feather RP2040 takes care of it all, running a single Arduino sketch. You can even build upon the code further to add more features such as CC re-mapping, note scale quantization, keyboard splitting, or even even build chord modes and arpeggiators.
Page last edited June 19, 2024
Text editor powered by tinymce.
Arduino IDE Setup
The Arduino Philhower core provides support for RP2040 microcontroller boards. This page covers getting your Arduino IDE set up to include your board.
Arduino IDE Download
The first thing you will need to do is to download the latest release of the Arduino IDE. The Philhower core requires version 1.8 or higher.
Download and install it to your computer.
Once installed, open the Arduino IDE.
Adding the Philhower Board Manager URL
In the Arduino IDE, navigate to the Preferences window. You can access it through File > Preferences on Windows or Linux, or Arduino > Preferences on OS X.
The Preferences window will open.
In the Additional Boards Manager URLs field, you'll want to add a new URL. The list of URLs is comma separated, and you will only have to add each URL once. The URLs point to index files that the Board Manager uses to build the list of available & installed boards.
Copy the following URL.
https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json
Add the URL to the the Additional Boards Manager URLs field (highlighted in red below).
Click OK to save and close Preferences.
Add Board Support Package
In the Arduino IDE, click on Tools > Board > Boards Manager. If you have previously selected a board, the Board menu item may have a board name after it.
In the Boards Manager, search for RP2040. Scroll down to the Raspberry Pi Pico/RP2040/RP2350 by Earle F Philhower, III entry. Click Install to install it. If it was previously installed, look to make sure you have the latest version.
Once installation is complete, click Close to close the Boards Manager.
Choose Your Board
In the Tools > Boards menu, you should now see Raspberry Pi RP2040 Boards (possibly followed by a version number).
Navigate to the Raspberry Pi Pico RP2040/RP2350 menu. You will see the available boards listed.
Navigate to the Raspberry Pi RP2040 Boards menu and choose Adafruit Feather RP2040 USB Host.
Now you're ready to begin using Arduino with your RP2040 board!
Troubleshooting
If you have any strange errors after updating the Raspberry Pi Pico/RP2040 by Earle F Philhower, III board support package (BSP) from the boards manager you may need to start with a fresh install of the BSP. Close out of the Arduino IDE and navigate to your Arduino packages folder: C:\Users\[username]\AppData\Local\Arduino15\packages and delete the /rp2040 folder. After that, open the Arduino IDE and follow the steps above for installing the BSP. The errors should not occur.
Page last edited June 19, 2024
Text editor powered by tinymce.
Arduino Usage
Now that you've set up the Arduino IDE with the Philhower RP2040 Arduino core, you're ready to start using Arduino with your RP2040.
RP2040 Arduino Pins
There is no pin remapping for Arduino on the RP2040. Therefore, the pin names on the top of the board are not the pin names used for Arduino. The Arduino pin names are the RP2040 GPIO pin names.
To find the Arduino pin name, check the PrettyPins diagram found on the Pinouts page. Each GPIO pin in the diagram has a GPIOx pin name listed, where x is the pin number. The Arduino pin name is the number following GPIO. For example, GPIO1 would be Arduino pin 1.
The Feather RP2040 USB Host has the GPIO pin names listed on the back of the board as x, where x is the pin number. So, for example, pin 5 would be Arduino pin 5.
Choose Your Board
Navigate to the Tools > Boards > Raspberry Pi RP2040 Boards menu. The Raspberry PI RP2040 Boards menu name may be followed by a version number.
Choose Feather RP2040 USB Host from the menu.
Begin by plugging in your board to your computer, and wait a moment for it to be recognised by the OS. It will create a COM/serial port that you can now select from the Tools > Port menu dropdown.
Open the Blink sketch by clicking through File > Examples > 01.Basics > Blink.
Click Upload. A successful upload will result in text similar to the following.
Once complete, the little red LED will begin blinking once every second! Try changing up the delay() timing to change the rate at which the LED blinks.
Manually Enter the Bootloader
If you get into a state with the bootloader where you can no longer upload a sketch, or you have uploaded code that crashes and doesn't auto-reboot into the bootloader, you may have to manually enter the bootloader.
To enter the bootloader, hold down the BOOT button, and while continuing to hold it (don't let go!), press and release the reset button. Continue to hold the BOOT button until the RPI-RP2 drive appears!
Once the RPI-RP2 drive shows up, your board is in bootloader mode. There will not be a port available in bootloader mode, this is expected.
Once you see RPI-RP2 drive, make sure you are no longer holding down any buttons (reset or boot0 button).
Now, click Upload on your sketch to try again.
Page last edited June 19, 2024
Text editor powered by tinymce.
Code the Messenger
The Arduino code for the project is available as a pre-compiled .UF2 file that you can drag and drop onto your USB Host Feather RP2040 board.
To enter the bootloader, hold down the BOOT button, and while continuing to hold it (don't let go!), press and release the reset button. Continue to hold the BOOT button until the RPI-RP2 drive appears!
Once you see RPI-RP2 drive, release the BOOT button. You should longer be holding down any buttons (reset or boot0 button).
You will see a new disk drive appear called RPI-RP2
Drag the USB_MIDI_Host_Messenger_Arduino.uf2 file to RPI-RP2
This will take a few moments and then the board will restart -- the RPI-RP2 drive will disappear from your computer's list of drives and then the code will run on the Feather.
Customize
If you'd like to look at the code or customize it further, you can download the Arduino sketch file below.
// SPDX-FileCopyrightText: 2024 john park for Adafruit Industries
//
// SPDX-License-Identifier: MIT
/**
* For USB MIDI Host Feather RP2040 with mini OLED FeatherWing and MIDI FeatherWing
* Modified 12 Jun 2024 - @todbot -- added USB MIDI forwarding
* Modified by @johnedgarpark -- added UART MIDI forwarding and display/message filtering
* originally from: https://github.com/rppicomidi/EZ_USB_MIDI_HOST/blob/main/examples/arduino/EZ_USB_MIDI_HOST_PIO_example/EZ_USB_MIDI_HOST_PIO_example.ino
*/
/*
* The MIT License (MIT)
*
* Copyright (c) 2023 rppicomidi
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
/**
* This demo program is designed to test the USB MIDI Host driver for a single USB
* MIDI device connected to the USB Host port. It also
* forwards MIDI received from the USB MIDI device to USB and UART MIDI devices.
*
* This program works with a single USB MIDI device connected via a USB hub, but it
* does not handle multiple USB MIDI devices connected at the same time.
*
* Libraries (all available via library manager):
* - MIDI -- https://github.com/FortySevenEffects/arduino_midi_library
*/
// Be sure to set the CPU clock to 120MHz or 240MHz before uploading to board
// USB Stack is TinyUSB
// Press A to change output MIDI channel
// Press B to change Program Change banks in groups of 8
// Press C for MIDI panic
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
//#define WIRE Wire1 //only if display needs it.
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 32 // OLED display height, in pixels
#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for OLED FeatherWing 128x32
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
//Screen buttons
const int buttonAPin = 9;
const int buttonBPin = 6;
const int buttonCPin = 5;
int buttonAState;
int buttonBState;
int buttonCState;
int lastButtonAState;
int lastButtonBState;
int lastButtonCState;
unsigned long lastDebounceATime = 0;
unsigned long lastDebounceBTime = 0;
unsigned long lastDebounceCTime = 0;
unsigned long debounceDelay = 50;
int userChannel = 1; //1-16
int userProgOffset = 0;
#include <MIDI.h>
#if defined(USE_TINYUSB_HOST) || !defined(USE_TINYUSB)
#error "Please use the Menu to select Tools->USB Stack: Adafruit TinyUSB"
#endif
#include "pio_usb.h"
#define HOST_PIN_DP 16 // Pin used as D+ for host, D- = D+ + 1
#include "EZ_USB_MIDI_HOST.h"
// USB Host object
Adafruit_USBH_Host USBHost;
USING_NAMESPACE_MIDI
USING_NAMESPACE_EZ_USB_MIDI_HOST
RPPICOMIDI_EZ_USB_MIDI_HOST_INSTANCE(usbhMIDI, MidiHostSettingsDefault)
Adafruit_USBD_MIDI usb_midi; // USB MIDI object
MIDI_CREATE_INSTANCE(Adafruit_USBD_MIDI, usb_midi, MIDIusb); // USB MIDI
MIDI_CREATE_INSTANCE(HardwareSerial, Serial1, MIDIuart); // Serial MIDI over MIDI FeatherWing
static uint8_t midiDevAddr = 0;
static bool core0_booting = true;
static bool core1_booting = true;
/* MIDI IN MESSAGE REPORTING */
static void onMidiError(int8_t errCode)
{
Serial.printf("MIDI Errors: %s %s %s\r\n", (errCode & (1UL << ErrorParse)) ? "Parse":"",
(errCode & (1UL << ErrorActiveSensingTimeout)) ? "Active Sensing Timeout" : "",
(errCode & (1UL << WarningSplitSysEx)) ? "Split SysEx":"");
}
int last_cc_cntrl = 1;
static void midiPanic()
{
for (int i=0; i<128; i++)
{
MIDIusb.sendNoteOff(i, 0, userChannel);
MIDIuart.sendNoteOff(i, 0, userChannel);
Serial.printf("note %u off\r\n", i);
last_cc_cntrl = 0; // dirty this
}
}
static void onNoteOff(Channel channel, byte note, byte velocity)
{
MIDIusb.sendNoteOff(note, velocity, userChannel);
MIDIuart.sendNoteOff(note, velocity, userChannel);
Serial.printf("ch%u: Note off#%u v=%u\r\n", userChannel, note, velocity);
display.setCursor(0,12);
display.setTextColor(SSD1306_WHITE, SSD1306_BLACK);
display.setTextWrap(false);
display.printf("Ch %u > %u Note %u \r\n", channel, userChannel, note);
display.display();
last_cc_cntrl = 0;
}
static void onNoteOn(Channel channel, byte note, byte velocity)
{
MIDIusb.sendNoteOn(note, velocity, userChannel);
MIDIuart.sendNoteOn(note, velocity, userChannel);
Serial.printf("ch%u: Note on#%u v=%u\r\n", userChannel, note, velocity);
display.setCursor(0,12);
display.setTextColor(SSD1306_WHITE, SSD1306_BLACK);
display.setTextWrap(false);
display.printf("Ch %u > %u Note %u o\r\n", channel, userChannel, note);
display.display();
last_cc_cntrl = 0;
}
static void onPolyphonicAftertouch(Channel channel, byte note, byte amount)
{
Serial.printf("ch%u: PAT#%u=%u\r\n", userChannel, note, amount);
MIDIusb.sendAfterTouch(note, amount, userChannel);
MIDIuart.sendAfterTouch(note, amount, userChannel);
}
static void onControlChange(Channel channel, byte controller, byte value)
{
MIDIusb.sendControlChange(controller, value, userChannel);
MIDIuart.sendControlChange(controller, value, userChannel);
Serial.printf("Ch %u CC#%u=%u\r\n", userChannel, controller, value);
if (last_cc_cntrl != controller){
display.setTextColor(SSD1306_WHITE, SSD1306_BLACK);
display.setTextWrap(false);
display.setCursor(0,12);
display.printf("CC# %u \r\n", controller);
display.display();
last_cc_cntrl = controller;
}
}
static void onProgramChange(Channel channel, byte program)
{
Serial.printf("ch%u: Prog=%u\r\n", userChannel, program);
MIDIusb.sendProgramChange(program + userProgOffset, userChannel);
MIDIuart.sendProgramChange(program + userProgOffset, userChannel);
display.setTextColor(SSD1306_WHITE, SSD1306_BLACK);
display.setCursor(0,24);
display.printf("Progs %u-%u [%u] \r\n", userProgOffset, (userProgOffset + 7), (program + userProgOffset));
display.display();
last_cc_cntrl = 0; // dirty this
}
static void onAftertouch(Channel channel, byte value)
{
Serial.printf("ch%u: AT=%u\r\n", userChannel, value);
MIDIusb.sendAfterTouch(value, userChannel);
MIDIuart.sendAfterTouch(value, userChannel);
}
static void onPitchBend(Channel channel, int value)
{
Serial.printf("ch%u: PB=%d\r\n", userChannel, value);
MIDIusb.sendPitchBend(value, userChannel);
MIDIuart.sendPitchBend(value, userChannel);
}
static void onSysEx(byte * array, unsigned size)
{
Serial.printf("SysEx:\r\n");
unsigned multipleOf8 = size/8;
unsigned remOf8 = size % 8;
for (unsigned idx=0; idx < multipleOf8; idx++) {
for (unsigned jdx = 0; jdx < 8; jdx++) {
Serial.printf("%02x ", *array++);
}
Serial.printf("\r\n");
}
for (unsigned idx = 0; idx < remOf8; idx++) {
Serial.printf("%02x ", *array++);
}
Serial.printf("\r\n");
}
static void onSMPTEqf(byte data)
{
uint8_t type = (data >> 4) & 0xF;
data &= 0xF;
static const char* fps[4] = {"24", "25", "30DF", "30ND"};
switch (type) {
case 0: Serial.printf("SMPTE FRM LS %u \r\n", data); break;
case 1: Serial.printf("SMPTE FRM MS %u \r\n", data); break;
case 2: Serial.printf("SMPTE SEC LS %u \r\n", data); break;
case 3: Serial.printf("SMPTE SEC MS %u \r\n", data); break;
case 4: Serial.printf("SMPTE MIN LS %u \r\n", data); break;
case 5: Serial.printf("SMPTE MIN MS %u \r\n", data); break;
case 6: Serial.printf("SMPTE HR LS %u \r\n", data); break;
case 7:
Serial.printf("SMPTE HR MS %u FPS:%s\r\n", data & 0x1, fps[(data >> 1) & 3]);
break;
default:
Serial.printf("invalid SMPTE data byte %u\r\n", data);
break;
}
}
static void onSongPosition(unsigned beats)
{
Serial.printf("SongP=%u\r\n", beats);
MIDIusb.sendSongPosition(beats);
MIDIuart.sendSongPosition(beats);
}
static void onSongSelect(byte songnumber)
{
Serial.printf("SongS#%u\r\n", songnumber);
MIDIusb.sendSongSelect(songnumber);
MIDIuart.sendSongSelect(songnumber);
}
static void onTuneRequest()
{
Serial.printf("Tune\r\n");
MIDIusb.sendTuneRequest();
MIDIuart.sendTuneRequest();
}
static void onMidiClock()
{
Serial.printf("Clock\r\n");
MIDIusb.sendClock();
MIDIuart.sendClock();
}
static void onMidiStart()
{
Serial.printf("Start\r\n");
MIDIusb.sendStart();
MIDIuart.sendStart();
}
static void onMidiContinue()
{
Serial.printf("Cont\r\n");
MIDIusb.sendContinue();
MIDIuart.sendContinue();
}
static void onMidiStop()
{
Serial.printf("Stop\r\n");
MIDIusb.sendStop();
MIDIuart.sendStop();
}
static void onActiveSense()
{
Serial.printf("ASen\r\n");
}
static void onSystemReset()
{
Serial.printf("SysRst\r\n");
}
static void onMidiTick()
{
Serial.printf("Tick\r\n");
}
static void onMidiInWriteFail(uint8_t devAddr, uint8_t cable, bool fifoOverflow)
{
if (fifoOverflow)
Serial.printf("Dev %u cable %u: MIDI IN FIFO overflow\r\n", devAddr, cable);
else
Serial.printf("Dev %u cable %u: MIDI IN FIFO error\r\n", devAddr, cable);
}
static void registerMidiInCallbacks()
{
auto intf = usbhMIDI.getInterfaceFromDeviceAndCable(midiDevAddr, 0);
if (intf == nullptr)
return;
intf->setHandleNoteOff(onNoteOff); // 0x80
intf->setHandleNoteOn(onNoteOn); // 0x90
intf->setHandleAfterTouchPoly(onPolyphonicAftertouch); // 0xA0
intf->setHandleControlChange(onControlChange); // 0xB0
intf->setHandleProgramChange(onProgramChange); // 0xC0
intf->setHandleAfterTouchChannel(onAftertouch); // 0xD0
intf->setHandlePitchBend(onPitchBend); // 0xE0
intf->setHandleSystemExclusive(onSysEx); // 0xF0, 0xF7
intf->setHandleTimeCodeQuarterFrame(onSMPTEqf); // 0xF1
intf->setHandleSongPosition(onSongPosition); // 0xF2
intf->setHandleSongSelect(onSongSelect); // 0xF3
intf->setHandleTuneRequest(onTuneRequest); // 0xF6
intf->setHandleClock(onMidiClock); // 0xF8
// 0xF9 as 10ms Tick is not MIDI 1.0 standard but implemented in the Arduino MIDI Library
intf->setHandleTick(onMidiTick); // 0xF9
intf->setHandleStart(onMidiStart); // 0xFA
intf->setHandleContinue(onMidiContinue); // 0xFB
intf->setHandleStop(onMidiStop); // 0xFC
intf->setHandleActiveSensing(onActiveSense); // 0xFE
intf->setHandleSystemReset(onSystemReset); // 0xFF
intf->setHandleError(onMidiError);
auto dev = usbhMIDI.getDevFromDevAddr(midiDevAddr);
if (dev == nullptr)
return;
dev->setOnMidiInWriteFail(onMidiInWriteFail);
}
/* CONNECTION MANAGEMENT */
static void onMIDIconnect(uint8_t devAddr, uint8_t nInCables, uint8_t nOutCables)
{
Serial.printf("MIDI device at address %u has %u IN cables and %u OUT cables\r\n", devAddr, nInCables, nOutCables);
midiDevAddr = devAddr;
registerMidiInCallbacks();
}
static void onMIDIdisconnect(uint8_t devAddr)
{
Serial.printf("MIDI device at address %u unplugged\r\n", devAddr);
midiDevAddr = 0;
}
/* MAIN LOOP FUNCTIONS */
static void blinkLED(void)
{
const uint32_t intervalMs = 1000;
static uint32_t startMs = 0;
static bool ledState = false;
if ( millis() - startMs < intervalMs)
return;
startMs += intervalMs;
ledState = !ledState;
digitalWrite(LED_BUILTIN, ledState ? HIGH:LOW);
}
// core1's setup
void setup1() {
#if ARDUINO_ADAFRUIT_FEATHER_RP2040_USB_HOST
pinMode(18, OUTPUT); // Sets pin USB_HOST_5V_POWER to HIGH to enable USB power
digitalWrite(18, HIGH);
#endif
//while(!Serial); // wait for native usb
Serial.println("Core1 setup to run TinyUSB host with pio-usb\r\n");
// Check for CPU frequency, must be multiple of 120Mhz for bit-banging USB
uint32_t cpu_hz = clock_get_hz(clk_sys);
if ( cpu_hz != 120000000UL && cpu_hz != 240000000UL ) {
delay(2000); // wait for native usb
Serial.printf("Error: CPU Clock = %lu, PIO USB require CPU clock must be multiple of 120 Mhz\r\n", cpu_hz);
Serial.printf("Change your CPU Clock to either 120 or 240 Mhz in Menu->CPU Speed \r\n");
while(1) delay(1);
}
pio_usb_configuration_t pio_cfg = PIO_USB_DEFAULT_CONFIG;
pio_cfg.pin_dp = HOST_PIN_DP;
USBHost.configure_pio_usb(1, &pio_cfg);
// run host stack on controller (rhport) 1
// Note: For rp2040 pico-pio-usb, calling USBHost.begin() on core1 will have most of the
// host bit-banging processing work done in core1 to free up core0 for other work
usbhMIDI.begin(&USBHost, 1, onMIDIconnect, onMIDIdisconnect);
core1_booting = false;
while(core0_booting) ;
}
// core1's loop
void loop1()
{
USBHost.task();
}
void setup()
{
TinyUSBDevice.setManufacturerDescriptor("LarsCo");
TinyUSBDevice.setProductDescriptor("MIDI Masseuse");
Serial.begin(115200);
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;); // Don't proceed, loop forever
}
// Show initial display buffer contents on the screen --
// the library initializes this with an Adafruit splash screen.
display.display();
delay(2000); // Pause for 2 seconds
// Clear the buffer
display.clearDisplay();
display.display();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE, SSD1306_BLACK);
display.setCursor(0,0);
display.print("USB MIDI Messenger");
display.setCursor(0,12);
display.print("Ch x > 1 Note_ CC#_\r\n");
display.setCursor(0,24);
display.printf("Progs %u-%u \r\n", userProgOffset, (userProgOffset + 7));
display.display();
pinMode(buttonAPin, INPUT_PULLUP);
pinMode(buttonBPin, INPUT); // OLED button B as a 100k pullup on it on 128x32 FW
pinMode(buttonCPin, INPUT_PULLUP);
MIDIusb.begin();
MIDIusb.turnThruOff(); // turn off echo
MIDIuart.begin(MIDI_CHANNEL_OMNI); // don't forget OMNI
// while(!Serial); // wait for serial port
pinMode(LED_BUILTIN, OUTPUT);
Serial.println("USB Host to MIDI Messenger\r\n");
core0_booting = false;
while(core1_booting) ;
}
void loop() {
// Handle any incoming data; triggers MIDI IN callbacks
usbhMIDI.readAll();
// Do other processing that might generate pending MIDI OUT data
// Tell the USB Host to send as much pending MIDI OUT data as possible
usbhMIDI.writeFlushAll();
// Do other non-USB host processing
blinkLED();
int readingA = digitalRead(buttonAPin);
int readingB = digitalRead(buttonBPin);
int readingC = digitalRead(buttonCPin);
if (readingA != lastButtonAState) {
lastDebounceATime = millis();
}
if (readingB != lastButtonBState) {
lastDebounceBTime = millis();
}
if (readingC != lastButtonCState) {
lastDebounceCTime = millis();
}
if ((millis() - lastDebounceATime) > debounceDelay) {
if (readingA != buttonAState) {
buttonAState = readingA;
if (buttonAState == LOW) {
userChannel = (userChannel % 16) + 1 ; // increment from 1-16
Serial.printf("Ch%u\r\n", userChannel);
display.setTextColor(SSD1306_WHITE, SSD1306_BLACK);
display.setCursor(0,12);
display.printf("Ch out %u \r\n", userChannel);
display.display();
}
}
}
if ((millis() - lastDebounceBTime) > debounceDelay) {
if (readingB != buttonBState) {
buttonBState = readingB;
if (buttonBState == LOW) {
userProgOffset = (userProgOffset + 8) % 128 ;
Serial.printf("Prog Progs %u through %u\r\n", userProgOffset, (userProgOffset + 7));
display.setTextColor(SSD1306_WHITE, SSD1306_BLACK);
display.setCursor(0,24);
display.printf("Progs %u-%u \r\n", userProgOffset, (userProgOffset + 7));
display.display();
}
}
}
if ((millis() - lastDebounceCTime) > debounceDelay) {
if (readingC != buttonCState) {
buttonCState = readingC;
if (buttonCState == LOW) {
midiPanic();
}
}
}
lastButtonAState = readingA;
lastButtonBState = readingB;
lastButtonCState = readingC;
}
How It Works
This sketch uses the EZ_USB_MIDI_HOST library by rppicomidi. It was derived from the PIO example code here.
Tod Kurt modified it to add USB MIDI forwarding (so incoming MIDI messages are received and then re-sent to a computer over the Feather's primary (non-Host) USB port.
John Park added the classic MIDI messaging output over UART, as well as message filtering to re-map MIDI channels, and OLED display/UI functionality.
Initial Setup and Libraries
Libraries
-
SPI.handWire.hfor SPI and I2C communication. -
Adafruit_GFX.handAdafruit_SSD1306.hfor OLED display control. -
MIDI.hfor MIDI communication. -
pio_usb.handEZ_USB_MIDI_HOST.hfor USB host support.
OLED Display Configuration
- Configures the OLED display with specified width, height, reset pin, and I2C address.
- Initializes the display and sets initial text.
Button Configuration
- Configures three buttons (A, B, C) with debounce logic to ensure reliable button presses.
MIDI Configuration
- Defines USB and UART MIDI interfaces.
- Sets up USB host and MIDI handling with callbacks for various MIDI messages.
MIDI Message Handling
The code includes a series of callback functions to handle various MIDI messages, including:
onNoteOffonNoteOnonPolyphonicAftertouchonControlChangeonProgramChangeonAftertouchonPitchBendonSysExonSMPTEqfonSongPositiononSongSelectonTuneRequestonMidiClockonMidiStartonMidiContinueonMidiStoponActiveSenseonSystemResetonMidiTickonMidiInWriteFail
Each of these functions forwards the MIDI message to both the USB and UART MIDI interfaces and updates the OLED display as needed. The midiPanic function is designed to send Note Off messages for all notes, effectively stopping all sound.
Core1 Setup and Loop
The Feather RP2040 has dual cores. The code sets up Core1 to handle the USB host stack:
- Initializes USB host configuration for MIDI devices.
- Runs the USB host task in a loop to manage MIDI device connections and data transfer.
Core0 Setup and Loop
Core0 handles the main program logic:
- Initializes Serial communication and the OLED display.
- Sets up the buttons and their debounce logic.
- Begins the MIDI interfaces and configures them to not echo messages (
turnThruOff).
The main loop on Core0 handles:
- Reading and processing button states to change the output MIDI channel, program change banks, and to send a MIDI panic message.
- Blinking an LED to indicate the program is running.
- Handling USB MIDI input and output.
Button Functionality
- Button A: Changes the output MIDI channel (1-16).
- Button B: Changes the program change bank in groups of 8.
- Button C: Sends a MIDI panic message, turning off all notes.
How it Works Together
Initialization
- Both cores are initialized, setting up USB host and MIDI interfaces.
- OLED display shows the initial message and sets up text properties.
Main Loop
- USB MIDI messages are read and processed, triggering appropriate callbacks.
- Button states are read and debounced. Button presses change MIDI settings or trigger panic.
- The OLED display is updated with the current MIDI channel, program bank, or control change as messages are received or settings are changed.
MIDI Message Handling
- Incoming MIDI messages are forwarded to both USB and UART MIDI interfaces.
- Messages are also displayed on the OLED screen and printed to the Serial monitor for debugging.
Page last edited June 19, 2024
Text editor powered by tinymce.
Build the Messenger
Quad 2x2 FeatherWing Prep
Solder socket headers into the Quad FeatherWing.
You'll add a slide switch to turn the Messenger on and off by shorting the Feather enable pin to ground.
Then, solder in the slide switch with enough height from the board to bend the switch sideways 90º as shown.
Solder a short length of wire from the common (center) leg of the switch to GND and another from the top leg to the Enable pin (marked En) on the FeatherWing.
Standoffs
Insert four M2.5 x 12mm M-F hex standoffs from under the Quad FeatherWing into the four corner mounting holes. Screw M2.4 x 6mm F-F hex standoffs on top of them. These will be used to fasten the board to the case.
Next, screw in seven M2.5 x 6mm hex standoffs onto the remaining mounting holes using seven M2.5 x 4mm screws. These will be used to secure the three Feather/FeatherWing positions we'll be populating, as shown.
MIDI FeatherWing
Solder in the DIN-5 MIDI jacks onto the MIDI FeatherWing. Then solder the header pins underneath.
Insert the MIDI FeatherWing into the Quad FeatherWing as shown.
Feather RP2040 and Battery
Solder header pins under the USB Host Feather RP2040.
Plug the LiPo battery into the Feather, then insert the Feather into the Quad FeatherWing.
OLED FeatherWing
Solder header pins under the OLED FeatherWing.
Insert it into the Quad FeatherWing as shown.
I used a piece of polyamide tape to add a nice amber tint to the OLED. This is optional. But just barely.
Screws
The MIDI and USB ports will be subjected to some force as you insert and remove cables, so it's a good idea to screw them into place and save the header pins from too much strain.
Use twelve M2.5 x 6mm screws to fasten them into the hex standoffs we prepared earlier.
3D Printed Case
You can optionally print a simple case to house the boards. The .stl file above can be printed in PLA at 0.2mm layer height.
Adhere a short length of double-stick foam tape to the inside of the case to secure the battery.
Then, fit the board in, being mindful of the switch so it notches into the side hole.
No 3D Printing Required
Alternatively, you can use the cutting guide linked below as a template to simply drill holes in a cigar box, take-out food container, or other box of your choosing.
Case Screws
Fasten the board to the case with four M2.5 x 6mm screws from underneath.
Please ignore the uneven first layer of my print and the slightly curled corner. Thank you.
You can optionally adhere rubber bumper feet to prevent sliding.
Page last edited June 19, 2024
Text editor powered by tinymce.
Use the Messenger
MIDI Cable
Plug in a MIDI cable to the MIDI FeatherWing output jack.
The other end of the MIDI cable will plug into your synthesizer, in this case a 1010music nanobox fireball via a DIN-5 to 3.5mm TRS adapter.
The USB host port will provide power to the MIDI keyboard/controller, as well as establish the connection for data.
MIDI Out Channel
Press the A button on the OLED FeatherWing to change the MIDI out channel. In this case it has been set to channel 2. You can increment up to 16 and then it will roll back around to 1.
Program Change
Your MIDI keyboard/controller may have buttons for sending Program Change messages. These are used to change the patch preset. The Messenger can increment the Program Change values in sets of eight to make it quicker to try different sounds.
Press the B button to increment the Program Change number by eight, up to 127, and then back to the beginning.
CC Number
When you adjust a CC knob or slider the Messenger will show which CC number has been changed. This is useful in figuring out which knobs are controlling different parameters.
MIDI Panic
If a MIDI note ever gets stuck you can send a MIDI panic to shut off all notes. Press the C button on the OLED FeatherWing to send MIDI panic.
Play Notes
As you play keys on your MIDI keyboard the Messenger will display the in-coming channel > out-going channel, and the note number. A small o shows up when the note is pressed and disappears when that same note number is released.
Data/Charging/USB MIDI Out
You can plug a USB C cable into the primary USB port of the Feather in order to charge the connected battery, update the firmware, or to forward the filtered MIDI messages along to a connected software synth on a computer (as well as still sending over UART classic MIDI).
Customization
This is just a small sampling of the filtering and modification you can do with the USB MIDI Host Messenger. The code could be update to do more advanced things such as:
- CC# re-mapping and value inversion
- Note scale quantization
- Keyboard splitting to send low notes over a different channel to a bass-line synth
- Automatic note harmonizer
- Arpeggiators
- MIDI clock source
- Velocity randomizer
- CC LFO
- Very tiny sheet music display
Enjoy cooking up new uses for your USB MIDI Host Messenger!
Page last edited June 19, 2024
Text editor powered by tinymce.