Please help a noob figure out his DIY MIDI project

Hello, I am new to this forum and generally to Arduino and C++ altogether. I took up an electronic drum project thinking that C++ would be easy to figure out given that I know some Python, but that is not the case.

This project uses a piezo sensor that picks up drum strikes, with the intention of coding a MIDI controller to send note and velocity values to a MIDI library on the PC (using Hairless MIDI and loopMIDI to route into Reaper for sampling).

I tried using other code from elsewhere and while it worked, there was a noticeable delay between a drum strike and the note fire that is not ideal in practice. There is a lot of signal processing going on in the code, and the note being fired is calculated as the maximum of signals stored in an array of specified size.

I made my own coding scheme in Python that gets me what I need and is short and simple. I took some ideas from the original code and gutted what I thought wasn't necessary, but I clearly missed something important.

Here is the code, pretending that only the snare is to be used (in reality there are up to 10 slots):

#define NUM_PIEZOS 1        //active piezo pins
#define START_SLOT 0        //first analog slot of piezos
#define BAUD_RATE 115200    //MIDI baud rate

//This should work for all triggers, and if not, change resistor used for that trigger
#define THRESHOLD 10

//MIDI note definitions for each trigger
#define SNARE_NOTE 38

//MIDI definitions
#define NOTE_ON_CMD 0x90
#define NOTE_OFF_CMD 0x80
#define MAX_MIDI_VELOCITY 127

//Minimum masking time
//TIME MEASURED IN MILLISECONDS
#define MIN_MASK 8

//float that scales max hit value for dynamic mask time between notes
//will be used as follows: 
//MASKTIME = MIN_MASK + (analogRead - THRESHOLD) * SIGNAL_SCALER
float SIGNAL_SCALER = 1.07 ;

//array of piezo names
unsigned short slotMap[NUM_PIEZOS];

//array of MIDI notes
unsigned short noteMap[NUM_PIEZOS];

//Ring buffers to store analog signal
unsigned short signalBuffer[NUM_PIEZOS][2];

boolean noteSuppress[NUM_PIEZOS];
unsigned long reinitNoteTime[NUM_PIEZOS];

void setup()
{
  Serial.begin(BAUD_RATE);

  //initialize globals
  for(short i=0; i<NUM_PIEZOS; ++i)
  {
    memset(signalBuffer[i],0,sizeof(signalBuffer[i]));
    signalBuffer[i][0] = 0;
    noteSuppress[i] = false;
    reinitNoteTime[i] = 0;
    slotMap[i] = START_SLOT + i;
  }
  
  noteMap[0] = SNARE_NOTE;
}

void loop()
{
  unsigned long currTime = millis();
  
  for(short i=0; i<NUM_PIEZOS; ++i)
  {
    unsigned short currSignal = analogRead(slotMap[i]);
    signalBuffer[i][1] = currSignal;
    unsigned short prevSignal = signalBuffer[i][0];

    //Serial.write(currSignal);
    
    //reinitialize
    if(currTime > reinitNoteTime[i]) noteSuppress[i] = false;

    if(prevSignal >= THRESHOLD){

      if( 
        prevSignal - currSignal > 1 &&
        noteSuppress[i] == false
      ) {
          noteFire(noteMap[i], signalBuffer[i][0]);
          noteSuppress[i] = true;
          
          //calculate time to reinitialization with dynamic masking time based on velocity
          reinitNoteTime[i] = currTime + MIN_MASK + (prevSignal - THRESHOLD) * SIGNAL_SCALER;
      }
    }
    signalBuffer[i][0] = currSignal;
  }
}

void noteFire(unsigned short note, unsigned short velocity)
{
  if(velocity > MAX_MIDI_VELOCITY)
    velocity = MAX_MIDI_VELOCITY;
  
  midiNoteOn(note, velocity);
  midiNoteOff(note, velocity);
}

void midiNoteOn(byte note, byte midiVelocity)
{
  Serial.write(NOTE_ON_CMD);
  Serial.write(note);
  Serial.write(midiVelocity);
}

void midiNoteOff(byte note, byte midiVelocity)
{
  Serial.write(NOTE_OFF_CMD);
  Serial.write(note);
  Serial.write(midiVelocity);
}

The code is supposed to do the following:

  1. Read analog value from the piezo and store it in signalBuffer[1], where signalBuffer[0] is the previous signal (initialized to 0 in setup()).
  2. Disable note suppression if the current time is outside the suppression window.
  3. If the previous signal is greater than the current signal, and note suppression is not on (initialized to false), and the previous signal is above the threshold, then:
    a. Fire the note.
    b. Enable note suppression.
    c. Calculate timestamp for when note suppression will be disabled.
  4. Assign the current signal as the previous signal.

As is, the code does not work. If I allow the read signal to be output during the loop, it seems that after a single drum strike the code stops executing. I have no idea why.

Any help is appreciated. And any coding tips or know-hows are welcome, I would like to continue with Arduino after this project is finished. Thanks!

