Skip to content

Instantly share code, notes, and snippets.

@cowboy
Last active March 10, 2026 03:15
Show Gist options
  • Select an option

  • Save cowboy/e37d95508b22bd79d1649c09db71d721 to your computer and use it in GitHub Desktop.

Select an option

Save cowboy/e37d95508b22bd79d1649c09db71d721 to your computer and use it in GitHub Desktop.
Launchkey Mini MK3 Supercharger (USB MIDI) - for Teensy 4.1
// To give your project a unique name, this code must be
// placed into a .c file (its own tab). It can not be in
// a .cpp file or your main sketch (the .ino file).
#include "usb_names.h"
// Edit these lines to create your own name. The length must
// match the number of characters in your custom name.
#define MIDI_NAME {'C','B',' ','S','u','p','e','r',' ','L','a','u','n','c','h','k','e','y'}
#define MIDI_NAME_LEN 18
// Do not change this part. This exact format is required by USB.
struct usb_string_descriptor_struct usb_string_product_name = {
2 + MIDI_NAME_LEN * 2,
3,
MIDI_NAME
};
// ===============================================================
// Launchkey Mini MK3 Supercharger (USB MIDI) - for Teensy 4.1
// "Cowboy" Ben Alman, 2022
// https://gist.github.com/cowboy/e37d95508b22bd79d1649c09db71d721
// ===============================================================
// Based on Arduino Teensy examples:
// File > Examples > Teensy > USB_MIDI
// File > Examples > USBHost_t36 > MIDI > Interface_16x16
//
// Settings:
// Tools > Board = "Teensy 4.1"
// Tools > USB Type to "MIDI"
#include <MIDI.h> // access to serial (5 pin DIN) MIDI
#include <USBHost_t36.h> // access to USB MIDI devices (plugged into 2nd USB port)
// Create the ports for USB devices plugged into Teensy's 2nd USB port (via hubs)
const int midiHostPorts = 4;
USBHost myusb;
USBHub hub1(myusb);
USBHub hub2(myusb);
USBHub hub3(myusb);
USBHub hub4(myusb);
MIDIDevice midi01(myusb);
MIDIDevice midi02(myusb);
MIDIDevice midi03(myusb);
MIDIDevice midi04(myusb);
MIDIDevice * midilist[4] = {
&midi01, &midi02, &midi03, &midi04
};
// LEDs
const int LED_OTHER = 17;
const int LED_HOST_ACTIVITY = 20;
const int LED_DEVICE_ACTIVITY = 15;
// Sustain
const int SUSTAIN_CC_NUMBER = 64;
bool isSustaining[16] = {};
bool isNoteOff[16][127] = {};
void initSustainArrays() {
for (int channel = 0; channel < 16; channel++) {
isSustaining[channel] = false;
for (int note = 0; note < 127; note++) {
isNoteOff[channel][note] = false;
}
}
}
// This code makes super heavy use of the knowledge shared in
// https://github.com/giezu/launchkeyMK3
// https://docs.google.com/spreadsheets/u/2/d/e/2PACX-1vQgwSu7S3ifJUJc8kXHBo6Be1NiIXhUXTK6S_oT_4rPPBQmic8yTu5OKbmn-la32DogcFcIzZE-TvMF/pubhtml#
const int LAUNCHKEY_BEATS = 8;
const int LAUNCHKEY_PAD_BEATS[LAUNCHKEY_BEATS] = {96,97,98,99,100,101,102,103};
const int LAUNCHKEY_FORCE_MIDI_CHANNELS = 6;
const int LAUNCHKEY_PAD_CHANNEL[LAUNCHKEY_FORCE_MIDI_CHANNELS] = {112,113,114,115,116,117};
const int LAUNCHKEY_PAD_SUSTAIN = 118;
const int LAUNCHKEY_PAD_FIXED_VEL = 119;
// https://docs.google.com/spreadsheets/u/2/d/e/2PACX-1vQgwSu7S3ifJUJc8kXHBo6Be1NiIXhUXTK6S_oT_4rPPBQmic8yTu5OKbmn-la32DogcFcIzZE-TvMF/pubhtml#
const int LAUNCHKEY_COLOR_BEAT_BLANK = 102;
const int LAUNCHKEY_COLOR_BEAT_QUARTER = 5;
const int LAUNCHKEY_COLOR_BEAT_SIXTEENTH = 29;
const int LAUNCHKEY_COLOR_BEAT_PLAYING = 3;
const int LAUNCHKEY_COLOR_BEAT_SELECTING = 57;
const int LAUNCHKEY_COLOR_TOGGLE[2] = {LAUNCHKEY_COLOR_BEAT_SELECTING, 66};
const int LAUNCHKEY_COLOR_CHANNEL[2] = {LAUNCHKEY_COLOR_BEAT_SELECTING, 66};
const int LAUNCHKEY_COLOR_SUSTAIN[2] = {9, 124};
// "Launchkey Mini MK3 MIDI"
// "MIDIIN2 (Launchkey Mini MK3 MID"
// "MIDIOUT2 (Launchkey Mini MK3 MI"
const byte launchkeyMidiDevice = 0;
const byte launchkeyControlCable = 1;
void debugMidiDevice() {
String manufacturerName = (char*)midilist[launchkeyMidiDevice]->manufacturer();
Serial.println(manufacturerName);
String productName = (char*)midilist[launchkeyMidiDevice]->product();
Serial.println(productName);
String serialNumberVal = (char*)midilist[launchkeyMidiDevice]->serialNumber();
Serial.println(serialNumberVal);
}
void sendLaunchkeyNote(byte channel, byte note, byte velocity) {
midilist[launchkeyMidiDevice]->sendNoteOn(note, velocity, channel, launchkeyControlCable);
}
void sendLaunchkeyCC(byte channel, byte ccNum, byte value) {
midilist[launchkeyMidiDevice]->sendControlChange(ccNum, value, channel, launchkeyControlCable);
}
void enableLaunchkeyDawMode() {
sendLaunchkeyNote(16, 12, 127);
}
void enableLaunchkeyKnobCustomMode() {
sendLaunchkeyCC(16, 9, 6);
}
void showPad(int pad, int color) {
enableLaunchkeyDawMode();
sendLaunchkeyNote(1, pad, color);
}
void showStateButton(int pad, bool state, const int * color_arr = LAUNCHKEY_COLOR_TOGGLE);
void showStateButton(int pad, bool state, const int * color_arr) {
showPad(pad, state ? color_arr[0] : color_arr[1]);
}
int midiChannel = 0;
void updateChannelDisplay() {
for (int i = 0; i < LAUNCHKEY_FORCE_MIDI_CHANNELS; i++) {
showStateButton(LAUNCHKEY_PAD_CHANNEL[i], i == midiChannel, LAUNCHKEY_COLOR_CHANNEL);
}
updateVelocityDisplay();
}
int fixedVelocityDefault = 3;
int fixedVelocityEnabled[LAUNCHKEY_FORCE_MIDI_CHANNELS] = {true,true,true,true,true,true};
int fixedVelocityLevel[LAUNCHKEY_FORCE_MIDI_CHANNELS] = {fixedVelocityDefault,fixedVelocityDefault,fixedVelocityDefault,fixedVelocityDefault,fixedVelocityDefault,fixedVelocityDefault};
int getVelocity(int idx) {
return (fixedVelocityLevel[idx] + 1) * 16 - 1;
}
void updateVelocityDisplay() {
showStateButton(LAUNCHKEY_PAD_FIXED_VEL, fixedVelocityEnabled[midiChannel]);
}
void showSustain(bool state) {
showStateButton(LAUNCHKEY_PAD_SUSTAIN, state, LAUNCHKEY_COLOR_SUSTAIN);
}
String launchkeyProductName = "Launchkey Mini MK3";
elapsedMillis initLaunchkeyMillis = 0;
bool launchKeyNeedsInit = true;
void initLaunchkey() {
if (initLaunchkeyMillis < 1000) {
return;
}
String productName = (char*)midilist[launchkeyMidiDevice]->product();
if (launchkeyProductName.equals(productName)) {
if (launchKeyNeedsInit) {
updateBeatDisplay();
updateChannelDisplay();
updateVelocityDisplay();
showSustain(false);
enableLaunchkeyKnobCustomMode();
launchKeyNeedsInit = false;
}
} else {
launchKeyNeedsInit = true;
}
initLaunchkeyMillis = 0;
}
// Assume 4/4 time
int BEATS_PER_MEASURE = 4;
int PPQN = 24;
int PPSN = PPQN / 4;
int MAX_PULSES = PPQN * LAUNCHKEY_BEATS;
int currentBeat = 0;
int currentSixteenth = 0;
bool clockRunning = false;
int clockCounter = 0;
void updateClock() {
if (clockCounter % PPQN == 0) {
digitalWriteFast(LED_OTHER, HIGH);
} else if (clockCounter % (PPQN / 2) == 0) {
digitalWriteFast(LED_OTHER, LOW);
}
currentBeat = clockCounter / PPQN;
currentSixteenth = (clockCounter / PPSN) % 4;
if (clockCounter % PPSN == 0) {
updateBeatDisplay();
}
if (++clockCounter >= MAX_PULSES) {
clockCounter = 0;
}
}
void startClock() {
clockCounter = 0;
clockRunning = true;
}
void stopClock() {
clockCounter = 0;
clockRunning = false;
updateBeatDisplay();
digitalWriteFast(LED_OTHER, LOW);
}
int heldBeatPad[LAUNCHKEY_BEATS] = {};
int beatDisplay[LAUNCHKEY_BEATS] = {};
void updateBeatDisplay() {
for (int i = 0; i < LAUNCHKEY_BEATS; i++) {
beatDisplay[i] = LAUNCHKEY_COLOR_BEAT_BLANK;
}
if (clockRunning) {
beatDisplay[currentSixteenth + (currentBeat < 4 ? 4 : 0)] = LAUNCHKEY_COLOR_BEAT_SIXTEENTH;
beatDisplay[currentBeat] = LAUNCHKEY_COLOR_BEAT_QUARTER;
}
for (int i = 0; i < LAUNCHKEY_BEATS; i++) {
if (!heldBeatPad[i]) {
showPad(LAUNCHKEY_PAD_BEATS[i], beatDisplay[i]);
}
}
}
void blink(int count) {
int delay_ms = 100;
for (int i = 0; i < count; i++) {
digitalWrite(LED_OTHER, HIGH);
delay(delay_ms);
digitalWrite(LED_OTHER, LOW);
delay(delay_ms);
}
}
void setup() {
Serial.begin(115200);
// Setup pins
pinMode(LED_OTHER, OUTPUT);
pinMode(LED_HOST_ACTIVITY, OUTPUT);
pinMode(LED_DEVICE_ACTIVITY, OUTPUT);
blink(3);
initSustainArrays();
// 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(1500);
myusb.begin();
}
// A variable to know how long the LED has been turned on
elapsedMillis hostActivityLedTime;
elapsedMillis deviceActivityLedTime;
const int MIDI_MESSAGE_NONE = 0;
const int MIDI_MESSAGE_DEFAULT = 1;
const int MIDI_MESSAGE_CLOCK = 2;
const uint8_t ledTimeMap[3] = {
0, // MIDI_MESSAGE_NONE
15, // MIDI_MESSAGE_DEFAULT
1 // MIDI_MESSAGE_CLOCK
};
int hostCurrentMessageType = MIDI_MESSAGE_NONE;
int deviceCurrentMessageType = MIDI_MESSAGE_NONE;
void loop() {
initLaunchkey();
bool deviceActivity = false;
bool hostActivity = false;
// Read messages arriving from the (up to) 4 USB devices plugged into the USB Host port
for (int port = 0; port < midiHostPorts; port++) {
if (midilist[port]->read()) {
uint8_t type = midilist[port]->getType();
uint8_t data1 = midilist[port]->getData1();
uint8_t data2 = midilist[port]->getData2();
uint8_t channel = midilist[port]->getChannel();
const uint8_t *sys = midilist[port]->getSysExArray();
int cable = midilist[port]->getCable();
deviceActivity = sendToUpstreamHost(type, data1, data2, channel, sys, cable);
}
}
// Read messages the PC (upstream host) sends and forward them to devices
if (usbMIDI.read()) {
byte type = usbMIDI.getType();
byte data1 = usbMIDI.getData1();
byte data2 = usbMIDI.getData2();
byte channel = usbMIDI.getChannel();
const uint8_t *sys = usbMIDI.getSysExArray();
byte cable = usbMIDI.getCable();
hostActivity = sendToDownstreamDevice(type, data1, data2, channel, sys, cable);
}
// Light show
if (hostActivity) {
digitalWriteFast(LED_HOST_ACTIVITY, HIGH);
hostActivityLedTime = 0;
}
if (hostCurrentMessageType != MIDI_MESSAGE_NONE && hostActivityLedTime > ledTimeMap[hostCurrentMessageType]) {
hostCurrentMessageType = MIDI_MESSAGE_NONE;
digitalWriteFast(LED_HOST_ACTIVITY, LOW);
}
if (deviceActivity) {
digitalWriteFast(LED_DEVICE_ACTIVITY, HIGH);
deviceActivityLedTime = 0;
}
if (deviceCurrentMessageType != MIDI_MESSAGE_NONE && deviceActivityLedTime > ledTimeMap[deviceCurrentMessageType]) {
deviceCurrentMessageType = MIDI_MESSAGE_NONE;
digitalWriteFast(LED_DEVICE_ACTIVITY, LOW);
}
}
bool velocityButtonPressed = false;
bool velocityValueChanged = false;
bool velocityEnabling = false;
// Send data from the downstream MIDI device (eg. controller) to the upstream host (eg. computer)
bool sendToUpstreamHost(byte type, byte data1, byte data2, byte channel, const uint8_t *sysexarray, byte cable) {
// int prevMessageType = deviceCurrentMessageType;
deviceCurrentMessageType = MIDI_MESSAGE_DEFAULT;
if (type != midi::SystemExclusive) {
// Serial.print("cable ");
// Serial.println(cable);
// Serial.print("channel ");
// Serial.println(channel);
// Serial.print("data1 ");
// Serial.println(data1);
int midiChannelOffset = 0;
int midiChannelOverride = 0;
// ==========================================
// Launchkey Mini MK3 "Session Mode" controls
// ==========================================
if (cable == launchkeyControlCable) {
bool preventMessage = true;
cable = 0;
// Toggle fixed velocity
if (data1 == LAUNCHKEY_PAD_FIXED_VEL) {
// press
if (type == usbMIDI.NoteOn) {
if (!fixedVelocityEnabled[midiChannel]) {
velocityEnabling = true;
fixedVelocityEnabled[midiChannel] = true;
}
velocityButtonPressed = true;
int idx = fixedVelocityLevel[midiChannel];
heldBeatPad[idx] = true;
showPad(LAUNCHKEY_PAD_BEATS[idx], LAUNCHKEY_COLOR_BEAT_SELECTING);
updateVelocityDisplay();
}
// release
else {
int idx = fixedVelocityLevel[midiChannel];
heldBeatPad[idx] = false;
showPad(LAUNCHKEY_PAD_BEATS[idx], beatDisplay[idx]);
fixedVelocityEnabled[midiChannel] = velocityEnabling || velocityValueChanged;
velocityButtonPressed = false;
velocityValueChanged = false;
velocityEnabling = false;
updateVelocityDisplay();
}
}
// Force MIDI channel
for (int i = 0; i < LAUNCHKEY_FORCE_MIDI_CHANNELS; i++) {
if (type == usbMIDI.NoteOn && data1 == LAUNCHKEY_PAD_CHANNEL[i]) {
midiChannel = i; // i == midiChannel ? 0 : i;
updateChannelDisplay();
break;
}
}
for (int i = 0; i < LAUNCHKEY_BEATS; i++) {
if (data1 == LAUNCHKEY_PAD_BEATS[i]) {
// Change fixed velocity level
if (velocityButtonPressed) {
velocityValueChanged = true;
int idx = fixedVelocityLevel[midiChannel];
heldBeatPad[idx] = false;
showPad(LAUNCHKEY_PAD_BEATS[idx], beatDisplay[idx]);
fixedVelocityLevel[midiChannel] = i;
heldBeatPad[i] = true;
showPad(data1, LAUNCHKEY_COLOR_BEAT_SELECTING);
break;
}
// Allow the beat display pads to work as regular drum pads
else {
heldBeatPad[i] = type == usbMIDI.NoteOn;
if (heldBeatPad[i]) {
showPad(data1, LAUNCHKEY_COLOR_BEAT_PLAYING);
} else {
showPad(data1, beatDisplay[i]);
}
midiChannelOffset = 9;
data1 = 60 + i;
preventMessage = false;
break;
}
}
}
// Send CC messages from Device / Volume / Pans / Sends knobs on MIDI channel 15
if (type == usbMIDI.ControlChange) {
preventMessage = false;
midiChannelOverride = 15;
}
// Don't actually send Launchkey Mini control signals to the USB host or blink the LED
if (preventMessage) {
return false;
}
}
// Completely override MIDI channel
if (midiChannelOverride) {
channel = midiChannelOverride;
}
// Set the MIDI channel if the data is coming from the default channel
else if (channel == 1) {
channel = midiChannel + 1 + midiChannelOffset;
if (type == usbMIDI.NoteOn && fixedVelocityEnabled[midiChannel]) {
data2 = getVelocity(midiChannel);
}
}
// ======================================================================
// Convert Note On + Sustain + Note Off => Note On + "Sustained" Note Off
// ======================================================================
if (type == usbMIDI.ControlChange && data1 == SUSTAIN_CC_NUMBER) {
isSustaining[channel] = data2 != 0;
showSustain(isSustaining[channel]);
// Sustain released, send all pending Note Off messages
if (!isSustaining[channel]) {
for (int note = 0; note < 127; note++) {
if (isNoteOff[channel][note]) {
usbMIDI.send(usbMIDI.NoteOff, note, 0, channel, cable);
}
isNoteOff[channel][note] = false;
}
}
// Don't actually send MIDI CC 64 Sustain messages, but blink the LED
return true;
}
if (isSustaining[channel]) {
if (type == usbMIDI.NoteOn) {
// Send a Note Off immediately so that multiple sequntial Note On messages aren't sent
if (isNoteOff[channel][data1]) {
usbMIDI.send(usbMIDI.NoteOff, data1, 0, channel, cable);
}
// Cancel any pending Note Off
isNoteOff[channel][data1] = false;
}
else if (type == usbMIDI.NoteOff) {
// Store Note Off for later
isNoteOff[channel][data1] = true;
// Don't send the Note Off now
return true;
}
}
usbMIDI.send(type, data1, data2, channel, cable);
}
else {
unsigned int SysExLength = data1 + data2 * 256;
usbMIDI.sendSysEx(SysExLength, sysexarray, true, cable);
}
return true;
}
// Send data from the upstream host (eg. computer) to the downstream MIDI device (eg. controller)
bool sendToDownstreamDevice(byte type, byte data1, byte data2, byte channel, const uint8_t *sysexarray, byte cable) {
int prevMessageType = hostCurrentMessageType;
hostCurrentMessageType = MIDI_MESSAGE_DEFAULT;
if (type != usbMIDI.SystemExclusive) {
// Handle clock and MMC messages
if (type == usbMIDI.Clock) {
hostCurrentMessageType = prevMessageType == MIDI_MESSAGE_NONE ? MIDI_MESSAGE_CLOCK : prevMessageType;
updateClock();
} else if (type == usbMIDI.Start || type == usbMIDI.Continue) {
startClock();
} else if (type == usbMIDI.Stop) {
stopClock();
}
midilist[cable]->send(type, data1, data2, channel);
// if (type == usbMIDI.Clock) {
// return false;
// }
}
else {
unsigned int SysExLength = data1 + data2 * 256;
midilist[cable]->sendSysEx(SysExLength, sysexarray, true);
}
return true;
}
@cowboy
Copy link
Author

