How to make a button repeat a press when you hold it down?

Hi, I made a MIDI-controller using a Teensy LC. I want the 'PREVIOUS' and 'NEXT' button to send their MIDI message again at a frequency of three times a second when I press it down for more then one second and stop when I let it go.

I can't seem to figure out how that works. Can someone help me? Here is the code I am using:

#include <Bounce.h>

const int NUM_OF_BUTTONS = 4;

const int MIDI_CHAN = 16;

const int DEBOUNCE_TIME = 20;

Bounce buttons[NUM_OF_BUTTONS] =
{
  Bounce (0, DEBOUNCE_TIME),
  Bounce (1, DEBOUNCE_TIME),
  Bounce (2, DEBOUNCE_TIME),
  Bounce (3, DEBOUNCE_TIME)
};

const int MIDI_NOTE_NUMS[NUM_OF_BUTTONS] = {1, 2, 3, 4};
const int MIDI_NOTE_VELS[NUM_OF_BUTTONS] = {127, 127, 127, 127};

void setup()
{
  for (int i = 0; i < NUM_OF_BUTTONS + 1; i++)
  {
    pinMode (i, INPUT_PULLUP);
  }
}



void loop()
{
  for (int i = 0; i < NUM_OF_BUTTONS; i++)
  {
    buttons[i].update();
  }

  
  if (buttons[0].fallingEdge()) // GO!
  {
    usbMIDI.sendNoteOn (MIDI_NOTE_NUMS[1], MIDI_NOTE_VELS(127), MIDI_CHAN);
  }
  else if (buttons[0].risingEdge())
  {
    usbMIDI.sendNotOff (MIDI_NOTE_NUMS[1], 0, MIDI_CHAN);
  }


  if (buttons[1].fallingEdge()) // PANIC!
  {
    usbMIDI.sendNoteOn (MIDI_NOTE_NUMS[2], MIDI_NOTE_VELS(127), MIDI_CHAN);
  }  
  else if (buttons[1].risingEdge())
  {
    usbMIDI.sendNotOff (MIDI_NOTE_NUMS[2], 0, MIDI_CHAN);
  }




  if (buttons[2].fallingEdge()) // PREVIOUS
  {
    usbMIDI.sendNoteOn (MIDI_NOTE_NUMS[3], MIDI_NOTE_VELS(127), MIDI_CHAN);
  }  
  else if (buttons[2].risingEdge())
  {
    usbMIDI.sendNotOff (MIDI_NOTE_NUMS[3], 0, MIDI_CHAN);
  }




  if (buttons[3].fallingEdge()) // NEXT
  {
    usbMIDI.sendNoteOn (MIDI_NOTE_NUMS[4], MIDI_NOTE_VELS(127), MIDI_CHAN);
  }  
  else if (buttons[3].risingEdge())
  {
    usbMIDI.sendNotOff (MIDI_NOTE_NUMS[4], 0, MIDI_CHAN);
  }




  while (usbMIDI.read())
  {
    // ignoring incoming messages, so don't do anything here.
  }
  
}

It is pointless to use a for loop to iterate over the buttons, when each iteration of the for loop does something different. Ditch the for loop.

You are only concerned about the switches changing from not pressed to pressed or from pressed to not pressed.

You need to record when each switch becomes pressed, if you want to do something n milliseconds after the switch becomes pressed. You need to update that time when you perform the action, so you know when to perform the action again.

Thanks Paul, I found the code only and adapted it to my needs. I deleted the for loop.

Can you explain how I record when the switches become pressed? Do you maybe have an example for me? I think I need to use mills() for that right?

Can you explain how I record when the switches become pressed?

The state change detection examples shows how to determine when a switch changes state.

Calling millis() when the state changes isn't rocket science. Storing the value returned in the appropriate type of variable is simple.

Good to hear it's simple! Although I have to confess, even the button presses were a lot of work for me. This is the first project I'm making and I don't have much coding experience.

I cleaned the code up to make it a bit easier for me to see. Would you be willing to edit the code and make the last two buttons repeat the MIDI message after 1 second and repeat that message again after 300ms until the state of the button is changed again? Only if it's not to much trouble of course! I'd love to understand how this works and be able to use this in future projects!

#include <Bounce.h>

const int NUM_OF_BUTTONS = 4;
const int MIDI_CHAN = 16;
const int DEBOUNCE_TIME = 20;

Bounce buttons[NUM_OF_BUTTONS] =
{
  Bounce (0, DEBOUNCE_TIME),
  Bounce (1, DEBOUNCE_TIME),
  Bounce (2, DEBOUNCE_TIME),
  Bounce (3, DEBOUNCE_TIME)
};