The while loop

is problematic. Since prevSignal isn't being updated inside the while loop so this can cause an infinite loop if prevSignal is greater than or equal to THRESHOLD.

If this statement is true, it will remain true forever since you never change either variable within the while() loop.

I'm not sure while you have a while() loop. Let loop() do what it was born to do... LOOP.

also it seems that you’re using the difference between the previous and current signal to detect a peak. Shouldn't the previous signal be the maximum detected signal from the strike, not just the immediately previous one ?

also it seems that you’re using the difference between the previous and current signal to detect a peak. Shouldn't the previous signal be the maximum detected signal from the strike, not just the immediately previous one ?

Good question. I did plenty of testing so that this scheme does detect the maximum. In nearly every case the max was found with this, but even if there was a rogue max, it was close enough. It's not 100% accurate but maybe 98%.

Here is a plot of what I was working with, just to show that it does a good job of detecting the max:

Untitled

This is what happens with nearly every hit.

is problematic. Since prevSignal isn't being updated inside the while loop so this can cause an infinite loop if prevSignal is greater than or equal to THRESHOLD.

If this statement is true, it will remain true forever since you never change either variable within the while() loop.

Thanks. I originally had if instead of while but I don't think that worked either.

i love this

Ok, please help me understand... so even if I change the while to if, will it still not execute? I assume that even if the condition is false and the whole block doesn't execute, it will still run to the end of the loop, reassign the signal to the first array value, and start the loop again with that assigned signal value, which will be compared to the next iteration's new signal.

Is there something I'm not getting here? Is that not what happens in void loop()?

could you summarise (without code, in english) what are the exact requirements ?

Sure. The goal is to compare signals, one being current and the other being the previous signal, and to fire a note if the current signal is less than the previous signal. After that the program should ignore readings for a short time before another can be read, as the piezo vibrates quite a but after a strike. Between the threshold and the suppression time, it should be a fairly accurate program.

In terms of the variables in the code:

  1. The loop should read the sensor with every pass and get a new value, and assign that value to signalBuffer[i][1].

  2. Then it will check to see if the note suppression window has ended, and if it has, it will set noteSuppress to false. This is initialized to false.

  3. Then the program will check to see if the previous signal, signalBuffer[i][0] (initialized to 0), is above THRESHOLD.

  4. If so, it will then compare signalBuffer[i][1], the newly assigned piezo signal, to signalBuffer[i][0]. In particular it will check to see if the previous signal is greater than the new signal, and it will also check if noteSuppress is true.

  5. If 3 and 4 are met, the program will fire a note through noteFire. It will also set noteSuppress[i] to true, and will calculate the time that noteSuppress will remain true with reinitNoteTime[i]. Once this timestamp is surpassed, noteSuppress[i] will be set to false (see above).

  6. At the end of the loop, whether 3 and 4 are met or not, the program should move the new signal in this iteration, signalBuffer[i][1], to signalBuffer[i][0]; in the next iteration, this must be the value that a newly-assigned piezo signal (see 1) is compared to.

Thank you for your help.

All, thanks for your help. I figured it out - it turns out that declaring NUM_PIEZOS = 1 while assigning 10 notes in setup() was causing the program to assign the final note value, or noteMap[10], to the boolean noteSuppress[i]. I don't know why that is, but after commenting out all but noteMap[0] in setup(), it works (mostly) as intended.

you might benefit from studying state machines. Here is a small introduction to the topic: Yet another Finite State Machine introduction

If I get what you describe correctly, the state machine is pretty simple and could look like this

You start in the IDLE state and listen to the Signal and when the event "Signal > THRESHOLD" becomes true, you record the signal's value that triggered the state change and go to the STRIKE_DETECTED state

In the STRIKE_DETECTED state you keep listening to the Signal and compare it with the previous value (and update it) until the Signal starts to drop in value (assuming it just peaked), at that point you record the time, fire the note and go to a INACTIVE state

in the INACTIVE state you just wait for a calculated timeout and go back to IDLE.

Then you could define an object representing ONE sensor which would encapsulate all the required information.

this could look like this

//Minimum masking time
//TIME MEASURED IN MILLISECONDS
const uint32_t MIN_MASK = 8;

//float that scales max hit value for dynamic mask time between notes
//will be used as follows:
//MASKTIME = MIN_MASK + (analogRead - THRESHOLD) * SIGNAL_SCALER
const double SIGNAL_SCALER = 1.07 ;

//MIDI definitions
const byte NOTE_ON_CMD = 0x90;
const byte NOTE_OFF_CMD = 0x80;
const byte MAX_MIDI_VELOCITY = 127;

class Sensor {
    enum SensorState {IDLE, STRIKE_DETECTED, INACTIVE} ;

  private:
    const char * sensorName;
    byte analogPin;
    byte note;
    uint16_t threshold;
    uint16_t previousSignal;
    SensorState state;
    uint32_t chrono;
    uint32_t duration;

  public:
    // Constructor
    Sensor(const char * name, byte pin, byte n, uint16_t t)
      : sensorName(name), analogPin(pin), note(n), threshold(t), previousSignal(0), state(IDLE)
    {}

    // return true if note was triggered
    bool poll() {
      bool noteSent = false;

      switch (state) {
        case IDLE: {
            analogRead(analogPin);                    // trow away one value for analog stability
            uint16_t signal = analogRead(analogPin);
            if (signal >= threshold) {
              previousSignal = signal;
              state = STRIKE_DETECTED;
            }
          }
          break;

        case STRIKE_DETECTED:
          {
            analogRead(analogPin);                    // trow away one value for analog stability
            uint16_t signal = analogRead(analogPin);
            if (signal < previousSignal) { // Signal starts decreasing
              noteFire(previousSignal); // is it OK to use the signal level as a velocity ?
              noteSent = true;
              chrono = millis();
              duration = MIN_MASK + (signal - threshold) * SIGNAL_SCALER;
              state = INACTIVE;
            } else {
              previousSignal = signal;
            }
          }
          break;

        case INACTIVE:
          if (millis() - chrono >= duration) state = IDLE;
          break;
      }

      return noteSent;
    }

    void midiNoteOn(byte note, byte midiVelocity)
    {
      Serial.write(NOTE_ON_CMD);
      Serial.write(note);
      Serial.write(midiVelocity);
    }

    void midiNoteOff(byte note, byte midiVelocity)
    {
      Serial.write(NOTE_OFF_CMD);
      Serial.write(note);
      Serial.write(midiVelocity);
    }

    void noteFire(uint16_t velocity) {
      if (velocity > MAX_MIDI_VELOCITY) velocity = MAX_MIDI_VELOCITY;
      midiNoteOn(note, velocity);
      midiNoteOff(note, velocity);
    }
};

// ---------------- THE MAIN CODE ---------------

//MIDI baud rate
#define BAUD_RATE 115200

//This should work for all triggers, and if not, change resistor used for that trigger
#define THRESHOLD 10

//MIDI note definitions for each trigger
const byte SNARE_NOTE = 38;
const byte BASS_NOTE = 36;
const byte ACOUSTIC_SNARE_NOTE = 38;
const byte CLOSED_HI_HAT_NOTE = 42;
const byte OPEN_HI_HAT_NOTE = 46;
const byte CRASH_CYMBAL_NOTE = 49; // Corrected to 49 for Crash Cymbal 1

Sensor sensors[] = {
  {"Snare",           A0, SNARE_NOTE, THRESHOLD},
  {"Bass",            A1, BASS_NOTE, THRESHOLD},
  {"Acoustic",        A2, ACOUSTIC_SNARE_NOTE, THRESHOLD},
  {"Closed Hi-Hat",   A3, CLOSED_HI_HAT_NOTE, THRESHOLD},
  {"Open Hi-Hat",     A4, OPEN_HI_HAT_NOTE, THRESHOLD},
  {"Crash Cymbal",    A5, CRASH_CYMBAL_NOTE, THRESHOLD}
};

void setup() {
  Serial.begin(BAUD_RATE);
}

void loop() {
  for (auto & s : sensors) s.poll();
}

or something like this (fully untested, typed here)

the advantage with the class is you do the work once and the main code becomes super simple:

you define the sensors

Sensor sensors[] = {
  {"Snare",           A0, SNARE_NOTE, THRESHOLD},
  {"Bass",            A1, BASS_NOTE, THRESHOLD},
  {"Acoustic",        A2, ACOUSTIC_SNARE_NOTE, THRESHOLD},
  {"Closed Hi-Hat",   A3, CLOSED_HI_HAT_NOTE, THRESHOLD},
  {"Open Hi-Hat",     A4, OPEN_HI_HAT_NOTE, THRESHOLD},
  {"Crash Cymbal",    A5, CRASH_CYMBAL_NOTE, THRESHOLD}
};

and the code just polls them in turn. that's it :slight_smile:

void setup() {
  Serial.begin(BAUD_RATE);
}

void loop() {
  for (auto & s : sensors) s.poll();
}

(the name of the sensors is not really needed, it might be useful for debugging or whatever if you want to print which sensor got triggered. Not needed in the final version)

Hey, been a while - thank you so much for your explanation and code. I haven't been able to mess with this lately but have started tinkering again. I love the code you provided - maybe mine was far too rudimentary to work well (which it doesn't so far; seems to read sensors inaccurately but could be the way they are made and set).

I think I will use what you provided, and let you know if I need help understanding things. This level of formality in coding is admittedly unfamiliar to me, especially in C++.

Merry Christmas!

edit: As for your question, is it ok to use the signal for the note value - I don't know for certain. I've seen it programmed as such elsewhere, and it does seem to be accurate in regards to how hard I hit.

A bit early but Merry Christmas too !