music

Bass detection with Arduino

This post is a continuation of my previous post about sound analysis in Arduino.

In this post I will explain how I concretely managed to synchronize a light to the low frequencies of music (that is the bass).

As seen in my previous post, there are several alternatives for doing this job, so I will focus on a particular selection of tools:

The circuit:
The easy one: I use the Sparkfun’s electret kit.
The algorithm:
I will try to create a FIR filter for selecting only the low frequencies of the signal, and I will not tweak the analog-to-digital sampling frequency.

As the circuit is trivial, let’s see the details about the algorithm. For designing the filter I have used a wonderful free online tool, TFilter, from Iowegian.

The tool allows specifying the sampling frequency and the frequency response of the filter.
In my case I chose to allow passing frequencies for 0 to 200Hz and to block frequencies from 300Hz on. You can see the graph of the frequency response of my filter below:

Low pass digital filter frequency response

Low pass digital filter frequency response

As for the sampling frequency it’s tricky. It, in fact, depends on the code which then depends on the filter that you have. Generating the right filter is therefore an iterative process: you generate a first version of the filter with a certain sampling frequency (say 5000Hz), you import the code from the online tool (the code is automatically generated for you in the “source code” tab) and stick into your sketch, then you run a sketch that measures the actual sampling frequency, for instance by filtering a thousand samples and measuring the time needed for that. An example of such code is the following:

#include "LowPassFilter.c"

//The number of samples to buffer before analyzing them
int samplesN = 1000;
int micPin = 0;

LowPassFilter* filter;

void setup(){
  Serial.begin(9600);
  filter = new LowPassFilter();
  LowPassFilter_init(filter);
}

void loop(){
  long pow = 0;
  long filtpow = 0;
  int peak = 0;
  long start = millis();
  for(int k=0; k<samplesN; k++){
    int val = analogRead(micPin);
    LowPassFilter_put(filter, val);

    int filtered = LowPassFilter_get(filter);

    pow += ((long)val*(long)val)/samplesN;
    filtpow += ((long)filtered*(long)filtered)/samplesN;
    peak = max(peak, filtered);
  }
  long end = millis(); 
  float freq = ((float)samplesN * (float)1000) / ((float)end - (float)start);

  Serial.print(freq);
  Serial.print(" ");
  Serial.print(pow);
  Serial.print(" ");
  Serial.print(filtpow);
  Serial.print(" ");
  Serial.println(peak);
}

This code takes 1000 samples, filters them and computes the power of the signal, both the filtered and unfiltered ones and the sampling frequency. The power is simply the sum of the second power of the samples divided by the number of samples.
Note that I am using the integer implementation of the filter (in the source code tab of TFilter you can select floating points or integers in the “Number format” field) because integers arithmetic is much faster than floating points one.

Once you know what sampling frequency you go back to your TFilter tool and redesign the filter with the actual sampling frequency, but, given that the specifications are changed, it is very likely that the length of your filter will be different now, fact that will affect the computation required to run it and, therefore, the sampling frequency again!

So you need to do this work of: designing the filter + measuring actual sampling frequency + adjusting the filter, in some iterations (2 or 3 should be enough).

In my case the final sampling frequency is 2500Hz and the filter has 31 “taps” (the more complex is the filter the more taps you need, the more time you need to make it run at each sample).

So let’s check that it really filters the signal around 200-300Hz. I connected a pair of headphones to the microphone and measured the power of the signal while emitting some sine signals through them. The results is shown in the following table:

noise 50 Hz 100 Hz 200 Hz 300 Hz 500 Hz 800 Hz
total power 265760 307555 427998 458491 464047 466989 483063
filtered power 163604 198169 378124 275661 161191 162059 181106
peak 623 772 1087 950 548 537 613
filter/unfilter(db) -2.11 -1.91 -0.54 -2.21 -4.59 -4.60 -4.26

in terms of decibel it is not very impressive, but it does some filtering though. The problem here is that the circuit gets a lot of noise and a significant part of it goes into the low frequencies so, for instance, when you push a 800Hz sine the Arduino gets the 800Hz + the noise, therefore the total power is not so low.

Now let’s do something with this signal, for instance synchronizing a light on the bass of the music. For that we can use the measurement of the power or, even a simpler measurement, the peak.

To make it more responsive, instead of sampling 1000 samples, we can get 200 instead, and, in order to make it adaptive to the noise and general sound level we can play this trick: as the peak will move between a minimum and a maximum, we take the current peak and map the interval between the average and the maximum peak to a 0-1023 interval.

The final code appears something like this:

#include "LowPassFilter.c"

//The number of samples to buffer before analyzing them
int samplesN = 200;

int micPin = 0;

LowPassFilter* filter;

void setup(){
  Serial.begin(115200);
  filter = new LowPassFilter();
  LowPassFilter_init(filter);
}

int index = 0;
int maxpeak = 0 ;
int minPeak = 1023;

void loop(){
  int peak = 0;

  for(int k=0; k<samplesN; k++){
    int val = analogRead(micPin);
    LowPassFilter_put(filter, val);

    int filtered = LowPassFilter_get(filter);
    peak = max(peak, filtered);
  }  
  maxpeak = max(maxpeak, peak);
  minPeak = min(minPeak, peak);

  index++;
  if(index == 1000){
    maxpeak = 0;
    minPeak = 1023;
  }
  int lvl = map(peak, minPeak, maxpeak, 0, 1023);
  Serial.println(lvl);
}

It’s a pretty simple code as you see. Remember to also add the .h and .c files generated by the TFilter tool and also check that the sampling frequency hasn’t changed (which probably has). Please note also that there is a counter (index) that, each 1000 iterations, resets the minimum and maximum detected values. This helps if the sound level changes and you want your code to adapt to it.
From this code you can also make a sort of beat detector, for instance by setting a threshold and counting the number of times the peak goes on top of the threshold in a certain time window, but it would require more elaboration for sure.

In this code I am not activating any light, I am just sending the data to the computer to see how it is working. As the numbers run really fast on the serial monitor, I have created a simple Processing script that acts as a sort of equalizer bar, the code is here:

import processing.serial.*;

Serial myPort;

float MIN_VAL = 0;
float MAX_VAL = 1023;

void setup () {
  size(300, 500);        
  println(Serial.list());
  // Open whatever port is the one you're using.
  myPort = new Serial(this, Serial.list()[0], 115200);

  // don't generate a serialEvent() unless you get a newline character:
  myPort.bufferUntil('\n');
  background(0);
}

void draw () {
  // everything happens in the serialEvent()
}

void serialEvent (Serial myPort) {
  String inString = myPort.readStringUntil('\n');
  if (inString != null) {
    // trim off any whitespace:
    inString = trim(inString);
    // convert to a float and map to the screen height:
    float inval = float(inString); 

    inval = map(inval, MIN_VAL, MAX_VAL, 0, height-20);
     background(0);
    rect(10,10, width-20, inval);
  }
}

I have tested with some disco music and I have o say that the effect is quite good, when the bass is pumping you really see the bar going up and down at the same tempo, though, I have to say, you can also see some delay, which does not matter as long as you want to make some light controller anyway. By reducing the number of acquired samples to something less than 200 (for instance 100) you get a more responsive system but also a more sensible one, which makes the bar moving really fast and sometimes you see the noise clearly getting into the animation.