void setup()
{
  pinMode (0, INPUT_PULLUP),
  pinMode (1, INPUT_PULLUP),
  pinMode (2, INPUT_PULLUP),
  pinMode (3, INPUT_PULLUP),  
}

void loop()
{
   
  if (buttons[0].fallingEdge()) // GO!
  {
    usbMIDI.sendNoteOn (MIDI_NOTE_NUMS[1], MIDI_NOTE_VELS(127), MIDI_CHAN);
  }
  else if (buttons[0].risingEdge())
  {
    usbMIDI.sendNotOff (MIDI_NOTE_NUMS[1], 0, MIDI_CHAN);
  }


  if (buttons[1].fallingEdge()) // PANIC!
  {
    usbMIDI.sendNoteOn (MIDI_NOTE_NUMS[2], MIDI_NOTE_VELS(127), MIDI_CHAN);
  }  
  else if (buttons[1].risingEdge())
  {
    usbMIDI.sendNotOff (MIDI_NOTE_NUMS[2], 0, MIDI_CHAN);
  }




  if (buttons[2].fallingEdge()) // PREVIOUS
  {
    usbMIDI.sendNoteOn (MIDI_NOTE_NUMS[3], MIDI_NOTE_VELS(127), MIDI_CHAN);
  }  
  else if (buttons[2].risingEdge())
  {
    usbMIDI.sendNotOff (MIDI_NOTE_NUMS[3], 0, MIDI_CHAN);
  }




  if (buttons[3].fallingEdge()) // NEXT
  {
    usbMIDI.sendNoteOn (MIDI_NOTE_NUMS[4], MIDI_NOTE_VELS(127), MIDI_CHAN);
  }  
  else if (buttons[3].risingEdge())
  {
    usbMIDI.sendNotOff (MIDI_NOTE_NUMS[4], 0, MIDI_CHAN);
  }




  while (usbMIDI.read())
  {
    // ignoring incoming messages, so don't do anything here.
  }
  
}

See if this comes close to what you want. Note: I had to use Bounce2, not Bounce so you may need to either update to that library or edit the code to support risingEdge() etc. Also, I don't have the MIDI library so I had to compile with that stuff commented out. Once it compiled I uncommented the midi stuff but clearly am not able to try it.

It uses a statemachine to process the buttons and their timing. At least that's the intent... Good luck :slight_smile:

#include <Bounce2.h>

//button state machine states
#define LOOK_FE             0
#define LOOK_RE_OR_HOLD     1
#define LOOK_HOLD           2

#define PREV                2
#define NEXT                3

//button timing scalars
#define BTN_HOLD_TIME       1000ul      //time button low for "hold repeat"
#define HOLD_RPT_PERIOD     333ul       //~3 times per second

const int NUM_OF_BUTTONS = 4;

const int MIDI_CHAN = 16;

const int DEBOUNCE_TIME = 20;
Bounce buttons[NUM_OF_BUTTONS] =
{
    Bounce(0, DEBOUNCE_TIME),
    Bounce(1, DEBOUNCE_TIME),
    Bounce(2, DEBOUNCE_TIME),
    Bounce(3, DEBOUNCE_TIME)
};

const int MIDI_NOTE_NUMS[NUM_OF_BUTTONS] = {1, 2, 3, 4};
const int MIDI_NOTE_VELS[NUM_OF_BUTTONS] = {127, 127, 127, 127};

void setup()
{
    for (int i = 0; i < NUM_OF_BUTTONS + 1; i++)
    {
        pinMode (i, INPUT_PULLUP);
    }//for
    
}//setup

