Categories
Arduino electronics MIDI software

DIY MIDI Percussion Kit – Part 2

In the first part we built up some percussion sensors using piezo electric elements that can detect a strike and provide feedback on the strength of the blow.

In this part we define a software framework that turns these, and any other similar sensors, into a DIY percussion kit.

Test software

Once the hardware was made and tested standalone, the next step was to write some code to understand what the signal profile looked like when processed by the software.

The small sketch below is designed to collect MAX_VALUES of analog data and then dump this to the serial monitor. Hopefully we should observe that the profile is somewhat like the final oscilloscope in the first article.

const uint8_t PIN = A0;
const uint16_t THRESHOLD = 3;
const uint16_t MAX_VALUES = 750;

uint16_t data[MAX_VALUES];    // stored values

void showValues(void)
{
  for (uint16_t i=0; i<MAX_VALUES; i++)
  {
    Serial.print(i);
    Serial.print('t');
    Serial.println(data[i]);
  }
}

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

void loop(void) 
{
  static enum {IDLE, COLLECT, SHOW } state = IDLE;
  static uint16_t idx = 0;

  switch(state)
  {
    case IDLE:  // waiting for threshold value
      {
        uint16_t v = analogRead(PIN);

        if (v >= THRESHOLD)
        {
          idx = 0;          // reset to start of data array
          data[idx++] = v;  // save this as the first value 
          state = COLLECT;
        }
      }
      break;

    case COLLECT: // collecting values
      if (idx >= MAX_VALUES)
        state = SHOW;   // all done
      else
        data[idx++] = analogRead(PIN);    // save next value
      break;

    case SHOW: // show the data
      showValues();
      state = IDLE;
      break;
  }
}

Three sets of data were collected, copied from the Serial monitor window, pasted into Microsoft Excel and charted (shown below). The signal profiles collected for all 3 trials were similar to previously seen oscilloscope trace (a good thing!). Also, looking at the data it would seem that maximum value for the first peak is a good measure striking force, which means we can ignore the rest of the data following.

Building the percussion kit

The next thing was to think about the design of the software and what features would make sense.

Foremost is a flexible, data driven, approach. The percussion instruments and their parameters should be defined in a data table, with the software adjusting how it works based on this data. Practically, this means no code changes are needed for a different combination of instruments.

As discussed earlier, the input methods to play these instruments can be a simple on/off switch (digital) interface or an analog piezo sensor for more expression (through the MIDI velocity parameter). The software should allow for both types of instruments.

On top of the inferred MIDI velocity from the analog signal, it makes sense to be able to specify the velocity either through a literal number or from another analog input. The latter enables using a potentiometer to set this value dynamically. Similarly, for analog instruments, the data needs to include a minimum trigger value (threshold) so that random signal noise does not accidently play the instrument.

The instruments also need to have exclusion timing parameters to allow for debouncing the signal and prevent it to trigger on the secondary and subsequent input signal peaks.

The complete sketch for this project can be downloaded from my code repository.

Data table definition

Once all this has been taken into consideration, and including some housekeeping values, the data required for the instrument table is defined in the instrument_t data structure, shown below.

typedef struct
{
  // Static data defining the instrument.
  uint8_t   kmValue;     // GM Percussion Key Map Value (eg, BASS_DRUM_1)
  instr_t   type;        // instrument Type (INSTR_ANALOG, INSTR_DIGITAL)
  uint8_t   pin;         // hardware pin number for the input, digital or analog
  uint16_t  activeTime;  // time between the NOTE_ON and NOTE_OFF messages in ms
  uint16_t  excludeTime; // time to exclude next activation for this instrument
  uint8_t   velocity;    // MIDI velocity setting. Setting required for INSTR_DIGITAL.
                         // Use 0 to infer velocity for analog instrument (INSTR_ANALOG) reading.
                         // Can be an analog pin or value. Use VAL() and PIN() macros to set the value.
  uint8_t   sensTrig;    // Sensor trigger threshold. Only valid for INSTR_ANALOG, ignored for INSTR_DIGITAL.
                         // Can be an analog pin or value. Use VAL() and PIN() macros to set the value.

  // Dynamic data created and used during run time.
  // This data does need to be statically initialized in the instrument table.
  bool      noteOn;      // true if NOTE_ON is current
  uint32_t  lastOnTime;  // last time the NOTE_ON was sent - saved millis() value
  uint16_t  lastValue;   // last value read for this instrument
  state_t   state;       // state for the FSM
} instrument_t;

The data table that can be created to control how the software works then looks like this