cowboy commented Aug 29, 2022

Using this as a reference https://github.com/giezu/LaunchkeyMiniMK3 I wrote a whole bunch of code in my Teensy that completely remaps the 16 pads in my Launchkey Mini MK3:

  • The first 6 of the bottom 8 pads are a MIDI channel selector (effectively a radio button selecting between channels 1-6) that affects the keyboard, 8 CC knobs, pitch and modulation
  • The 7th of the bottom 8 pads shows sustain pedal status
  • Sustain + Note On + Note Off is translated to Note On + "Pre-Sustained" Note Off (suppressing the actual CC 64 messages)
  • The 8th of the bottom 8 pads toggles fixed velocity for the keys/pads. Holding this pad down and pressing one of the top 8 pads selects a different level of fixed velocity (eg. pad #4 = 63, pad #8 = 127) for the selected MIDI channel
  • The top 8 pads show the current beat out of 2 measures along with the sixteenth note
  • The top 8 pads also function as drum pads that play on whichever MIDI channel is selected plus 9 (eg. channel 1 keys => drum pad channel 10)

@rickrode37
Copy link

It would be interesting to see how the code structure handles device communication and optimization. Discussions around unique tools and experimental coding approaches like this often remind me of the chaotic physics and creativity found in games like Ragdoll Hit, where unexpected interactions can lead to fun and surprising results.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment