I wanted to play my MIDI hardware synths with a cheap small portable MIDI USB keyboard.
There are a few MIDI USB Host converters on the market, but they tend to be pricey and not so flexible; for example, the MIDItech USB Host converter I purchased (at $80+ ) does not filter "Keep Alive" MIDI messages, which cause issues with many older MIDI synth modules. Moreover, when you connect some MIDI USB keyboards, they send spurious junk over MIDI for the first second or so, again causing issues to some hardware modules.
So this is it: a converter powered by the phenomenal Teensy 3.6. I did not need processing power and wanted to save battery, so I downlocked it to 24 MHz, more than enough.
Connected to a smartphone USB powerbank, it runs for many hours while powering the USB keyboard.
Thanks to my dear friend Alessandro for the soldering work and the 3D printed case.
For the hardware part, I just followed the instructions on PJRC website:
https://www.pjrc.com/teensy/td_libs_MIDI.html
Added a couple LEDs to signal errors and MIDI connection
Software is not really complete, but works OK for my limited needs:
I'm infinitely grateful to PJRC and all of PJRC Forum members.
Thanks guys!
There are a few MIDI USB Host converters on the market, but they tend to be pricey and not so flexible; for example, the MIDItech USB Host converter I purchased (at $80+ ) does not filter "Keep Alive" MIDI messages, which cause issues with many older MIDI synth modules. Moreover, when you connect some MIDI USB keyboards, they send spurious junk over MIDI for the first second or so, again causing issues to some hardware modules.
So this is it: a converter powered by the phenomenal Teensy 3.6. I did not need processing power and wanted to save battery, so I downlocked it to 24 MHz, more than enough.
Connected to a smartphone USB powerbank, it runs for many hours while powering the USB keyboard.
Thanks to my dear friend Alessandro for the soldering work and the 3D printed case.
For the hardware part, I just followed the instructions on PJRC website:
https://www.pjrc.com/teensy/td_libs_MIDI.html
Added a couple LEDs to signal errors and MIDI connection
Software is not really complete, but works OK for my limited needs:
Code:
// v0.7.1
// first version 20180121, last modified 20200209-1
// last comments 20200425-1
// TODO:
// Check if latency improves with higher CPU clocks (now 24 MHz to save power)
// Measure how much current is drawn by 5V microUSB when various keyboards (Akai LPK25, Miditech37, ProDipe49, Korg MiniKey etc.) are connected:
// CPU @ 24 MHz
// Miditech37: total current (converted + keyboard) = 65mA (340 mW)
// Jammin Pro PK25: total current = 65mA (340 mW)
// Handle "SysEx chunks" (not we only have a debug placeholder)
// Add bridge from microUSB_input 5V and GND pins to USB Host out, to get more power (but check Teensy 3.6 schematics first! Where does USB Host get power from? Do we risk frying anything?)
// Add PANIC button (all notes off): send Control Change message 123 (not SysEx!) on all channels
// Don't need to set "MIDI" under USB Type; "Serial" works OK and allows for VisualMicro SerialMonitor to work (MIDI + Serial works as well, but MIDI is not needed since we only use USB Host MIDI, not microUSB MIDI)
// Reference: https://www.pjrc.com/teensy/td_midi.html
// Wiring: v. https://www.pjrc.com/teensy/td_libs_MIDI.html
// Simplified version (MIDI out only):
// MIDI female pin 4 to +3.3V via 47 Ohm (replace with 33 Ohm should problems arise)
// MIDI female pin 5 to Teensy pin 10 (Serial2 TX) via 47 Ohm (replace with 33 Ohm should problems arise)
// MIDI female pin 2 to GND
// v. https://forum.pjrc.com/threads/49282-Teensy-3-6-as-USBMIDI-to-SerialMIDI-adapter
// NOTE: USB Host cable must be plugged with +5V at top position. No problem if the fifth pin is not connected (redundant GND)
// PCB: Blue pin = Serial2TX via 47 Ohm (to MIDI pin 5)
// Yellow pin = +3.3V via 47 Ohm (to MIDI pin 4)
// White pin = pin 6 via 4.7 KOhm (to white "running" LED). White LED draws 160uA at 3.4V = 545mW ; stays lit all the time a keyboard is connected
// Orange pin = pin 5 via 1 KOhm (to yellow "no input device" LED). Orange LED draws 1435 uA at 3.4V = 4.88mW; stays lit when a keyboard is not connected (so typically just a few seconds)
// Red pins = +3.3V (unused)
// Black pins = GND (to MIDI pin 2 and LED negative pins)
#include <USBHost_t36.h> // Access to USB Host MIDI
#include <MIDI.h> // access to serial (5 pin DIN) MIDI
#define MIDIADAPTER_VERSION "0.7.1"
#define HW_SERIAL_PORT Serial2
#define SERIALDEBUG_TIMEOUT_MS 5000 // After that, it starts even if no serial debug port active
#define SETUP_DELAY_MS 1500
#define KEYBOARD_CHECK_INTERVAL_MS 200
#define DISCARD_INITIAL_JUNK_INTERVAL_MS 1500
#define SERIAL_DEBUG_SPEED 115200
#define ERROR_LED_PIN LED_BUILTIN
#define NO_INPUT_DEVICE_LED_PIN 5
#define RUNNING_LED_PIN 6
#define BLOCK_ACTIVE_SENSING // Filter out annoying useless "I am alive" messages which mess up playing with many MIDI synths
#define ENABLE_RUNNING_LED // Disable to save 0.5 milliwatts
//#define SERIALDEBUG
MIDI_CREATE_INSTANCE(HardwareSerial, HW_SERIAL_PORT, MIDI);
/////////////
// GLOBALS
/////////////
USBHost gUSBHostPort; // g prefix for "global"
USBHub gUSBHostHub(gUSBHostPort);
MIDIDevice gInputMIDIUSBDevice(gUSBHostPort);
uint32_t gMIDIjunkStartTime;
uint32_t gSerialStartTime;
//////////
// MISC
//////////
static void printBytes(const byte *data, unsigned int size)
{
while (size > 0)
{
byte b = *data++;
if (b < 16) Serial.print('0');
Serial.print(b, HEX);
if (size > 1) Serial.print(' ');
size = size - 1;
}
}
//////////
// LEDS
//////////
static void initializeLEDs()
{
pinMode(ERROR_LED_PIN, OUTPUT);
pinMode(NO_INPUT_DEVICE_LED_PIN, OUTPUT);
#ifdef ENABLE_RUNNING_LED
pinMode(RUNNING_LED_PIN, OUTPUT);
#endif
}
// Error LEDs turn on when a MIDI event is not handled (an appropriate event handler has yet to be written)
static void errorLED_On()
{
digitalWriteFast(ERROR_LED_PIN, HIGH);
}
static void errorLED_Off()
{
digitalWriteFast(ERROR_LED_PIN, LOW);
}
static void noDeviceLED_On()
{
digitalWriteFast(NO_INPUT_DEVICE_LED_PIN, HIGH);
}
static void noDeviceLED_Off()
{
digitalWriteFast(NO_INPUT_DEVICE_LED_PIN, LOW);
}
static void runningLED_On()
{
digitalWriteFast(RUNNING_LED_PIN, HIGH);
}
static void runningLED_Off()
{
digitalWriteFast(RUNNING_LED_PIN, LOW);
}
/////////////////////////////////////////////
// WAIT FOR INPUT DEVICE (on USB Host port)
/////////////////////////////////////////////
static void waitForUSBMIDIdevice()
{
#ifdef ENABLE_RUNNING_LED
runningLED_Off();
#endif
noDeviceLED_On(); // To save battery power, external yellow LED starts ON and turns off when a USB MIDI keyboard is detected
while (!gInputMIDIUSBDevice)
{
//Snooze.sleep(gSnoozeConfig); // Idea was saving battery while waiting for USB device, but won't work (disconnects USB port)
delay(KEYBOARD_CHECK_INTERVAL_MS);
}
noDeviceLED_Off(); // Input connected: turn off LED
#ifdef ENABLE_RUNNING_LED
runningLED_On();
#endif
startJunkTimer();
}
///////////////////////////
// MIDI MESSAGE HANDLERS
///////////////////////////
static void myNoteOn(byte channel, byte note, byte velocity)
{
// When a USB device with multiple virtual cables is used,
// gInputMIDIUSBDevice.getCable() can be used to read which of the virtual
// MIDI cables received this message.
errorLED_Off(); // This event handler is implemented: no need to signal an error
#ifdef SERIALDEBUG
Serial.print("Note On, ch=");
Serial.print(channel, DEC);
Serial.print(", note=");
Serial.print(note, DEC);
Serial.print(", velocity=");
Serial.println(velocity, DEC);
#endif
if (elapsedJunkTimer()) // Shortly after connected/reconnected, most MIDI input devices send junk commands: ignore them
{
MIDI.sendNoteOn(note, velocity, channel);
}
else
{
#ifdef SERIALDEBUG
Serial.println("Blocked by junk timer");
#endif
}
}
static void myNoteOff(byte channel, byte note, byte velocity)
{
errorLED_Off(); // This event handler is implemented
#ifdef SERIALDEBUG
Serial.print("Note Off, ch=");
Serial.print(channel, DEC);
Serial.print(", note=");
Serial.print(note, DEC);
Serial.print(", velocity=");
Serial.println(velocity, DEC);
#endif
if (elapsedJunkTimer()) // Shortly after connected/reconnected, most MIDI input devices send junk commands: ignore them
{
MIDI.sendNoteOff(note, velocity, channel);
}
else
{
#ifdef SERIALDEBUG
Serial.println("Blocked by junk timer");
#endif
}
}
static void myAfterTouchPoly(byte channel, byte note, byte velocity)
{
errorLED_Off(); // This event handler is implemented
#ifdef SERIALDEBUG
Serial.print("AfterTouch Change, ch=");
Serial.print(channel, DEC);
Serial.print(", note=");
Serial.print(note, DEC);
Serial.print(", velocity=");
Serial.println(velocity, DEC);
#endif
if (elapsedJunkTimer()) // Shortly after connected/reconnected, most MIDI input devices send junk commands: ignore them
{
MIDI.sendAfterTouch(note, velocity, channel);
}
else
{
#ifdef SERIALDEBUG
Serial.println("Blocked by junk timer");
#endif
}
}
static void myControlChange(byte channel, byte control, byte value)
{
errorLED_Off(); // This event handler is implemented
#ifdef SERIALDEBUG
Serial.print("Control Change, ch=");
Serial.print(channel, DEC);
Serial.print(", control=");
Serial.print(control, DEC);
Serial.print(", value=");
Serial.println(value, DEC);
#endif
if (elapsedJunkTimer()) // Shortly after connected/reconnected, most MIDI input devices send junk commands: ignore them
{
MIDI.sendControlChange(control, value, channel);
}
else
{
#ifdef SERIALDEBUG
Serial.println("Blocked by junk timer");
#endif
}
}
static void myProgramChange(byte channel, byte program)
{
errorLED_Off(); // This event handler is implemented
#ifdef SERIALDEBUG
Serial.print("Program Change, ch=");
Serial.print(channel, DEC);
Serial.print(", program=");
Serial.println(program, DEC);
#endif
if (elapsedJunkTimer()) // Shortly after connected/reconnected, most MIDI input devices send junk commands: ignore them
{
MIDI.sendProgramChange(program, channel);
}
else
{
#ifdef SERIALDEBUG
Serial.println("Blocked by junk timer");
#endif
}
}
static void myAfterTouchChannel(byte channel, byte pressure)
{
errorLED_Off(); // This event handler is implemented
#ifdef SERIALDEBUG
Serial.print("After Touch, ch=");
Serial.print(channel, DEC);
Serial.print(", pressure=");
Serial.println(pressure, DEC);
#endif
if (elapsedJunkTimer()) // Shortly after connected/reconnected, most MIDI input devices send junk commands: ignore them
{
MIDI.sendAfterTouch(pressure, channel);
}
else
{
#ifdef SERIALDEBUG
Serial.println("Blocked by junk timer");
#endif
}
}
static void myPitchChange(byte channel, int pitch)
{
errorLED_Off(); // This event handler is implemented
#ifdef SERIALDEBUG
Serial.print("Pitch Change, ch=");
Serial.print(channel, DEC);
Serial.print(", pitch=");
Serial.println(pitch, DEC);
#endif
if (elapsedJunkTimer()) // Shortly after connected/reconnected, most MIDI input devices send junk commands: ignore them
{
MIDI.sendPitchBend(pitch, channel);
}
else
{
#ifdef SERIALDEBUG
Serial.println("Blocked by junk timer");
#endif
}
}
// This 3-input System Exclusive function is more complex, but allows you to
// process very large messages which do not fully fit within the gInputMIDIUSBDevice's
// internal buffer. Large messages are given to you in chunks, with the
// 3rd parameter to tell you which is the last chunk. This function is
// a Teensy extension, not available in the Arduino MIDI library.
//
static void mySystemExclusiveChunk(const byte *data, uint16_t length, bool last)
{
if (elapsedJunkTimer()) // Shortly after connected/reconnected, most MIDI input devices send junk commands: ignore them
{
errorLED_On(); // TODO This event handler is not implemented yet!
}
#ifdef SERIALDEBUG
Serial.print("SysEx Chunk (not implemented!): ");
printBytes(data, length);
if (last)
{
Serial.println(" (end)");
}
else
{
Serial.println(" (to be continued)");
}
#endif
// TODO how to proceed? Maybe buffering to a global array and increment length (global)
// until we are called with last = true, then we send the global buffer via MIDI.sendSysEx()?
}
// This simpler 2-input System Exclusive function can only receive messages
// up to the size of the internal buffer. Larger messages are truncated, with
// no way to receive the data which did not fit in the buffer. If both types
// of SysEx functions are set, the 3-input version will be called by gInputMIDIUSBDevice.
//
static void mySystemExclusive(byte *data, unsigned int length)
{
errorLED_Off(); // This event handler is implemented
#ifdef SERIALDEBUG
Serial.print("SysEx Message: ");
printBytes(data, length);
Serial.println();
#endif
if (elapsedJunkTimer()) // Shortly after connected/reconnected, most MIDI input devices send junk commands: ignore them
{
MIDI.sendSysEx(length, data);
}
else
{
#ifdef SERIALDEBUG
Serial.println("Blocked by junk timer");
#endif
}
}
static void myTimeCodeQuarterFrame(byte data)
{
errorLED_Off(); // This event handler is implemented
static char SMPTE[8] = { '0','0','0','0','0','0','0','0' };
static byte fps = 0;
byte index = data >> 4;
byte number = data & 15;
if (index == 7) {
fps = (number >> 1) & 3;
number = number & 1;
}
if (index < 8 || number < 10)
{
SMPTE[index] = number + '0';
#ifdef SERIALDEBUG
Serial.print("TimeCode: "); // perhaps only print when index == 7
Serial.print(SMPTE[7]);
Serial.print(SMPTE[6]);
Serial.print(':');
Serial.print(SMPTE[5]);
Serial.print(SMPTE[4]);
Serial.print(':');
Serial.print(SMPTE[3]);
Serial.print(SMPTE[2]);
Serial.print('.');
Serial.print(SMPTE[1]); // perhaps add 2 to compensate for MIDI latency?
Serial.print(SMPTE[0]);
switch (fps)
{
case 0: Serial.println(" 24 fps"); break;
case 1: Serial.println(" 25 fps"); break;
case 2: Serial.println(" 29.97 fps"); break;
case 3: Serial.println(" 30 fps"); break;
default: Serial.println(" Unsupported FPS!"); break; // TODO should turn on ErrorLED
}
}
else
{
Serial.print("TimeCode: invalid data = "); // TODO should turn on ErrorLED
Serial.println(data, HEX);
}
#else
}
#endif
if (elapsedJunkTimer()) // Shortly after connected/reconnected, most MIDI input devices send junk commands: ignore them
{
MIDI.sendTimeCodeQuarterFrame(data); // TODO not sure about this!
}
else
{
#ifdef SERIALDEBUG
Serial.println("Blocked by junk timer");
#endif
}
}
static void mySongPosition(uint16_t beats)
{
errorLED_Off(); // This event handler is implemented
#ifdef SERIALDEBUG
Serial.print("Song Position, beat=");
Serial.println(beats);
#endif
if (elapsedJunkTimer()) // Shortly after connected/reconnected, most MIDI input devices send junk commands: ignore them
{
MIDI.sendSongPosition(beats);
}
else
{
#ifdef SERIALDEBUG
Serial.println("Blocked by junk timer");
#endif
}
}
static void mySongSelect(byte songNumber)
{
errorLED_Off(); // This event handler is implemented
#ifdef SERIALDEBUG
Serial.print("Song Select, song=");
Serial.println(songNumber, DEC);
#endif
if (elapsedJunkTimer()) // Shortly after connected/reconnected, most MIDI input devices send junk commands: ignore them
{
MIDI.sendSongSelect(songNumber);
}
else
{
#ifdef SERIALDEBUG
Serial.println("Blocked by junk timer");
#endif
}
}
static void myTuneRequest()
{
errorLED_Off(); // This event handler is implemented
#ifdef SERIALDEBUG
Serial.println("Tune Request");
#endif
if (elapsedJunkTimer()) // Shortly after connected/reconnected, most MIDI input devices send junk commands: ignore them
{
MIDI.sendTuneRequest();
}
else
{
#ifdef SERIALDEBUG
Serial.println("Blocked by junk timer");
#endif
}
}
static void myClock()
{
errorLED_Off(); // This event handler is implemented
#ifdef SERIALDEBUG
Serial.println("Clock");
#endif
if (elapsedJunkTimer()) // Shortly after connected/reconnected, most MIDI input devices send junk commands: ignore them
{
MIDI.sendRealTime(midi::Clock); // TODO ...not sure...?
}
else
{
#ifdef SERIALDEBUG
Serial.println("Blocked by junk timer");
#endif
}
}
static void myStart()
{
errorLED_Off(); // This event handler is implemented
#ifdef SERIALDEBUG
Serial.println("Start");
#endif
if (elapsedJunkTimer()) // Shortly after connected/reconnected, most MIDI input devices send junk commands: ignore them
{
MIDI.sendRealTime(midi::Start); // TODO ...not sure...?
}
else
{
#ifdef SERIALDEBUG
Serial.println("Blocked by junk timer");
#endif
}
}
static void myContinue()
{
errorLED_Off(); // This event handler is implemented
#ifdef SERIALDEBUG
Serial.println("Continue");
#endif
if (elapsedJunkTimer()) // Shortly after connected/reconnected, most MIDI input devices send junk commands: ignore them
{
MIDI.sendRealTime(midi::Continue); // TODO ...not sure...?
}
else
{
#ifdef SERIALDEBUG
Serial.println("Blocked by junk timer");
#endif
}
}
static void myStop()
{
errorLED_Off(); // This event handler is implemented
#ifdef SERIALDEBUG
Serial.println("Stop");
#endif
if (elapsedJunkTimer()) // Shortly after connected/reconnected, most MIDI input devices send junk commands: ignore them
{
MIDI.sendRealTime(midi::Stop); // TODO ...not sure...?
}
else
{
#ifdef SERIALDEBUG
Serial.println("Blocked by junk timer");
#endif
}
}
static void myActiveSensing()
{
errorLED_Off(); // This event handler is implemented
#ifdef SERIALDEBUG
Serial.println("Active Sensing");
#endif
#ifndef BLOCK_ACTIVE_SENSING
if (elapsedJunkTimer()) // Shortly after connected/reconnected, most MIDI input devices send junk commands: ignore them
{
MIDI.sendRealTime(midi::ActiveSensing); // TODO ...not sure...?
}
else
{
#ifdef SERIALDEBUG
Serial.println("Blocked by junk timer");
#endif
}
#else
#ifdef SERIALDEBUG
Serial.println("Blocked by filter");
#endif
#endif
}
static void mySystemReset()
{
errorLED_Off(); // This event handler is implemented
#ifdef SERIALDEBUG
Serial.println("System Reset");
#endif
if (elapsedJunkTimer()) // Shortly after connected/reconnected, most MIDI input devices send junk commands: ignore them
{
MIDI.sendRealTime(midi::SystemReset); // TODO ...not sure...?
}
else
{
#ifdef SERIALDEBUG
Serial.println("Blocked by junk timer");
#endif
}
}
static void myRealTimeSystem(uint8_t realtimebyte)
{
errorLED_Off(); // This event handler is implemented
#ifdef SERIALDEBUG
Serial.print("Real Time Message, code=");
Serial.println(realtimebyte, HEX);
#endif
if (elapsedJunkTimer()) // Shortly after connected/reconnected, most MIDI input devices send junk commands: ignore them
{
MIDI.sendRealTime((midi::MidiType) realtimebyte);
}
else
{
#ifdef SERIALDEBUG
Serial.println("Blocked by junk timer");
#endif
}
}
static void startJunkTimer()
{
gMIDIjunkStartTime = millis();
}
static bool elapsedJunkTimer()
{
if (millis() - gMIDIjunkStartTime > DISCARD_INITIAL_JUNK_INTERVAL_MS)
return true;
return false;
}
///////////////
//
// SETUP
//
///////////////
void setup()
{
initializeLEDs();
// configure wakeup timer
//gWakeupTimer.setTimer(KEYBOARD_CHECK_INTERVAL_MS);
#ifdef SERIALDEBUG
Serial.begin(SERIAL_DEBUG_SPEED);
#endif
// Wait 1.5 seconds before turning on USB Host. If connected USB devices
// use too much power, Teensy at least completes USB enumeration, which
// makes isolating the power issue easier.
delay(SETUP_DELAY_MS);
#ifdef SERIALDEBUG
gSerialStartTime = millis();
while (!Serial && (millis() - gSerialStartTime < SERIALDEBUG_TIMEOUT_MS))
{
delay(10);
}
Serial.print("USB MIDI Adapter v.");
Serial.println(MIDIADAPTER_VERSION);
delay(100);
#endif
MIDI.begin(); // Channel indication not needed in this case
gUSBHostPort.begin();
#ifdef SERIALDEBUG
Serial.println("Waiting for USB MIDI input device...");
#endif
waitForUSBMIDIdevice();
// At this point, a device has been connected
#ifdef SERIALDEBUG
Serial.println("USB MIDI input device connected");
#endif
gInputMIDIUSBDevice.setHandleNoteOn(myNoteOn);
gInputMIDIUSBDevice.setHandleNoteOff(myNoteOff);
gInputMIDIUSBDevice.setHandleAfterTouchPoly(myAfterTouchPoly);
gInputMIDIUSBDevice.setHandleControlChange(myControlChange);
gInputMIDIUSBDevice.setHandleProgramChange(myProgramChange);
gInputMIDIUSBDevice.setHandleAfterTouchChannel(myAfterTouchChannel);
gInputMIDIUSBDevice.setHandlePitchChange(myPitchChange);
// Only one of these System Exclusive handlers will actually be
// used. See the comments below for the difference between them.
gInputMIDIUSBDevice.setHandleSystemExclusive(mySystemExclusiveChunk);
gInputMIDIUSBDevice.setHandleSystemExclusive(mySystemExclusive);
gInputMIDIUSBDevice.setHandleTimeCodeQuarterFrame(myTimeCodeQuarterFrame);
gInputMIDIUSBDevice.setHandleSongPosition(mySongPosition);
gInputMIDIUSBDevice.setHandleSongSelect(mySongSelect);
gInputMIDIUSBDevice.setHandleTuneRequest(myTuneRequest);
gInputMIDIUSBDevice.setHandleClock(myClock);
gInputMIDIUSBDevice.setHandleStart(myStart);
gInputMIDIUSBDevice.setHandleContinue(myContinue);
gInputMIDIUSBDevice.setHandleStop(myStop);
gInputMIDIUSBDevice.setHandleActiveSensing(myActiveSensing);
gInputMIDIUSBDevice.setHandleSystemReset(mySystemReset);
// This generic System Real Time handler is only used if the
// more specific ones are not set.
gInputMIDIUSBDevice.setHandleRealTimeSystem(myRealTimeSystem);
}
//////////////
//
// LOOP
//
//////////////
void loop()
{
// The handler functions are called when gInputMIDIUSBDevice reads data. They
// will not be called automatically. You must call gInputMIDIUSBDevice.read()
// regularly from loop() for gInputMIDIUSBDevice to actually read incoming
// data and run the handler functions as messages arrive.
gUSBHostPort.Task();
if (gInputMIDIUSBDevice) // Input device could have been disconnected after a while (after setup()), so always check
{
gInputMIDIUSBDevice.read();
}
else
{
waitForUSBMIDIdevice();
}
}
I'm infinitely grateful to PJRC and all of PJRC Forum members.
Thanks guys!