// Global Data
instrument_t PT[] =  // Percussion Table
{
  // kmValue        Type          Pin Activ Excl      Vel    Sens
  { CRASH_CYMBAL_1, INSTR_DIGITAL,  7,    0,  50,  PIN(A4),  VAL(0) },
  { COWBELL,        INSTR_DIGITAL,  6,    0,  50, VAL(127),  VAL(0) },
  { LO_BONGO,       INSTR_DIGITAL,  4,    0,   0, VAL(127),  VAL(0) },
  { HI_MID_TOM,     INSTR_ANALOG,  A0,  500,  50,   VAL(0), PIN(A5) },
};

PIN() and VAL() are macros that allow a pin number or a value to be packed into the same variable so that they can be recognised for what they are when they are later unpacked.

The main control loop processes table entry in a round-robin fashion so that each instrument can be played simultaneously. How the instrument data is processed depends on the type of instrument interface.

Digital instruments interface

Digital instruments only provide an ‘on’ signal.

The code to handle a digital instrument is a Finite State Machine with three states – IDLE, PLAY and EXCLUDE.

  • The IDLE state waits for the first change of the digital signal from HIGH to LOW. Once this is detected the state is changed to PLAY.
  • The PLAY state plays the instrument, does some housekeeping, and then sets the next state to EXCLUDE.
  • The EXCLUDE state does a non-blocking wait for the exclusion time before resetting the state to IDLE which allows the cycle to repeat.
void handleDigital(instrument_t* p)
// Handle an instrument that has been designated as digital type.
// The value is either on or off and we need to detect a transition. 
{
  uint8_t value = digitalRead(p->pin);

  switch (p->state)
  {
  case START:
    // check for a HIGH to LOW transition
    if (value != p->lastValue)
    {
      if ((value == LOW) && (p->lastValue == HIGH))
        p->state = PLAY;
      p->lastValue = value;
    }
    break;

  case PLAY:
    if (p->noteOn) midi.noteOff(p->kmValue);  // switch it off if currently on
    midi.noteOn(p->kmValue, deRef(p->velocity));  // now turn the note on
    p->noteOn = (p->activeTime != 0);         // don't record if timer is zero
    p->lastOnTime = millis();
    p->state = EXCLUDE;
    break;

  case EXCLUDE: // ignore signals during the exclusion time
    if (millis() - p->lastOnTime >= p->excludeTime)
      p->state = START;
    break;
  }

Analog instrument interface

Analog instruments can provide data on the strength of the percussive hit. The ‘on’ is inferred by the signal passing the configured threshold.

The code to handle an analog instrument is a slightly more complex than for digital instruments and is Finite State Machine with four states – IDLE, MEASURE, PLAY and EXCLUDE.

  • The IDLE state waits for the analog signal to exceed the threshold value. Once this is detected the state is changed to either
    • PLAY if the velocity has been specified either explicitly or as an analog input, or
    • MEASURE if the velocity was specified as zero.
  • The MEASURE state measures the instrument analog input until it has determined the first maximum value. Once this is established, the state changes to PLAY.
  • The PLAY state plays the instrument, does some housekeeping, and then sets the next state to EXCLUDE.
  • The EXCLUDE state does a non-blocking wait for the exclusion time before resetting the state to IDLE, which allows the cycle to repeat.
void handleAnalog(instrument_t *p)
// Handle an instrument that has been designated as an analog type.
// The value is first detected when it exceeds the threshold, the max 
// reading can then be used as the MIDI velocity, and finally the debounce 
// time is counted out before resetting for next hit detection.
{
  uint16_t v = analogRead(p->pin);

  switch(p->state)
  {
  case START: // waiting for an activation
    if (v > deRef(p->sensTrig))
    {
      uint16_t vel = deRef(p->velocity);

      // if the velocity is 0, then we need to measure the height of the signal
      // to infer the velocity. Otherwise, use the value specified in the table.
      p->lastValue = (vel == 0 ? v : vel);
      p->state = (vel == 0 ? MEASURE : PLAY);    // next state
    }
  break;
      
  case MEASURE: // capturing the maximum value of the activation
    if (v > p->lastValue)
      p->lastValue = v;
    else
      p->state = PLAY;
    break;
      
  case PLAY: // play the instrument
    if (p->noteOn) midi.noteOff(p->kmValue);  // if it was on, turn it off
    midi.noteOn(p->kmValue, p->lastValue);    // play the note
    p->noteOn = (p->activeTime != 0);         // don't record if timer is zero
    p->lastOnTime = millis();
    p->state = EXCLUDE;
  break;

  case EXCLUDE: // ignore signals during the exclusion time
    if (millis() - p->lastOnTime >= p->excludeTime)
      p->state = START;
  break;
  }
}

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s