Skip to content

Instantly share code, notes, and snippets.

@cowboy
Last active July 27, 2023 04:37
Show Gist options
  • Save cowboy/e37d95508b22bd79d1649c09db71d721 to your computer and use it in GitHub Desktop.
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)

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