Drum Master

DIY Electronic Drum Brain

 

Master Software

The Master software is what runs on the Arduino controller. In its very simplest form, it polls all 32 analog inputs and 8 digital inputs, reports back to the computer when a hit occurs. To decrease the latency, we actually poll the digital triggers, and only read an analog sensor if a logic high is detected.

The Arduino code as of the time of this writing is included below. This is a Wiring / Processing style program, which is essentially a subset of C, so it should be pretty familiar to most programmers. For more specific information about the language, see the Arduino Reference page.

/*
 * Drum Master - controller for up to 32 + 8 sensors.
 * Copyright 2008 - 2009 Wyatt Olson <wyatt.olson@gmail.com>
 * 
 * At a very high level, Drum Master will listen to a series of sensors (both analog, via piezo
 * transducers, and digital, via grounding pullup resistors), and report the values back to the
 * computer via the serial port.  It does this in packets, after a certain maximum time delay,
 * using the ASCII protocol "<channel 1>:<velocity 1>;<channel 2>:<velocity 2>;...<channel x:velocity x>;"
 * where <channel> is the channel number, between 0 and 39, and <velocity> is the velocity of the
 * analog strike, between 0 and 1023 (for channels 0 - 31), or velocity is the state of the digital
 * channel 0 (off) or 1 (on), (for channels 32 - 39).
 *
 * Slave software, running on the computer, must take these data packets and map them to digital
 * audio samples, based on the channel, velocity, and current state of the program.
 *
 * For more information, please visit http://drummaster.digitalcave.ca.
 * 
 * Changelog:
 * 1.5 - June 2009: -Added the concept of 'active channels'; channels that report
 *                  a value X number of times in a row are assumed to be active;
 *                  this is used for things like analog hi-hat controllers, where
 *                  we want to have a continuous report of changes, but not
 *                  constantly waste time on the serial port if there are no changes.
 *                  -Combine multiple simultaneous strikes into a single data packet
 *                  to reduce the number of expensive (~20ms each) serial writes.  
 *                  This has successfully decreased latency to unnoticable levels
 *                  when there are multiple simultaneous strikes.
 * 1.0 - May(?) 2008: -Initial version.  Works fine for basic drumming requirements.
 */

//Send debug signals as well as data.  This will likely make slave software 
// stop working; only set to true when you are debugging via Arduino serial
// console view.
#define DEBUG false

//Analog Pins offset and count
#define A_OFFSET 0
#define A_COUNT 4

//Trigger Pins offset; count is the same as Analog
#define T_OFFSET 2

//Digital Pins offset and count
#define D_OFFSET 6

//Multiplexer selector pins.  S2 is MSB; S0 is LSB.
#define S0 10
#define S1 9
#define S2 8

//How long to wait after selecting a new sensor in a bank (us).
//#define SELECTION_DELAY 2

//How many times to read the input; this can help to ensure a good average reading is obtained.
#define ANALOG_SAMPLES 3

//EXTERNAL or DEFAULT.  Leave this as EXTERNAL unless you know what you are doing.
#define ANALOG_REFERENCE EXTERNAL 

//After a hit, don't report the same sensor for this long (ms).
#define ANALOG_BOUNCE_PERIOD 15  
//After a digital change of state, don't report changes for this long (ms).
#define DIGITAL_BOUNCE_PERIOD 50  

//After this many polling iterations which contain data, we will assume that the channel is
// an active channel, and will poll less frequently.  Furthermore, we will only send a new event 
// when the state changes by more than a given percentage, to reduce useless chatter on the
// serial port.  (Active channels are used, for instance, on a hi hat, or other device where 
// there will always be some voltage present).  We only use this for analog sensors, as digital
// sensors will report all the time anyway, for any state changes (and there is almost no overhead
// incurred in reading a digital sensor, while there is about 1ms delay when reading analog).
#define MIN_ACTIVE_CHANNEL_POLL_COUNT 25
//How much of a change (in absolute values, based on a full data range of 0-1023) must happen 
// before we report a change over the serial port
#define MIN_ACTIVE_CHANNEL_REQUIRED_CHANGE 25
//How long (in ms) do we wait between polling active channels?
#define ACTIVE_CHANNEL_POLL_INTERVAL 35


//This is used for a number of data buffers.  It should be set to the number of channels (40 in default hardware)
#define BUFFER_SIZE 40

//In ms, how frequently (max) do we send a data burst?  If 
#define MAX_DATA_PACKET_INTERVAL 20

//Temp variables; i and j are iterators for port and bank respectively; s and v are the selector 
// address (channel) and velocity respectively;  x is truly a temp variable, and can be used for
// anything you wish.  There are no guarantees that it will keep its value for any length of time,
// so make sure nothing else stores to it before you read your value.
int i, j, s, v;
long x;



