Summary:

Use rudimentary digital signal processing techniques to trigger events when the bass is bangin'.

Materials:

materials-6

  1. Arduino
  2. USB-Micro Cable
  3. Adafruit MAX9814 - Electret Microphone
  4. LED (and appropriate resistors)
  5. Breadboard & Leads
  6. Arduino IDE

Process:

Assembly:

Plug the USB-Micro cable into the Arduino and computer. Connect the Vdc and GND pin of the MAX9814 board to the 5V and GND pin of the Arduino, respectively. Connect the Out pin of the MAX9814 to an analog pin on the Arduino. Connect the LED cathode to the GND pin of the Arduino, and the LED anode to an Arduino pin.

connections-2

Listening to Sounds:

The MAX9814 board is super easy to use. Essentially, it returns the input voltage scaled to indicate the amplitude of sounds heard (minus some offset voltage). To plot the microphone input we only need a few lines of code.

First, we define a constant representing the pin on which the microphone is attached. Since the MAX9814 specifies a bias voltage of 1.25 volts, we'll need to calculate a value to subtract from our reading. The analogRead() Arduino function returns a value between 0 and 1024 indicating the voltage detected between 0 and VCC. Since our board is a 5V board we'll need to offset the reading by (1.25/5)*1024. Finally, after setting up a fast serial connection we can sample and print the value read every 10 ms or so.

#define MIC_PIN A3

float v_offset = (1.25/5.0)*1024.f;

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

void loop() {
    unsigned long time = micros();
    int i = 0;
    
    for(i=0;;++i) {
        if(i == 10000 ) { // 10ms
            float sample = (float)analogRead(MIC_PIN)-v_offset;
            Serial.println(sample);
            i=0;
        }
    }
}

If we run this code while playing music we can capture the results with the Serial Plotter (Tools > Serial Plotter). We'll see something like the following.

music

Not bad, you can clearly see peaks in the graph where beats are louder than the rest of the music. To detect these we'll need to do some digital signal processing to isolate them.

Digital Signal Processing:

We want to perform (minimum) 3 operations on the raw data to do beat detection. First we'll create a bandpass filter to reject frequencies outside of the bass range. Then we'll create an envelope filter to isolate frequencies of10 Hz and detect envelope. Then we'll filter for 1.7 - 3 Hz (100-180 bpm) with another bandpass filter.[1].

// 20 - 200hz Single Pole Bandpass IIR Filter
float bassFilter(float sample) {
    static float xv[3] = {0,0,0}, yv[3] = {0,0,0};
    xv[0] = xv[1]; xv[1] = xv[2]; 
    xv[2] = (sample) / 3.f; // change here to values close to 2, to adapt for stronger or weeker sources of  audio  
    
    yv[0] = yv[1]; yv[1] = yv[2]; 
    yv[2] = (xv[2] - xv[0])
        + (-0.7960060012f * yv[0]) + (1.7903124146f * yv[1]);
    return yv[2];
}

// 10hz Single Pole Lowpass IIR Filter
float envelopeFilter(float sample) { //10hz low pass
    static float xv[2] = {0,0}, yv[2] = {0,0};
    xv[0] = xv[1]; 
    xv[1] = sample / 50.f;
    yv[0] = yv[1]; 
    yv[1] = (xv[0] + xv[1]) + (0.9875119299f * yv[0]);
    return yv[1];
}

// 1.7 - 3.0hz Single Pole Bandpass IIR Filter
float beatFilter(float sample) {
    static float xv[3] = {0,0,0}, yv[3] = {0,0,0};
    xv[0] = xv[1]; xv[1] = xv[2]; 
    xv[2] = sample / 2.7f;
    yv[0] = yv[1]; yv[1] = yv[2]; 
    yv[2] = (xv[2] - xv[0])
        + (-0.7169861741f * yv[0]) + (1.4453653501f * yv[1]);
    return yv[2];
}

Triggering an Action:

Now to use this information for beat detection we just have to apply our filters, and a threshold over which we want to trigger an action. Based on testing with my hardware I found 120 to be a suitable value, but you'll need to play around.

Inside our sample loop we'll apply all three filters to the value read from our analog pin. Periodically we'll apply the low frequency bandpass filter, and use the results to determine if a beat was present. If so we'll light up an LED. For debugging we'll also send our filtered value, threshold, and trigger indicator to the serial port.

#define THRESHOLD 120

for (i=0;;++i) {
    sample = (float)analogRead(MIC_PIN)-254.f;

    // Filter only bass component
    value = bassFilter(sample);

    // Take signal amplitude and filter
    if(value < 0)value=-value;
    envelope = envelopeFilter(value);

    // Every 200 samples (25hz) filter the envelope 
    if(i == 200) {

            // Filter out repeating bass sounds 100 - 180bpm
            beat = beatFilter(envelope);
            
            Serial.print(THRESHOLD);
            Serial.print(",");Serial.print(beat);
            Serial.print(",");Serial.println(50*(beat>THRESHOLD));
            
            digitalWrite(6, (beat>THRESHOLD) ? HIGH : LOW);

            //Reset sample counter
            i = 0;
}

Running this code with the Serial Plotter yields a graph like the following. We can see that whenever the signal (red) is greater than the threshold (blue) our trigger indicator (green) is high.

beat_vs_thresh

Code:

Code examples for today can be found here.

Future Work:

This is a common technique, useful for basic animated light shows, VU-meter simulations, or other sound-triggered projects. The functions


  1. Base filter code from Damian Peckett's DSP algorithms. ↩︎