MIDI Button combinations

Hi guys.
I would like to send MIDI notes when 2 buttons are pressed at the same time.
Let's say I have a control surface with 3 MIDI note buttons.
When I press buttons 1 and 2 OR buttons 1 and 3 I would like to send different MIDI notes.

See the sketch below, unfortunately it doesn't work as expected:

#include <Control_Surface.h>
USBMIDI_Interface midi;
using namespace MIDI_Notes;

//BUTTONS COMBINATION
const byte PinBut1 = 2;
const byte PinBut2 = 3;
const byte PinBut3 = 4;
byte butState;

//BUTTONS
NoteButton button1 = {
   2,
  {note(A, 0), CHANNEL_1}, // Note on MIDI channel 
};

NoteButton button2 = {
  3,
  {note(Bb, 0), CHANNEL_1}, // Note on MIDI channel 
};

NoteButton button3 = {
   4,
  {note(C, 0), CHANNEL_1}, // Note on MIDI channel 
};



void setup() {
Serial.begin(115200);
Control_Surface.begin();

pinMode (PinBut1, INPUT_PULLUP);
pinMode (PinBut2, INPUT_PULLUP);

butState = digitalRead (PinBut1) | digitalRead (PinBut2);
butState = digitalRead (PinBut1) | digitalRead (PinBut3); 
}


void loop() {
Control_Surface.loop();

const MIDIAddress note0 = MIDI_Notes::Eb[1];
const MIDIAddress note1 = MIDI_Notes::E[1];
const uint8_t velocityHIGH = 127;            
const uint8_t velocityLOW = 0; 

// COMBINATION 1+2
byte but = digitalRead (PinBut1) | digitalRead (PinBut2);
if (butState != but)  {
butState = but;
delay (20);

if (LOW == butState)
midi.sendNoteOn(note0, velocityHIGH);

// COMBINATION 1+3
byte but = digitalRead (PinBut1) | digitalRead (PinBut3);
if (butState != but)  {
butState = but;
delay (20);

if (LOW == butState)
midi.sendNoteOn(note1, velocityHIGH);

}}}

The problems:

  • The first combination (buttons 1+2) works but it keeps sending notes if the buttons are held pressed.
  • The second combination (buttons 1+3) doesn't work.

Could you guys help me with this?
Much appreciated.

You can not use the same variable butState to keep track of the two states you want to monitor. They need to be different.

If you want to only send a note once, then you need to detect when a button combination becomes pressed, not if a button combination is pressed. Check out the State Change Detection example in the IDE to learn how (File->examples->02.digital->StateChangeDetection)

1 Like

I don't see anything in the posted code that is involved with how close to simultaneously the combination needs to be pressed.

If single presses are to be handled as well as combinations that's a problem. You don't want to hang around too long waiting on a possible combination in a musical instrument context, but there will be some delay introduced no matter it be very short.

a7

You should ask @PieterP , since he is the author of the library.

i have seen sketches with switches and their combination does control change, but buttons?
is it a big deal to add one or two additional buttons?

1 Like

wel you can, but not the way it is done now. You can do something like

butState = digitalRead (PinBut1) | digitalRead (PinBut2) << 1 | digitalRead (PinBut3) << 2; 

and then test for the whole thing like