//If there are this many loops without reading any new data, but there is data still waiting in 
// the buffer, we just go ahead and send the waiting data right away.  This ensures that we don't
// wait for the max amount of time unless we are actually still reading sensor data.
#define MAX_LOOPS_WITHOUT_DATA 3

//Have we read any data in the last loop?  Set to false at the beginning of the loop, and marked as
// true whenever we read any data.
boolean read_data_this_loop;

//Is it time to read active channels?  Set to true at the beginning of a loop if this is the case, and
// reset at the end.
boolean read_active_channels;

//Counts how many consecutive loops have not resulted in any data being read.  Once this goes over 
// MAX_LOOPS_WITHOUT_DATA, we will send a data packet (if there is data waiting in the buffer).
int loops_without_reading_data;

//Active counter.  Increments each cell by one each time a value for the corresponding channel is
// read; if there is no value read, it will reset the cell to 0.  Once a given cell gets above
// MIN_ACTIVE_CHANNEL_POLL_COUNT, we will poll on a less frequent basis, and will only report large
// changes over the serial port.
int consecutive_reads[BUFFER_SIZE];

//Keep track of active channels, to reduce computations.  Once a channel is set to be active,
// only a reset will change it back.
boolean active_channel[BUFFER_SIZE];

//Set to the current time at the beginning of each loop, to reduce the number of expensive 
// calls to millis().
long time;

//When was the last time that each channel was read?  Used for debounce.
long last_read_time[BUFFER_SIZE];
//What was the last value read from each channel?
int last_value[BUFFER_SIZE];
//When was each bank last read?
long bank_last_read_time[8];
//When did we last read the active channels?
long last_read_active_channels;

//When we first sense something, we mark the time; once MAX_DATA_PACKET_INTERVAL ms
// have passed (or we have reached the max number of loops without seeing any new data)
// will we send the data burst.
long start_data_packet_time = -1; 

//This is where we buffer data until we have read enough to send a (very expensive) data
// burst over the serial port.
int data_buffer[BUFFER_SIZE]; 


//Setup method is called once at the beginning of execution.
void setup() {
  //Setup 3.3v reference pin if desired
  analogReference(ANALOG_REFERENCE);

  //You don't have to set the analog sensors to input mode; here we just set the triggers (which are digital)
  for (i = 0; i < A_COUNT; i++){
    pinMode(i + T_OFFSET, INPUT);
  }
  //The digital pin, however, does need to be set to input mode.
  pinMode(D_OFFSET, INPUT);

  //The three MUX selector switches need to be set to output.
  pinMode(S2, OUTPUT);
  pinMode(S1, OUTPUT);
  pinMode(S0, OUTPUT);

  //Initialize array counters
  for (i = 0; i < BUFFER_SIZE; i++){
    //Initialize data buffer to all -1's
    data_buffer[i] = -1; //-1 is the marker for 'no data' 

    //Initialize the active consecutive reads counter
    consecutive_reads[i] = 0;

    //Reset the active channels to all false
    active_channel[i] = false;
  }

  Serial.begin(115200);
}

