1

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

2

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;
  }
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.