How to avoid using delay() when playing a MIDI beat?

Hi guys

I am trying to use a Bare Conductive Touch Board, which integrates Freescale MPR121 and VLSI VS1053B and is Arduino Leonardo compatible, to make a piano with a drum beat playing in the background.
I am running it in on board MIDI mode, similar to a SparkFun Music Instrument Shield.
Most of the code is from the tutorial here: http://www.bareconductive.com/make/on-board-midi-mode/
While the example codes works just fine, I want to have a drum beat playing in the background, so I’ve set up 2 MIDI channels, channel 0 for the piano and channel 1 for the drum.
This is the rough code I have currently and it works but sometimes doesn’t capture touch input:

#define bpm 120

#include <MPR121.h>
#include <Wire.h>
#include <SoftwareSerial.h>
#include <Metro.h>


Metro bpm_metro = Metro(60000/bpm);
byte note = 0; // The MIDI note value to be played
unsigned char currentIdx;


// key definitions
const byte notes[] = {60, 62, 64, 65, 67, 69, 71, 72, 74, 76, 77, 79, 81};


void setup(){ 
  // initialise MIDI
  setupMidi();

  currentIdx = 1;
}


void loop(){
 
   if (bpm_metro.check() == 1) { // check if the metro has passed it's interval .
      
       switch(currentIdx)
            {
                case 1:
                    //Notes on channel 1, some note value (note), 75% velocity (0x60):
                    noteOn(1, 35, 0x60);
                    noteOn(1, 42, 0x60);
                    noteOn(1, 44, 0x60);

                    delay(100);

                    //Turn off notes on channel 1:
                    noteOff(1, 35, 0x60);
                    noteOff(1, 42, 0x60);
                    noteOff(1, 44, 0x60);

                    currentIdx++;
                    break;  
        
                case 2:
                    noteOn(1, 42, 0x60);
                    
                    delay(100);

                    noteOff(1, 42, 0x60);

                    currentIdx++;
                    break;

                case 3:
                    noteOn(1, 42, 0x60);
                    noteOn(1, 47, 0x60);
                    
                    delay(100);

                    noteOff(1, 42, 0x60);
                    noteOff(1, 47, 0x60);

                    currentIdx++;
                    break;          
                
                case 4:
                    noteOn(1, 42, 0x60);
                    
                    delay(100);

                    noteOff(1, 42, 0x60);

                    currentIdx = 1;
                    break;

                default:
                    break;
            }   
   }

   if(MPR121.touchStatusChanged()){
     
     MPR121.updateAll();
     
     // code below sets up the board essentially like a piano, with an octave
     // mapped to the 12 electrodes, each a semitone up from the previous
     for(int i=firstPin; i<=lastPin; i++){
       
       note = notes[i];
       if(MPR121.isNewTouch(i)){
         
          //Note on channel 0, some note value (note), 75% velocity (0x60):
          noteOn(0, note, 0x60);

       }
       
       else if(MPR121.isNewRelease(i)) {  
 
         // Turn off the note on channel 0 with a given off/release velocity
         noteOff(0, note, 0x60);  
                
        }
     }
   }
}

I have used the Metro library to repeat a drum loop according to the tempo.
My question is: inside this timed sequence, how can I avoid using delay() between noteOn() and noteOff()? The delay() function as it is used now is effectively the duration of the note, because noteOn() and noteOff() don’t have a duration argument.
I don’t know if this helps, but the VS1053 is polyphonic.
Perhaps the solution is obvious but please forgive me as I’m quite new to Arduino, and in fact, programming in general.

More ambitiously, I’m also wondering if it’s possible to make a hardware version of Plink by Dinahmoe Labs: www.dinahmoelabs.com/plink, specifically the beat matching feature on touch input? Currently, a single press and hold will only produce one sound, but can it constantly repeat as long as the touch surface is pressed? After a touch surface is pressed, can the time when the sound is produced be delayed to match the tempo?

Many thanks!

how can I avoid using delay() between noteOn() and noteOff()?

The secret is revealed in this thread Several things at the same time

UKHeliBob:
The secret is revealed in this thread Several things at the same time

Hi, thanks for your reply, but I'm not sure how to implement it.
I've already used the Metro library to repeat a drum loop according to a set interval, but noteOn() and noteOff() are inside this sequence.
I might be going in a totally wrong direction here, but I think I need something like a one-time-use "disposable" timer?
In the code I've posted above, there are 4 switch cases making a simple drum loop.
What if I wanted to make it more complicated by, in one of the cases, playing 3 notes 50 milliseconds after each other lasting for 100 milliseconds, and in another case, 2 notes together lasting for 150 milliseconds?

rlrh1996:
Hi, thanks for your reply, but I'm not sure how to implement it.

In Several Things at a Time there are three LEDs flashing. Imagine that they are the timing for your drum beats. Does that help?

Once delay() is no longer appropriate (i.e. in almost every serious program) you need to deal with each interval (how ever many you may need) as a series of "is it time yet?" questions that are asked each time loop() repeats. And the code should be designed to allow loop() to repeat as often as possible.

