Go Down

Topic: MIDI transpose and channel changer (Read 2558 times) previous topic - next topic

thistof

For the long term goal, I'm trying to build a project with which I can take incoming midi from various channels output by a midi guitar, transpose each channel independently, combine them if necessary, and output to the channel of my choice.

I decided to focus on channel changing first, and build the transpose functionality in later. Bite size chunks. The best piece of code I've found was in an old thread at http://forum.arduino.cc/index.php?topic=119362.0
And with a little tweaking I came up with the following code.

Code: [Select]
byte incomingByte;

void setup(){
   Serial.begin(31250);
}
 
void loop () {
  if (Serial.available() > 0) {   // read the incoming byte:
    incomingByte = Serial.read();
    if(incomingByte & 0x80) {
      // Command byte. Change channel to 0xf if this is NOT a system message
      if((incomingByte & 0xf0) != 0xf0)incomingByte |= 0x0f;
    }
    Serial.write(incomingByte); //debugging over midi out
  }
}


This allows me to put any midi channels into the device, and it translates them all to channel 16, which is a great start, and I'm quite pleased. Next I tried changing the line

Code: [Select]
if((incomingByte & 0xf0) != 0xf0)incomingByte |= 0x0f;

to

Code: [Select]
if((incomingByte & 0xf0) != 0xf0)incomingByte |= 0x0e;

thinking that it would send them all to channel 15. It kinda worked, but now any notes coming from odd numbered channels go to channel 15, while those from even numbered channels are still output to channel 16. Any ideas what I'm missing?

PieterP

That's normal: channel 15 = 14= 0x0E = 0b00001110.
And an even channel is represented by 0bmmmmccc1, where mmmm is the message type, and ccc the three MSB of the channel. An odd channel would be 0bmmmmccc0.

Performing a bitwise or:
Odd:
0bmmmmccc0
0b00001110
---------- |
0bmmmm1110

That's channel 15 (0b1110)

Even:
0bmmmmccc1
0b00001110
---------- |
0bmmmm1111

So that's channel 16 (0b1111)

You have to get rid of the final 1.
You can easily do this using a bitwise and to set the channel nibble to zero before performing the bitwise or:

Even:
0bmmmmccc1
0b11110000
---------- &
0bmmmm0000
0b00001110
---------- |
0bmmmm1110


This will also work for channels other than 15.

Pieter

thistof

#2
Jun 24, 2017, 02:32 am Last Edit: Jun 25, 2017, 12:06 am by thistof Reason: Next step of the project.
Thanks Pieter, that makes sense and works perfectly.

My next attempt was a basic transpose of all channels, which worked just fine.
Code: [Select]
byte statusByte;
byte trans = -2;

void setup(){
   Serial.begin(31250);
}
 
void loop () {
  if (Serial.available() > 0) {
    statusByte = Serial.read(); //read incoming byte:
   
    if(statusByte < 0x80) {
        statusByte += trans;
    }
   
    Serial.write(statusByte); //midi out
  }
}


So I moved onto the next step, transposing only one channel. In this case, channel 6, to simulate "Drop D" tuning.
Code: [Select]
byte statusByte;
byte chan;
byte trans = -2;

void setup(){
   Serial.begin(31250);
}
 
void loop () {
  if (Serial.available() > 0) {
    statusByte = Serial.read(); //read incoming byte:

    if(statusByte > 0x7F) {
      chan = statusByte;
      chan &= 0b00001111;
    }
   
    if(statusByte < 0x80) {
      if(chan & 0x05) {
        statusByte += trans;
      }
    }

    Serial.write(statusByte); //midi out
  }
}


This produced some unexpected results, as channels 2, 4, 5, and 6 were all transposed. I tried a bunch of different comparators in the (chan & 0x05) such as &&, ==, |, etc. but those only made things worse. I'll continue to think about this, maybe it will come to me in a dream as has happened in the past.

Grumpy_Mike

Quote
This produced some unexpected results, as channels 2, 4, 5, and 6 were all transposed
The & operation is the bit wise AND it is not a comparison operation. What ANDing with 5 does is just leave bits 0 and 2 in the channel number. Also the variable chan is only set if the status byte is > 0x7E, when it is less you use the channel value from last time.