//Loop is called repeatedly after setup has completed.
void loop() {
  time = millis();
  read_data_this_loop = false;

  //If ACTIVE_CHANNEL_POLL_INTERVAL ms have passed since the last time we read active channels,
  // go ahead and read them now.
  if (time - last_read_active_channels > ACTIVE_CHANNEL_POLL_INTERVAL){
    read_active_channels = true;
    last_read_active_channels = time;
  }

  //We read pin 0 on each multiplexer in turn, then select pin 1 and read from each in turn,
  // etc.  This reduces the number of switches we need to make on the multiplexers.  While 
  // in theory these switches are not very expensive (the spec sheets indicate that they are
  // almost instantaneous), doing it this way can possibly allow people to use slower MUXs
  // if desired; you could add a couple Microsecond delay after selecting the next port, and
  // give the MUX time to settle out.
  for (i = 0; i < 0x8; i++){  //i == port (one channel on a multiplexer)
    set_mux_selectors(i);
    //     delayMicroseconds(SELECTION_DELAY);

    for (j = 0; j < A_COUNT; j++){ // j == bank
      s = get_channel(j, i);      

      //If the channel is defined to be 'active' (i.e., connected to a device which always
      // reports its state, such as a hi hat pedal, rather than a device which only reports
      // state when struck, like a piezo), then we poll less frequently.
      if (consecutive_reads[s] > MIN_ACTIVE_CHANNEL_POLL_COUNT && !active_channel[s]){
        active_channel[s] = true;
        if (DEBUG) {
          Serial.print("Switching channel ");
          Serial.print(s);
          Serial.println(" to be 'active'");
        }
      }

      //Check the last read time, and if the trigger is active.  Triggers are used to determine
      // whether an analog sensor has any data waiting to be read; reading a digital signal 
      // (Trigger) is much less expensive than reading an analog sensor, (an analog sensor 
      // seems to take upwards of 1ms to read, while a digital sensor is a fraction of that).
      if (!active_channel[s] || read_active_channels){
        if (time - last_read_time[s] > ANALOG_BOUNCE_PERIOD){
          if (digitalRead(j + T_OFFSET)){
            v = get_velocity(j + A_OFFSET);
            if (!active_channel[s] || abs(v - last_value[s]) > MIN_ACTIVE_CHANNEL_REQUIRED_CHANGE){
              if (DEBUG){
                if (active_channel[s]){
                  Serial.print("Large change of value; old value is ");
                  Serial.print(last_value[s]);
                  Serial.print("; new value is ");
                  Serial.println(v);
                } 
              }
              if (start_data_packet_time == -1)
                start_data_packet_time = time;

              if (data_buffer[s] < v){
                data_buffer[s] = v;
                last_value[s] = v;
              }
              last_read_time[s] = time;
              bank_last_read_time[i] = time;
              read_data_this_loop = true;

              //Only bother incrementing consecutive reads for channels not already defined as active.
              if (!active_channel[s]){
                consecutive_reads[s] = consecutive_reads[s] + 1;
              }
            }
          }
          else {
            if (DEBUG){
              //             Serial.print("Resetting consecutive reads for channel ");
              //             Serial.println(s);
            }
            consecutive_reads[s] = 0;
          }  
        }   
      }
    } //for j ...

    j = 4; //Digital pin.  Change this to a loop if we add another.
    s = get_channel(j, i);
    if (time - last_read_time[s] > DIGITAL_BOUNCE_PERIOD){
      //Remember that digital switches in drum master are reversed, since they 
      // use pull up resisitors.  Logic 1 is open, logic 0 is closed.  We invert
      // all digital readings to make this easy to keep straight.
      v = !digitalRead(D_OFFSET);
      if (v != last_value[s]){
        if (start_data_packet_time == -1)
          start_data_packet_time = time;
        data_buffer[s] = v;
        last_read_time[s] = time;
        last_value[s] = v;
        read_data_this_loop = true;
      }
    }
  }


  //If there is data in the buffer (start_data_packet_time > -1) and either there
  // was no data read this last iteration OR the max packet time has expired, 
  // we will send the data burst
  if (start_data_packet_time > -1){
    if (!read_data_this_loop){
      loops_without_reading_data++;
      if (DEBUG){
        //         Serial.print("loops without reading data:");
        //         Serial.println(loops_without_reading_data);
      }
    }
    else {
      loops_without_reading_data = 0; 
    }

    if (loops_without_reading_data > MAX_LOOPS_WITHOUT_DATA 
      || start_data_packet_time + MAX_DATA_PACKET_INTERVAL < time){
      if (DEBUG){
        //         Serial.print("Sending data burst ");
        //         Serial.print(time - start_data_packet_time);
        //         Serial.println("ms after first hit detected"); 
      }
      send_data_burst();
    }
  }

  //We will read active channels again in ACTIVE_CHANNEL_POLL_INTERVAL ms.
  read_active_channels = false;
}

/*
 * Writes all data from the data buffer to the serial port, and finishes with a new line character.
 * Resets everything in the buffer to -1.  Also resets the loop without reading data counter, and 
 * the start time for the data packet.
 */
void send_data_burst(){
  //After all ports / banks have been read, send a data burst.
  for (i = 0; i < BUFFER_SIZE; i++){
    if (data_buffer[i] != -1){
      Serial.print(i);
      Serial.print(":");
      Serial.print(data_buffer[i]);
      Serial.print(";"); 
      data_buffer[i] = -1;
    }
  }
  Serial.println();

  start_data_packet_time = -1;
  loops_without_reading_data = 0;
}

/*
 * Sets pins S2 - S0 to select the multiplexer output.
 */
void set_mux_selectors(int s){
  digitalWrite(S2, s & 0x4);
  digitalWrite(S1, s & 0x2);
  digitalWrite(S0, s & 0x1);
}

/*
 * Convert from a bank / port tuple to single channel number (value from 0 - 39 inclusive)
 * for sending to Drum Slave software.
 */
int get_channel(int bank, int port){
  return (bank << 3) | port;
}

/*
 * Reads the input a number of times, and returns the maximum.  The number 
 * of times to read the input is defined by ANALOG_SAMPLES
 */
int get_velocity(int pin){
  if (DEBUG){
    x = millis(); 
  }
  int max_val = 0;
  for (int t = 0; t < ANALOG_SAMPLES; t++){
    max_val = max(analogRead(pin), max_val); 
  }
  if (DEBUG){
    Serial.print("Read ");
    Serial.print(ANALOG_SAMPLES);
    Serial.print(" samples from pin ");
    Serial.print(pin);
    Serial.print(" in ");
    Serial.print(millis() - x);
    Serial.println("ms");
  }

  return max_val;
}