void ButtonSM( void )
{
    static byte
        idx = 0;
    static byte
        stateB[NUM_OF_BUTTONS] = {0};
    static unsigned long
        timeB[NUM_OF_BUTTONS];
    unsigned long
        timeNow;

    switch( stateB[idx] )
    {
        case    LOOK_FE:
            //initially looking for a falling edge on all buttons
            buttons[idx].update();
            if( buttons[idx].fell() )
            {
                //falling edge seen; send MIDI msg
                usbMIDI.sendNoteOn (MIDI_NOTE_NUMS[idx+1], MIDI_NOTE_VELS(127), MIDI_CHAN);
                //grab the time and move to next state
                timeB[idx] = millis();
                stateB[idx] = LOOK_RE_OR_HOLD;
                
            }//if
        break;
            
        case    LOOK_RE_OR_HOLD:
            //looking for a rising edge (RE) or a sustained hold
            buttons[idx].update();
            //if rising edge, send msg and return to LOOK_FE state
            if( buttons[idx].rose() )
            {
                usbMIDI.sendNotOff (MIDI_NOTE_NUMS[idx+1], 0, MIDI_CHAN);
                stateB[idx] = LOOK_FE;
                
            }//if
            else
            {
                //rising edge not seen; if we're looking at NEXT or PREV buttons...                
                if( idx == NEXT || idx == PREV )
                {
                    ///...and the button is reading low and the button has been low for BTN_HOLD_TIME... 
                    if( ( millis() - timeB[idx] > BTN_HOLD_TIME ) && buttons[idx].read() == LOW )
                    {
                        //grab the time now to start the 3Hz timing and move to HOLD state
                        timeB[idx] = millis();
                        stateB[idx] = LOOK_HOLD;
                            
                    }//if
                    
                }//if
                
            }//if
            
        break;

        case    LOOK_HOLD:
            //button is being held...looking for a rising edge...
            //looking for a rising 
            buttons[idx].update();
            if( buttons[idx].rose() )
            {
                //got; send msg and move back to look for falling edge
                usbMIDI.sendNotOff (MIDI_NOTE_NUMS[idx+1], 0, MIDI_CHAN);
                stateB[idx] = LOOK_FE;
                
            }//if
            else
            {
                //button is still down...has 3Hz timer expired?
                if( ( millis() - timeB[idx] > HOLD_RPT_PERIOD ) && buttons[idx].read() == LOW )
                {
                    //yes send msg and reset timer
                    usbMIDI.sendNoteOn (MIDI_NOTE_NUMS[idx+1], MIDI_NOTE_VELS(127), MIDI_CHAN);
                    timeB[idx] = millis();
                            
                }//if
                   
            }//if
                
        break;
        
    }//switch

    //each pass thru we'll process a different switch. Bump the index (idx) for the next switch
    //on the next pass. Limit to 0-3. Conveniently we can do this in one line as 0x3 makes a handy mask
    //idx counts 0 1 2 3 0 1 2 3 etc
    idx = (idx + 1) & (NUM_OF_BUTTONS-1);

}//ButtonSM


void loop()
{
    ButtonSM();
    
    while( usbMIDI.read() )
    {
        // ignoring incoming messages, so don't do anything here.
    }//while
  
}//loop

Thanks! The MIDI notes changed (the NEXT knop now has note 66), but the rest works. Can you explain what you use this for?

    unsigned long
        timeNow;

You are making the job 4 times harder than it needs to be, by trying to deal with four buttons at once.

Dump all the code for 3 of the buttons.

You need to save the current state of a button (I HATE that name) object, and compare that to its state the last time you read the button. That is the only way to determine if the state changed.

You can NOT add functionality to deal with the continuing state of a switch to code that deals with the either it's become pressed or it hasn't nature of the switch.

Draw a flow chart of what you want to happen when the switch becomes pressed, when the switch remains pressed, when the switch becomes released, and when the switch remains released.

Then, you'll have a much easier time making the code work.

nvrie:
Thanks! The MIDI notes changed (the NEXT knop now has note 66), but the rest works. Can you explain what you use this for?

    unsigned long

timeNow;

Actually, I don't. Sometimes when starting code I have something in my head and put initializers for it but as the code evolves, I change my mind and do it differently. I try to clean up the unused declarations but missed that one.

Ah, it got me wondering :wink: I cannot quite put my finger on the fact that the midi notes are different. Go now is note 2, Stop is 3, prev is 4 and next is 66. I think it might have something to do with this line, but I'm not sure. Do you have an idea?

idx = (idx + 1) & (NUM_OF_BUTTONS-1);

Well, one thing that confused me in your original code was this: You declare:

const int MIDI_NOTE_NUMS[NUM_OF_BUTTONS] = {1, 2, 3, 4};