thistof

I agree, I only tried that (and every other comparison operator and bitwise operator I could find) since

if(chan == 0x05)

didn't work. How can I actually check if the channel is 0x05 if the == operator doesn't do it?

PieterP

The first byte of the MIDI event is 0b1mmmcccc. So the channel is 0b1mmmcccc & 0b00001111. Add 1 to get the actual channel (cccc = 0 → channel 1).

Then check if that channel equals 5.

Pieter

Grumpy_Mike

Quote
how can I actually check if the channel is 0x05 if the == operator doesn't do it?
The == operator does do it, it is probable that the chan variable has not been set right.
I would use
Code: [Select]

if((chan & 0xF) == 0x5) { // you have found channel 6

 remember that in midi channel 1 has a numeric value of 0, because musicians can't cope with the concept of channel zero. So MIDI channels run from 1 to 16 with the numbers in code being 0 to 15.

thistof

#7
Jul 21, 2017, 07:55 am Last Edit: Jul 21, 2017, 10:04 am by thistof
Lol, no I don't have a problem with the channel number counting, it's the same idea as when using photoshop and the 8-bit color channels go from 0-255.

It's been a couple weeks since I've had a chance to work on this, so I reverse engineered things a little to come up with a clearer look at what's going on.

With lines 19 and 20 commented out, all 6 channels are transposed down two half steps (based on the variable "trans" being set to -2) This works exactly how I want it to, other than that it seems to decrease the note velocity a bit.

Now when I uncomment those two lines, I would expect that transpose to be applied only to channel 6 (aka 0x05). It does apply it only to channel six, but now it transposes it much lower than two half steps, and the note velocity on that channel only is extremely low.

So it seems the problem I'm having is where to apply the "trans" variable so that it affects only the pitch (not the velocity) and transposes the appropriate amount.

According to https://www.midi.org/specifications/item/table-2-expanded-messages-list-status-bytes the note number (0-127) should be in the "2nd byte" but I'm not really sure what to do with that information.

Code: [Select]

byte statusByte;
byte chan;
byte trans = -2;

void setup(){
   Serial.begin(31250);
}
 
void loop () {
  if (Serial.available() > 0) {
    statusByte = Serial.read(); //read incoming byte:

    if(statusByte > 0x7F) {
      chan = statusByte;
      chan &= 0b00001111;
    }
   
    if(statusByte < 0x80) {
//      if(chan == 0x05) {
        statusByte += trans;
//      }
    }
Serial.write(statusByte); //midi out
  }
}

Grumpy_Mike

#8
Jul 21, 2017, 11:01 am Last Edit: Jul 21, 2017, 11:07 am by Grumpy_Mike
You are applying the transformation to both the note number and the velocity because you do not count the bytes you have received.

You want to have a variable that counts bytes so when you see something greater than &7F you set it to one. Then for each byte following you increment it. In your test for the channel number you include a test that the bytes received is equal to 2

Code: [Select]
if(chan == 5 && numberOfBytes == 2) {

Then keep the note number in a variable and also keep the command in a variable and the velocity number.

Only write to MIDI out when you have received three bytes then write those variables out one at a time.

Look at the code you can download from my project here http://www.thebox.myzen.co.uk/Hardware/MIDI_Shield.html

thistof

Ok, I tried your suggestion via a series of if/ifelse/else statements and got the same results I was having before.
Also, would what your suggesting be similar to creating a data range by saying:

if(chan == 5 && statusByte > 0x7F && statusByte < 0x100) {

Maybe I'm going about this the wrong way. Is there a way to break the whole midi message up into three different variables (let's call them statusByte, dataByte1, and dataByte2) and deal with each byte separately, only to put them back together just before serial write? Seems like that might be a little easier for me to digest, even if it's not as efficient code-wise.

Another perhaps basic question, that maybe I shouldn't take for granted. When the bytes are received via Serial.read, are we talking three separate packets, each containing a one byte number, or is it one huge number and we're dealing with the number places?

For example, in decimal (something a little easier for me to quickly understand in an analogy) if we're given the number 120,522,184 and I only want to increment the middle set, I could use "+= 1000" However if we're talking three discrete numbers: 120,   522,   and   184, then adding 1000 would do what exactly? Would it add 1000 to each of those three numbers?

Grumpy_Mike

Quote
Ok, I tried your suggestion via a series of if/ifelse/else statements and got the same results I was having before.
Then you did not do what I said. Post you code and let's see what you did wrong.

Quote
Is there a way to break the whole midi message up into three different variables
That is what I told you to do.

Each serial read reads one byte that can be considered to be a number between 0 and 255. One MIDI message for note on / note off is three bytes.

PieterP

#11
Jul 22, 2017, 11:21 am Last Edit: Jul 22, 2017, 11:28 am by PieterP
The first byte of a MIDI packet has msb == 1, and all following bytes have msb == 0. That should help you split up the message.
You could have an array of 3 uint8_t's. Everytime you receive a byte, add it to the array and increment the index (don't let it go higher than 2). Every time the byte you received has msb == 1, reset the index to zero.
If the message type is noteOn, noteOff or Control Change, you know that the message is complete after you've received 3 bytes. If you need Program Change as well, it's complete after 2 bytes, so don't wait for a third one.

Edit: you probably won't even need to store them in an array. Just check what the index is, change the byte if you have to, and write it out.

Pieter

thistof

Success! Thanks guys, and esp for clarifying that for me Pieter. Thinking of the msb of each byte makes more sense than considering the overal value of the byte, which I had been doing. Practically it doesn't make a difference, I suppose, but it does wonders for mental organization.

I also figured out the note velocity problem by transposing only if the byteCount wasn't two:

Code: [Select]
// Transpose
    if(chan == 0x05 && byteCount != 2) {
        midiByte += trans5;
    }


I'm not sure why but apparently the transpose needs to be applied to bytes 0 and 1, doing it only to byte 1 returns no results.

Here's the working code for Drop-D tuning on this rockband mustang guitar:

Code: [Select]
byte midiByte;
byte chan;
byte trans5 = -2;
byte byteCount;

void setup(){
   Serial.begin(31250);
}
 
void loop () {
//Read Byte
  if (Serial.available() > 0) {
    midiByte = Serial.read(); //read incoming byte:

// Sort Bytes and assign Channel variable
    if (midiByte > 0x7F){
      byteCount = 0;
      chan = midiByte;
      chan &= 0b00001111;
    }
    else{
      byteCount ++;
    }

// Transpose
    if(chan == 0x05 && byteCount != 2) {
        midiByte += trans5;
    }

// Condense All Channels To Channel 00   
    if(midiByte > 0x7F) {
        midiByte &= 0b11110000;
        midiByte |= 0x00;
    }

// Send Byte
    Serial.write(midiByte); //midi out
  }
}

PieterP

"byte trans5 = -2;"

A byte is an unsigned 8-bit number (uint8_t), so it can only be between 0 and 255, it can't be negative. Assigning -2 to a uint8_t just converts it to 254.
You want an int8_t instead of a byte.
In this case, it works, because the result of the addition overflows.

It should work without "transposing" the first status byte. It doesn't mean anything to subtract 2 from the status. It just changes the channel, and in the worst case (if the channel nibble is 0 or 1), it will change the message type nibble, resulting in weird behavior.

Transposing means only changing the second byte. This is the first data byte, and for NoteOn/NoteOff events, this is the note number.

Pieter

thistof

Thanks for the tip, I switched to the int_8 variable as suggested.

I agree that it doesn't make sense to add/subtract that trans5 value from the status byte. Originally I expected that it only needed to be applied to the second byte (first data byte) and I tried that again just now, along with every other combination, but no other combination transposes the pitch of the note coming out.

The only way I am getting the transpose to work is by using:

if(chan == 0x05 && byteCount != 2)

which seems that it would apply it to all but the third byte, if my byteCount incrementing has been done properly. Maybe I am miscounting and accidentally getting the right results the wrong way?

Go Up