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;
}