byte but = digitalRead (PinBut1) | digitalRead (PinBut2) << 1 | digitalRead (PinBut3) << 2; 
if (butState != but)  { etc.

But you will have to allow for the de-bounce and you haven't yet. You will not be able to start pressing to both buttons exactly at once. But at least you can now just simply compare all states at once from a single variable.
So after every change you should check to see if the state is constant for a period of something like 100ms before you actually accept it as a new state.

and so

if ((but == 0b100) && (butState == 0b111)) {

2 & 3 are being pressed now and before none were pressed.

And at the same time you are running the control surface on those same pins ? I am not so sure that is a good idea. If there is a method for combined button in there you should use that, otherwise you should do all of it outside of that.

1 Like

Thanks for your help guys.

I added a second variable but still can't figure out what's the C++ correspondent function of when rather than if.
The new sketch get compiled, but I haven't tested it yet.

#include <Control_Surface.h>
USBMIDI_Interface midi;
using namespace MIDI_Notes;

//BUTTONS COMBINATION
const byte PinBut1 = 2;
const byte PinBut2 = 3;
const byte PinBut3 = 4;
byte butState1;
byte butState2;

//BUTTONS
NoteButton button1 = {
   2,
  {note(A, 0), CHANNEL_1}, // Note on MIDI channel 
};

NoteButton button2 = {
  3,
  {note(Bb, 0), CHANNEL_1}, // Note on MIDI channel 
};

NoteButton button3 = {
   4,
  {note(C, 0), CHANNEL_1}, // Note on MIDI channel 
};



void setup() {
Serial.begin(115200);
Control_Surface.begin();

pinMode (PinBut1, INPUT_PULLUP);
pinMode (PinBut2, INPUT_PULLUP);

butState1 = digitalRead (PinBut1) | digitalRead (PinBut2);
butState2 = digitalRead (PinBut1) | digitalRead (PinBut3); 
}


void loop() {
Control_Surface.loop();

const MIDIAddress note0 = MIDI_Notes::Eb[1];
const MIDIAddress note1 = MIDI_Notes::E[1];
const uint8_t velocityHIGH = 127;            
const uint8_t velocityLOW = 0; 

// COMBINATION 1+2
byte but = digitalRead (PinBut1) | digitalRead (PinBut2);
if (butState1 != but)  {
butState1 = but;
delay (20);

if (LOW == butState1)
midi.sendNoteOn(note0, velocityHIGH);

// COMBINATION 1+3
byte but = digitalRead (PinBut1) | digitalRead (PinBut3);
if (butState2 != but)  {
butState2 = but;
delay (20);

if (LOW == butState2)
midi.sendNoteOn(note1, velocityHIGH);

}}}

if you want to make sure the note stops you will have to use 'else'

if (LOW == butState2) midi.sendNoteOn(note1, velocityHIGH);
else midi.sendNoteOn(note1, velocityLOW);

btw it is a good idea to be consistent in use of '{' & '}' with if statements and use ctrl-T to auto format your code. Don't put more than 1 '}" on a line so it is easier to see the nesting thru the indent int the code.
Sa,e here of course

if (LOW == butState1) midi.sendNoteOn(note0, velocityHIGH);
else  midi.sendNoteOn(note0, velocityLOW);

also

byte but = 

You declare a variable twice with the same name within the same scope, that is not a good idea ! you can re-use the variable without issue, but re-declare ? the compiler should throw a warning about it.

It doesn't look like you studied the State Change Detection example at all. Please go look at it and it will show you exactly how to do it.
https://docs.arduino.cc/built-in-examples/digital/StateChangeDetection/

If it is the same identifier in the same scope, the compiler should throw an error.

a7

yeah of course, and now i see that it isn't the same scope, in the original code.

byte but = digitalRead (PinBut1) | digitalRead (PinBut2);
if (butState1 != but)  {

so really in the original code the rest only gets executed if there is a change in the state of 1 & 2 combination.
Ah well, that explains the strange behavior. This is what happens with poorly formatted and indented code i guess.

I was too lazy to look - I figured the OP had presented code that did not compile, admittedly a very rare occurrence around these parts. Cough!

a7

The code in question does not seem to be related to the Control Surface library, it's just standard digital input state change detection logic.
That being said, Control Surface does come with a Button class that could be useful for that purpose: Control Surface: 2.Button.ino

1 Like

Hi guys, thanks for your answers.
@Deva_Rishi I tried to use your bit of sketch;
it runs but it constantly send both notes regardless if I press the buttons or not (see the screenshot attached).

And see the sketch below:

#include <Control_Surface.h>
USBMIDI_Interface midi;
using namespace MIDI_Notes;


const byte PinBut1 = 2;
const byte PinBut2 = 3;
const byte PinBut3 = 4;
byte butState1;
byte butState2;


//BUTTONS
NoteButton button1 = {
  2,
  { note(Ab, -1), CHANNEL_1 },  // Note on MIDI channel
};

NoteButton button2 = {
  3,
  { note(A, -1), CHANNEL_1 },  // Note on MIDI channel
};

NoteButton button3 = {
  4,
  { note(Bb, -1), CHANNEL_1 },  // Note on MIDI channel
};

void setup() {

  Serial.begin(115200);
  Control_Surface.begin();

RelativeCCSender::setMode(relativeCCmode::MACKIE_CONTROL_RELATIVE);

  pinMode(PinBut1, INPUT_PULLUP);
  pinMode(PinBut2, INPUT_PULLUP);

  butState1 = digitalRead(PinBut1) | digitalRead(PinBut2);
  butState2 = digitalRead(PinBut1) | digitalRead(PinBut3);
}

void loop() {

  Control_Surface.loop();

  const MIDIAddress note0 = MIDI_Notes::Eb[1];
  const MIDIAddress note1 = MIDI_Notes::E[1];
  const uint8_t velocityHIGH = 127;
  const uint8_t velocityLOW = 0;

  // COMBINATION 1+2
  if (LOW == butState1) midi.sendNoteOn(note1, velocityHIGH);
  else midi.sendNoteOn(note1, velocityLOW);

  // COMBINATION 1+3

  if (LOW == butState2) midi.sendNoteOn(note0, velocityHIGH);
  else midi.sendNoteOn(note0, velocityLOW);
}

Thank you guys and sorry if sometimes I write Gibberish codes, I'm still learning.

Seriously ?
first of all all button need to be declared INPUT_PULLUP
You also have to read the every time.
Then you compare the value with the old value.

If there is a change, check for every possible double press in the new value for noteOn
and for the old state for noteOff.

Then you update the old value.

There is no de-bounce in this code, But try this.

#include <Control_Surface.h>
USBMIDI_Interface midi;
using namespace MIDI_Notes;


const byte PinBut1 = 2;
const byte PinBut2 = 3;
const byte PinBut3 = 4;
byte butState;

//BUTTONS
NoteButton button1 = {
  2,
  { note(Ab, -1), CHANNEL_1 },  // Note on MIDI channel
};

NoteButton button2 = {
  3,
  { note(A, -1), CHANNEL_1 },  // Note on MIDI channel
};

NoteButton button3 = {
  4,
  { note(Bb, -1), CHANNEL_1 },  // Note on MIDI channel
};

void setup() {

  Serial.begin(115200);
  Control_Surface.begin();

  RelativeCCSender::setMode(relativeCCmode::MACKIE_CONTROL_RELATIVE);

  pinMode(PinBut1, INPUT_PULLUP);
  pinMode(PinBut2, INPUT_PULLUP);
  pinMode(PinBut3, INPUT_PULLUP);

  butState = digitalRead (PinBut1) | digitalRead (PinBut2) << 1 | digitalRead (PinBut3) << 2;
}

void loop() {

  Control_Surface.loop();

  const MIDIAddress note0 = 15; //MIDI_Notes::Eb[1];
  const MIDIAddress note1 = 16; //MIDI_Notes::E[1];
  const uint8_t velocityHIGH = 127;
  const uint8_t velocityLOW = 0;

  byte but = digitalRead (PinBut1) | digitalRead (PinBut2) << 1 | digitalRead (PinBut3) << 2;
  if (butState != but)  {
    // COMBINATION 1+2
    if (but == 0b100) {  // pin 1 & 2 pressed
      midi.sendNoteOn(note1, velocityHIGH);
    }
    else if (butState == 0b100) {  // were pressed, but aren't now
      midi.sendNoteOn(note1, velocityLOW);
    }    
    // COMBINATION 1+3
    if (but == 0b010) {  // pin 1 & 2 pressed
      midi.sendNoteOn(note0, velocityHIGH);
    }
    else if (butState == 0b010) {
      midi.sendNoteOn(note0, velocityLOW);
    }
    // COMBINATION 2+3
    if (but == 0b001) {  // pin 1 & 2 pressed

    }
    else if (butState == 0b001) {

    }
    butState = but;

  }
}

What you posted is not my sketch at all and doesn't contain anything that i wrote !

one of the button pins was not pulled up, and the button states never got updated. I am seriously confused about what you posted, but i think that with i posted, you will sort of get what you want. I do hope something clicks and you understand a bit better now.

Hello @Deva_Rishi
thanks for your sketch.
I'm still trying to make it work, unsuccessfully for now I'm afraid.
Also I would like to know what the binary values stand for (0b010 & 0b001).

@PieterP I'd be very intrigued to use the Button you mentioned.

..but how can I use a combination of two or more variables instead of just Button? (see the example below).

if (pushbutton.update() == Button::Falling) {

Thank you all!

The binary values are the result of

digitalRead (PinBut1) | digitalRead (PinBut2) << 1 | digitalRead (PinBut3) << 2;

bitwise 'or' of the result of the pin state reads. each shifted into their individual bits.
(read up on bitshifting and bitwise logic operations)
All pins are set to INPUT_PULLUP, so if one of the pins is pulled LOW the result of the digitalRead() is
HIGH. Actually digitalRead() returns an int and HIGH == 1 and LOW == 0 by definition. Some people may argue not to use them that way since they are just definitions and they may change in the future, In way that is correct, but somehow i do doubt that it will ever happen. Anyway if all pins are HIGH but == 0b111. for any of the buttons one of the bits is '0' if it is being pressed. (0b101 is the result when pinBut2 is LOW and being pressed.) The program works, maybe the hardware is faulty ?
i ran a simple testprogram to test this particular method,

void setup() {
  Serial.begin(115200);
  pinMode(2, INPUT_PULLUP);
  pinMode(3, INPUT_PULLUP);
  pinMode(4, INPUT_PULLUP);
  Serial.println(digitalRead(2), DEC);
  Serial.println(digitalRead(3), DEC);
  Serial.println(digitalRead(4), DEC);
  Serial.print("Or ");
  uint8_t but =  digitalRead(2) | digitalRead(3) << 1 |  digitalRead(4) << 2;
  
  Serial.println(but, BIN);
}

void loop() {

}

I had no buttons i wanted to connect, but just used some jumper cables to confirm it does just in case.

Why don't you show some pictures of your hardware.

Hello @Deva_Rishi
I managed to make it work!
There was an issue with IDE.
I really appreciate your help!

However it doesn't seem to work if use buttons that are connected to the Arduino board through a multiplexer.
The sketch gets uploaded and single buttons work as expected BUT buttons combinations don't work.
I wonder if it is anything related with the multiplexer's frequency.

How are you reading the buttons through the multiplexer ? (show the sample sketch)

@Deva_Rishi Yes, I wanted to experiment button combinations through a multiplexer.
As I said, all buttons work fine, except the combinations (button1+button2 & button1+button3).

I can't figure out why. Any idea?
Thank you!

#include <Control_Surface.h>
USBMIDI_Interface midi;
using namespace MIDI_Notes;

CD74HC4051 mux{ 5, { 2, 3, 4 } }; 

const byte PinBut1 = mux.pin(4);
const byte PinBut2 = mux.pin(6);
const byte PinBut3 = mux.pin(7);
byte butState;


//BUTTONS
NoteButton button1 = {
  mux.pin(4),
  { note(Ab, -1), CHANNEL_1 },  // Note on MIDI channel
};

NoteButton button2 = {
  mux.pin(6),
  { note(A, -1), CHANNEL_1 },  // Note on MIDI channel
};

NoteButton button3 = {
  mux.pin(7),
  { note(Bb, -1), CHANNEL_1 },  // Note on MIDI channel
};


void setup() {

  Serial.begin(115200);
  Control_Surface.begin();

  RelativeCCSender::setMode(relativeCCmode::MACKIE_CONTROL_RELATIVE);

  pinMode(PinBut1, INPUT_PULLUP);
  pinMode(PinBut2, INPUT_PULLUP);
  pinMode(PinBut3, INPUT_PULLUP);


  byte but = digitalRead(PinBut1) | digitalRead(PinBut2) << 1 | digitalRead(PinBut3) << 2;
}

void loop() {

  Control_Surface.loop();

  const MIDIAddress note0 = MIDI_Notes::Eb[1];
  const MIDIAddress note1 = MIDI_Notes::E[1];
  const uint8_t velocityHIGH = 127;
  const uint8_t velocityLOW = 0;

  
byte but = digitalRead (PinBut1) | digitalRead (PinBut2) << 1 | digitalRead (PinBut3) << 2;

if (butState != but)  {
    // COMBINATION 1+2
    if (but == 0b100) {  // pin 1 & 2 pressed
      midi.sendNoteOn(note1, velocityHIGH);
    }
    else if (butState == 0b100) {  // were pressed, but aren't now
      midi.sendNoteOn(note1, velocityLOW);
    }    
    // COMBINATION 1+3
    if (but == 0b010) {  // pin 1 & 2 pressed
      midi.sendNoteOn(note0, velocityHIGH);
    }
    else if (butState == 0b010) {
      midi.sendNoteOn(note0, velocityLOW);
    }
    
  
    butState = but;
}}