To define the problem, the L6 Helix can generate 6 immediate MIDI messages. These messages are pretty flexible as to what you want, but the Voodoo Labs GCX needs at least 8 CC messages to properly configure it worst case. Also, I don't want to burn all of my immediate commands to control most of the GCX. So I had the idea to create a Arduino project that would take MIDI PC and MIDI Bank Select to produce a 8 bit value which I could then convert into 8 CC messages between CC#80 and CC#87.
1 Answer 1
To solve this problem I used an Arduino UNO and a Olimex MIDI shield. I also want to use MIDI PC messages and Bank Selects.
For reference:
- A Channel 16 PC 77 with Bank select 0 converts to binary 0x4D => Binary 01001101
- A Channel 16 PC 77 with Bank Select 1 converts to binary 0xCD => Binary 11001101
Just to point out, the Bank Select value basically controls bit 8 and the PC value represents bits 6 down to 0
Based on the binary output, I scroll through bits 0 to 7 and send messages on CC 80 to CC 87. If a bit is a 1, I turn the loop on by setting the corresponding CC value to 127 and if it is a zero, I set the CC value to 0.
The Helix can generate a Bank Select and PC as one message, so for the price of one message, this project will make me 8 CC#'s effectively. ;)
It should allow other messages through, with the Arduino's SW loop latency. There maybe some other side effects to, but those should be limited to channel 16 only.
I have tested this on a Arduino UNO board with an Olimex MIDI Shield.
// GCX Editor
// By Rich Maes
// Converts MIDI PC's on channel 16 to GCX CC's starting at 0x80
// Allows everything else to pass.
#define MIDI_PC_CH16 0xCF
#define MIDI_CC_CH16 0xBF
#define MIDI_SYSEX 0xF0
#define MIDI_SYSRT_CLK 0xF8
boolean byteReady;
boolean sendCCMessage;
int ccMsgsToSend;
unsigned char midiByte;
unsigned char capturePCByte;
unsigned char captureCCByte;
// Queue Logic for storing messages
int headQ = 0;
int tailQ = 0;
unsigned char tx_queue[128];
int getQDepth();
void addQueue(unsigned char myByte);
void addCCQueue(unsigned char captureCCByte, unsigned char capturePCByte);
unsigned char deQueue();
static enum {
STATE_UNKNOWN,
STATE_1PARAM,
STATE_1PARAM_CONTINUE,
STATE_2PARAM_1,
STATE_2PARAM_2,
STATE_2PARAM_1_CONTINUE,
STATE_PASSTHRU
} state = STATE_UNKNOWN;
void setup() {
// put your setup code here, to run once:
// Set MIDI baud rate:
Serial.begin(31250);
sendCCMessage = false;
byteReady = false;
midiByte = 0x00;
state = STATE_UNKNOWN;
captureCCByte = 0;
capturePCByte = 0;
ccMsgsToSend = 0;
}
int getQDepth() {
int depth = 0;
if (headQ < tailQ) {
depth = 128 - (tailQ - headQ);
} else {
depth = headQ - tailQ;
}
return depth;
}
void addQueue (unsigned char myByte) {
int depth = 0;
depth = getQDepth();
if (depth < 126) {
tx_queue[headQ] = myByte;
headQ++;
headQ = headQ % 128; // Always keep the headQ limited between 0 and 127
}
}
void addCCQueue(unsigned char myCaptureCCByte, unsigned char myCapturePCByte) {
int i;
if (getQDepth() < 80) {
// There is enough space to add our CC messages
for (i = 0; i < 8; i++) {
addQueue(0xBF);
addQueue(80 + i);
addQueue(127 * ((((myCaptureCCByte * 128) + myCapturePCByte) >> i) % 2));
}
} else {
// This is an error condition. So reset the queue and pointers
headQ = 0;
tailQ = 0;
byteReady = false;
for (i = 0; i < 128; i++) {
tx_queue[i] = 0;
}
}
}
unsigned char deQueue() {
unsigned char myByte;
myByte = tx_queue[tailQ];
tailQ++;
tailQ = tailQ % 128; // Keep this tailQ contained within a limit
// Now that we dequeed the byte, it must be sent.
return myByte;
}
void loop() {
if (byteReady) {
if (midiByte >= 0xF0) {
// This automatically passes all clocks and System Realtime Messages
state = STATE_PASSTHRU;
} else if (midiByte >= 0x80) {
switch (midiByte) {
case MIDI_PC_CH16:
state = STATE_1PARAM;
break;
case MIDI_CC_CH16:
state = STATE_2PARAM_1;
break;
default:
state = STATE_PASSTHRU;
break;
}
} else {
switch (state) {
case STATE_1PARAM:
capturePCByte = midiByte;
state = STATE_1PARAM_CONTINUE;
addCCQueue(captureCCByte, capturePCByte);
break;
case STATE_2PARAM_1:
if (midiByte == 32) state = STATE_2PARAM_2;
else state = STATE_2PARAM_1_CONTINUE;
break;
case STATE_2PARAM_2:
state = STATE_2PARAM_1_CONTINUE;
captureCCByte = midiByte;
break;
default:
state = STATE_PASSTHRU;
break;
}
}
}
if ((state == STATE_PASSTHRU) && byteReady) {
// Just pass messages unaltered. Also don't let any of our modified message through if
// we are passing though. Our burst of modified CC# can wait.
// Serial.write(midiByte);
addQueue(midiByte);
// state = STATE_UNKNOWN;
}
byteReady = false;
if (getQDepth() > 0) {
// We have a byte to send, dequeu and send it
Serial.write(deQueue());
}
}
// The little function that gets called each time loop is called.
// This is automated somwhere in the Arduino code.
void serialEvent() {
if (Serial.available()) {
// get the new byte:
midiByte = (unsigned char)Serial.read();
byteReady = true;
}
}