In your button checks you have this:

  if (buttons[0].fallingEdge()) // GO!
  {
    usbMIDI.sendNoteOn (MIDI_NOTE_NUMS[1], MIDI_NOTE_VELS(127), MIDI_CHAN);
  }
  else if (buttons[0].risingEdge())
  {
    usbMIDI.sendNotOff (MIDI_NOTE_NUMS[1], 0, MIDI_CHAN);

Note that you refer to MIDI_NOTE_NUMS[1] for switch '0'. I thought that odd, that you wouldn't use MIDI_NOTE_NUMS[0].

For the last button, '3':

if (buttons[3].fallingEdge()) // NEXT
  {
    usbMIDI.sendNoteOn (MIDI_NOTE_NUMS[4], MIDI_NOTE_VELS(127), MIDI_CHAN);
  }  
  else if (buttons[3].risingEdge())
  {
    usbMIDI.sendNotOff (MIDI_NOTE_NUMS[4], 0, MIDI_CHAN);
  }

You're actually looking outside the table to the 5th element (4). Any reason this isn't '3'?

I preserved this because I didn't dig deeply into what you were doing but this looks wrong.

Here's my code updated with "zero"-based indexing. See if this is better:

#include <Bounce2.h>

//button state machine states
#define LOOK_FE             0
#define LOOK_RE_OR_HOLD     1
#define LOOK_HOLD           2

#define PREV                2
#define NEXT                3

//button timing scalars
#define BTN_HOLD_TIME       1000ul      //time button low for "hold repeat"
#define HOLD_RPT_PERIOD     333ul       //~3 times per second

const int NUM_OF_BUTTONS = 4;

const int MIDI_CHAN = 16;

const int DEBOUNCE_TIME = 20;
Bounce buttons[NUM_OF_BUTTONS] =
{
    Bounce(0, DEBOUNCE_TIME),
    Bounce(1, DEBOUNCE_TIME),
    Bounce(2, DEBOUNCE_TIME),
    Bounce(3, DEBOUNCE_TIME)
};

const int MIDI_NOTE_NUMS[NUM_OF_BUTTONS] = {1, 2, 3, 4};
const int MIDI_NOTE_VELS[NUM_OF_BUTTONS] = {127, 127, 127, 127};

void setup()
{
    for (int i = 0; i < NUM_OF_BUTTONS + 1; i++)
    {
        pinMode (i, INPUT_PULLUP);
    }//for
    
}//setup

void ButtonSM( void )
{
    static byte
        idx = 0;
    static byte
        stateB[NUM_OF_BUTTONS] = {0};
    static unsigned long
        timeB[NUM_OF_BUTTONS];

    switch( stateB[idx] )
    {
        case    LOOK_FE:
            //initially looking for a falling edge on all buttons
            buttons[idx].update();
            if( buttons[idx].fell() )
            {
                //falling edge seen; send MIDI msg
                usbMIDI.sendNoteOn (MIDI_NOTE_NUMS[idx], MIDI_NOTE_VELS(127), MIDI_CHAN);
                //grab the time and move to next state
                timeB[idx] = millis();
                stateB[idx] = LOOK_RE_OR_HOLD;
                
            }//if
        break;
            
        case    LOOK_RE_OR_HOLD:
            //looking for a rising edge (RE) or a sustained hold
            buttons[idx].update();
            //if rising edge, send msg and return to LOOK_FE state
            if( buttons[idx].rose() )
            {
                usbMIDI.sendNotOff (MIDI_NOTE_NUMS[idx], 0, MIDI_CHAN);
                stateB[idx] = LOOK_FE;
                
            }//if
            else
            {
                //rising edge not seen; if we're looking at NEXT or PREV buttons...                
                if( idx == NEXT || idx == PREV )
                {
                    ///...and the button is reading low and the button has been low for BTN_HOLD_TIME... 
                    if( ( millis() - timeB[idx] > BTN_HOLD_TIME ) && buttons[idx].read() == LOW )
                    {
                        //grab the time now to start the 3Hz timing and move to HOLD state
                        timeB[idx] = millis();
                        stateB[idx] = LOOK_HOLD;
                            
                    }//if
                    
                }//if
                
            }//if
            
        break;

        case    LOOK_HOLD:
            //button is being held...looking for a rising edge...
            //looking for a rising 
            buttons[idx].update();
            if( buttons[idx].rose() )
            {
                //got; send msg and move back to look for falling edge
                usbMIDI.sendNotOff (MIDI_NOTE_NUMS[idx], 0, MIDI_CHAN);
                stateB[idx] = LOOK_FE;
                
            }//if
            else
            {
                //button is still down...has 3Hz timer expired?
                if( ( millis() - timeB[idx] > HOLD_RPT_PERIOD ) && buttons[idx].read() == LOW )
                {
                    //yes send msg and reset timer
                    usbMIDI.sendNoteOn (MIDI_NOTE_NUMS[idx], MIDI_NOTE_VELS(127), MIDI_CHAN);
                    timeB[idx] = millis();
                            
                }//if
                   
            }//if
                
        break;
        
    }//switch

    //each pass thru we'll process a different switch. Bump the index (idx) for the next switch
    //on the next pass. Limit to 0-3. Conveniently we can do this in one line as 0x3 makes a handy mask
    //idx counts 0 1 2 3 0 1 2 3 etc
    idx = (idx + 1) & (NUM_OF_BUTTONS-1);

}//ButtonSM


void loop()
{
    ButtonSM();
    
    while( usbMIDI.read() )
    {
        // ignoring incoming messages, so don't do anything here.
    }//while
  
}//loop

You're absolutely right. After fixing that and playing around with the time values (and editing out some of my typo's in the MIDI part of the code) it worked perfectly! Thanks, I learned a lot from it!