Last active
July 27, 2023 04:37
-
-
Save cowboy/e37d95508b22bd79d1649c09db71d721 to your computer and use it in GitHub Desktop.
Launchkey Mini MK3 Supercharger (USB MIDI) - for Teensy 4.1
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// =============================================================== | |
// 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; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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: