Jump to content
Sign in to follow this  
richmaes

Arduino project to let a Helix control a Voodoo Labs GCX

Recommended Posts

I came up with this cool solution to control my GCX from my Line 6 Helix, but it could be used with practically anything. 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 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.

 

#UPDATED 2/25/17:  I have also added a feature to generate a MIDI clock using channel 16 CC# 00 which is a feature that Helix doesn't currently have right now either.  Basically, CC#00 value + 40 is the BPM of the clock, so a CC#00 value == 70 translates to a BPM of 110.  Because CC value can only go up to 127, the max BPM is 167. 

 

To demonstrate the PC BS conversion process,

  • 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

Notice that the Bank Select value basically controls bit 8 and the PC value represents bits 6 down to 0.  Taken together, they represent a 8 bit value representing the 8 loops of the GCX.

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. icon_e_wink.gif 

This code is hardcoded to channel 16 because, that is the "ONLY" channel a GCX will receive on.  This code should allow other messages through, with only the Arduino's SW loop latency. There maybe some other side effects to, but those side effects should be limited to channel 16 PC and channel 16 CC#32.

I have tested this on a Arduino UNO board with a Olimex MIDI Shield.

// GCX Editor
// By Rich Maes
// email:r i c h m a e sAThotmailDOTcom
// 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
#define REQUIRE_CLOCK_SETTING 1

boolean byteReady;
boolean sendCCMessage;
boolean generateClock;
boolean disableClockOnNextPC;
int ccMsgsToSend;
unsigned char midiByte;
unsigned char capturePCByte;
unsigned char captureCCByte;
// unsigned char captureCCMSbyte;
unsigned long lastClock;
unsigned long captClock;
unsigned long clk_period_us;

// 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_3PARAM_2,
    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;
  // captureCCMSbyte = 0;
  generateClock = false;
  disableClockOnNextPC = true;
  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 (generateClock) {
        captClock = micros();
        if (lastClock > captClock) {
            // we have a roll over condition
            if (clk_period_us <= (4294967295 - (lastClock - captClock))) {
                lastClock = captClock;
                addQueue(0xF8);
            }
        } else if (clk_period_us <= captClock-lastClock) {
                lastClock = captClock;
                addQueue(0xF8);
        }
    }

    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);
                #ifdef REQUIRE_CLOCK_SETTING
                if (disableClockOnNextPC) generateClock = false;
                disableClockOnNextPC = true;
                #endif
                break;
            case STATE_2PARAM_1:
                if (midiByte == 32) state = STATE_2PARAM_2;
                if (midiByte == 00) {
                  state = STATE_3PARAM_2;
                  #ifdef REQUIRE_CLOCK_SETTING
                      // We have just update the clock so don't force the next PC to disable
                      // clock generation
                      disableClockOnNextPC = false;
                  #endif
                }
                else state = STATE_2PARAM_1_CONTINUE;
                break;
            case STATE_2PARAM_2:
                state = STATE_2PARAM_1_CONTINUE;
                captureCCByte = midiByte;
                break;
            case STATE_3PARAM_2:
                state = STATE_2PARAM_1_CONTINUE;
                clk_period_us = 60000000 / (24 * (40 + midiByte));
                generateClock = (midiByte > 0);
                lastClock = micros();
                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;
  }
}

Share this post


Link to post
Share on other sites

This is very cool. On a side note, would a midi solutions event processor be able to handle similar tasks?

  • Upvote 1

Share this post


Link to post
Share on other sites

If my understanding of the unit is correct, you could program one midi command from the helix to translate to multiple cc's to the GCX via the event processor. In theory, this would free up the other 5 instant commands.

 

I like the out of the box thinking that you're using to solve this issue. If I were not trying to do everything strictly in the helix, I would be trying to do similar things with my GCX.

  • Upvote 1

Share this post


Link to post
Share on other sites

This is very cool. On a side note, would a midi solutions event processor be able to handle similar tasks?

So looking at the Midi Solutions Event Processor, it looks like a very full featured solution.  The software guide seems to indicate you can do "If this, and then if this, do this" type of commands which is required for this solution.  So I think you could do it.  The big difference is that the MIDI solutions event processor is about $150 bucks and the Arduino and Midi shield you can get for around $25 without a case.   BTW, it turns out that finding a case that fits an Arduino WITH a MIDI shield is not easy.  I found a guy who makes a taller transparent plastic arduino box.  I have ordered one.  I'll repost when I get it. 

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this  

×