...R

Robin2:
In Several Things at a Time there are three LEDs flashing. Imagine that they are the timing for your drum beats. Does that help?

Once delay() is no longer appropriate (i.e. in almost every serious program) you need to deal with each interval (how ever many you may need) as a series of "is it time yet?" questions that are asked each time loop() repeats. And the code should be designed to allow loop() to repeat as often as possible.

...R

Thanks a lot, I tried implementing it as per below, but it sounds a bit off sometimes compared to using delay(). Am I implementing it correctly?

unsigned long currentMillis = 0;    // stores the value of millis() in each iteration of loop()
unsigned long previousMillis1 = 0;
unsigned long previousMillis2 = 0;

const int noteDuration = 100;

void loop(){

   currentMillis = millis(); 

   if (bpm_metro.check() == 1) { // check if the metro has passed it's interval .
      
       switch(currentIdx)
            {
                case 1:
                    //Notes on channel 1, some note value (note), 75% velocity (0x60):
                    noteOn(1, 35, 0x60);
                    noteOn(1, 42, 0x60);

                    if (currentMillis - previousMillis1 >= noteDuration) {
                       noteOff(1, 35, 0x60);
                       noteOff(1, 42, 0x60);
                       previousMillis1 += noteDuration;
                    }
                    
                    currentIdx++;
                    break;  
        
                case 2:
                    noteOn(1, 42, 0x60);
                    
                    if (currentMillis - previousMillis2 >= noteDuration) {
                       noteOff(1, 42, 0x60);
                       previousMillis2 += noteDuration;
                    }

                    currentIdx++;
                    break;
}

The problem is that I know NOTHING about music.

I notice that you are using the same noteDuration in both cases - is that intentional.

I wonder if the whole thing needs to be a tad more complex. I suspect your CASE should just have

                case 2:
                    noteOn(1, 42, 0x60);
                    prevMillis2 = currentMillis;
                    break;

and outside all the CASE statments you should have this (for all the notes)

                    if (currentMillis - previousMillis2 >= noteDuration) {
                       noteOff(1, 42, 0x60);
                       previousMillis2 += noteDuration;
                    }

That would start the clock when the CASE was triggered and would turn off the note after the duration regardless of whether the CASE was triggered again.

...R

rlrh1996:
Thanks a lot, I tried implementing it as per below, but it sounds a bit off sometimes compared to using delay(). Am I implementing it correctly?

Please include your whole sketch. It is very difficult, and sometimes impossible to investigate the issue without the whole sketch. Code snippets often do not have the line that is causing the problem, just the line that the person who cannot get it working “thinks” is causing the problem.

rlrh1996:
Hi, thanks for your reply, but I’m not sure how to implement it.
I’ve already used the Metro library to repeat a drum loop according to a set interval, but noteOn() and noteOff() are inside this sequence.
I might be going in a totally wrong direction here, but I think I need something like a one-time-use “disposable” timer?
In the code I’ve posted above, there are 4 switch cases making a simple drum loop.
What if I wanted to make it more complicated by, in one of the cases, playing 3 notes 50 milliseconds after each other lasting for 100 milliseconds, and in another case, 2 notes together lasting for 150 milliseconds?

The structure you currently have is probably not what you want for the more complex simultaneous notes.
You need to work out how to define your pattern in a data structure. Once you have worked that out, turning that into notes becomes a lot easier.

Something like this might work for you:

class Note
{
  public:
    int channel;
    int start_beat;
    int pitch;
    float duration_in_beats;

    Note(int c, int sb, int p, float d)
    {
        channel = c;
        start_beat = sb;
        pitch = p;
        duration_in_beats = d;
    }
};

Note beats[] = 
{
    Note(1, 1, 45, 0.9),
    Note(1, 1, 49, 0.9),
    Note(1, 2, 55, 1.9),
    Note(1, 2, 59, 1.9),
    Note(1, 2, 63, 1.9),
};

int number_of_notes = sizeof(beats) / sizeof(Note);

setup()
{

   // just show the data
   for (int i=0; i < number_of_notes; i++)
   {
       cout << beats[i].channel << ",";
       cout << beats[i].pitch << ",";
       cout << beats[i].duration_in_beats << endl;
   }
}

loop()
{
     // now play the notes using millis() for timing
     
}

This data structure is based on beats, not milliseconds. That allows you to change from say 100bpm to 120bpm without changing the data.

Robin2:
The problem is that I know NOTHING about music.

I notice that you are using the same noteDuration in both cases - is that intentional.

I wonder if the whole thing needs to be a tad more complex. I suspect your CASE should just have

                case 2:

noteOn(1, 42, 0x60);
                    prevMillis2 = currentMillis;
                    break;




and outside all the CASE statments you should have this (for all the notes)


if (currentMillis - previousMillis2 >= noteDuration) {
                      noteOff(1, 42, 0x60);
                      previousMillis2 += noteDuration;
                    }




That would start the clock when the CASE was triggered and would turn off the note after the duration regardless of whether the CASE was triggered again.

...R

Happy new year and thanks a lot! :slight_smile:

I got it working and synced with the below code based on your structure.
Here I'm trying to recreate the drum beat in "Tell Her You Love Her" by Echosmith, so it's bass, bass again, bass and 4 sticks, then bass and hi-hat.
Problem is, I can't get my head around managing all the different time intervals, so ended up having to use delay() for the 4 sticks (in case 3), and unfortunately the touch input doesn't work during that interval.
I tried to put the same switch structure inside case 3, but couldn't get it to sync up.

unsigned char currentIdx;

unsigned long currentMillis = 0;    // stores the value of millis() in each iteration of loop()
unsigned long prevMillis1 = 0;
unsigned long prevMillis2 = 0;
unsigned long prevMillis3 = 0;
unsigned long prevMillis4 = 0;

const int noteDuration = 100;

void loop(){

   currentMillis = millis(); 
 
   if (bpm_metro.check() == 1) { // check if the metro has passed it's interval .
      switch(currentIdx)
         {
                case 1:
                    noteOn(1, 35, 0x60); // bass
                    prevMillis1 = currentMillis;
                    currentIdx++;
                    break;          
                case 2:
                    noteOn(1, 35, 0x60); // bass
                    prevMillis2 = currentMillis;
                    currentIdx++;
                    break;
                case 3:
                    noteOn(1, 35, 0x60); // on bass
                    noteOn(2, 31, 0x60); // on sticks1
                    noteOn(1, 37, 0x60); // on sticks2
                    delay(100);
                    noteOff(2, 31, 0x60); // off sticks1
                    noteOff(1, 37, 0x60); // off sticks2
                    noteOn(2, 31, 0x60);
                    delay(100);
                    noteOff(2, 31, 0x60);
                    noteOn(2, 31, 0x60);
                    noteOn(1, 37, 0x60);
                    delay(100);
                    noteOff(2, 31, 0x60);
                    noteOff(1, 37, 0x60);
                    noteOn(2, 31, 0x60);
                    delay(100);
                    noteOff(2, 31, 0x60);
                    noteOff(1, 35, 0x60); // off bass
                    prevMillis3 = currentMillis;
                    currentIdx++;
                    break;  
                case 4:
                    noteOn(1, 35, 0x60); // bass
                    noteOn(1, 46, 0x60); // hi-hat
                    prevMillis4 = currentMillis;
                    currentIdx = 1;
                    break;
                default:
                    break;
            }
   }

   if (currentMillis - prevMillis1 >= noteDuration) {
                       noteOff(1, 35, 0x60);
                       prevMillis1 += noteDuration;
        }
   if (currentMillis - prevMillis2 >= noteDuration) {
                       noteOff(1, 35, 0x60);
                       prevMillis2 += noteDuration;
        }
   if (currentMillis - prevMillis4 >= noteDuration) {
                       noteOff(1, 35, 0x60);
                       noteOff(1, 46, 0x60);
                       prevMillis4 += noteDuration;
        }
}

Also, I’m trying to get the sounds produced upon touching any of the 12 piano keys to match up with the tempo.

I’m using the Metro library to keep track of the bpm timing. The tempo for the keys is currently set at twice the bpm (half the interval) for testing purposes.

I got a crude version working by storing the current note played and playing it continuously according to the tempo as long as the same key is pressed and held. However, if 2 or more keys are pressed at the same time or if one key is introduced after another, it fails to work.

Is there a solution to this problem, perhaps using an array to store each concurrently played note, or just rearranging some of the code into the correct location inside the loop?

This code is below the above code inside void loop():

if(MPR121.touchStatusChanged()){
     
     MPR121.updateAll();
     
     // code below sets up the board essentially like a piano, with an octave
     // mapped to the 12 electrodes, each a semitone up from the previous
     for(int i=firstPin; i<=lastPin; i++){
       note = whiteNotes[lastPin-i];
       if(MPR121.isNewTouch(i)){
          currentNote = note;
          if (tempo_metro.check() == 1) {
             noteOn(0, note, 0x60);
          }       
       }
       else if(MPR121.isNewRelease(i)) {   
         currentNote = 0;
         noteOff(0, note, 0x60);      
        }
     }
}
else {
   if (tempo_metro.check() == 1) {
      noteOn(0, currentNote, 0x60);
      prevMillis5 = currentMillis;
      if (currentMillis - prevMillis5 >= noteDuration) {
                       noteOff(0, currentNote, 0x60);
                       prevMillis5 += noteDuration;
      }
   }
}

This uses the MPR121 library by Bare Conductive.

rlrh1996:
Problem is, I can't get my head around managing all the different time intervals,

Sorry, but I am going to drop out of this because the music stuff is just so far beyond me.

...R

Robin2:
Sorry, but I am going to drop out of this because the music stuff is just so far beyond me.

...R

Thanks a lot for all your help though!