Frequency Detection

Hello,

I have looked around a lot for something to detect frequency but I thought maybe some direct help would be better.

All I want to do is to have a microphone input to my Arduino Uno and, once a frequency is present, then produce a high on a digital pin. Maybe a program that will detect the peak and then I can say something like:?

if (peak < upperValue && peak > lowerValue)
DigitalWrite(pin 1, HIGH)

It seems pretty straightforward but I know there is a lot to it. But there has got to be a good base code that is out there somewhere.

The trouble with that is a microphone picks up every frequency. Background noise has a frequency. So not only do you need to detect frequency but you also need amplitude (loudness.)

There may not be a lot of code out there as most of this project will be done in analog circuits. You need to adjust the gain of the microphone, filter out frequencies completely outside your area of interest and then narrow down on the actual frequency you want.

The Arduino analog input can't sample fast enough to capture the waveform of anything but the lowest frequencies, so you don't have a lot of data to do digital signal processing on. Another processor with better DSP hardware might be better.

The Arduino analog input can't sample fast enough to capture the waveform of anything but the lowest frequencies, so you don't have a lot of data to do digital signal processing on. Another processor with better DSP hardware might be better.

I think your going to far with the above statement.

Surely a bandpass filter (as you say) is the first stage, follow this with a rectifier(full wave?) and some smoothing and then sample with the ADC. If the value from analogRead is > than a threshold then we have it.

Mark

Well I am trying to detect a whistle. There are three frequencies that will be present and detecting one frequency is fine with me. I am open to using something else. I just kind of want it to be as straight forward as possible!

At minimum, you'll need a preamp to boost the mic output from a few millivolts to something in the useful-range of the Arduino's ADC . If you feed the audio directly into the Arduino* you'll need to bias the signal, since the Arduino can't read the negative-half of the AC audio waveform. If you use an electret condenser mic, the preamp needs to provide power to the mic.

This can be done in hardware as Mark is suggesting, or in software with a digital filter.

Oh... A bandpass filter passes a certain frequency-band. It rejects (or reduces) frequencies that are higher or lower.

If you do it in software you'll have to sample the audio at a known sample-rate to get PCM audio data. (i.e. CDs are sampled at 44.1kHz, but it's my understanding that the Arduino's ADC isn't quite capable of sampling that fast.)

To minimize false detection, you might also want to reject any short-term sounds that happen to match the whistle frequency... Maybe require the whistle-sound to be present for half a second or more before triggering your event.

  • If you follow Mark's suggestion and use a rectifier or peak detector, you are not sending audio into the Arduino and you don't need the bias.

CDs - yes, two channels at 44100 Hz, 16 bits minimum.
Arduino controlling an external ADC can store 1 channel to an SD card at that speed & width.
Or read from SD card and playback thru external DAC.
Can't do both together and can't do 2 channels.

I have been looking in to this for a while now and came up with a really good solution not too long ago. Its called FFT (Fast Fourier Transform) and it takes an audio signal (or any analog signal) and it returns several bins of varying frequencies.

Now about the ADC speed. If you take into account that the ADC takes >100uS to sample, then you could only sample at about 10khz maximum, not including the FFT calculations. HOWEVER! if you change the ADC prescaler to 16 (instead of 128), you can reduce the sample time to 20uS a sample without too much loss of accuracy, effectively giving you a max sample rate of 50khz.

Here is my code that works for me.

You will need this library: ArduinoFFT - Open Music Labs Wiki

//defines
#define WINDOW 0
#define LIN_OUT 1
#define FFT_N 128 // set to x point fft

// Define various ADC prescaler (slight loss in precision)
const unsigned char PS_16 = (1 << ADPS2);
const unsigned char PS_32 = (1 << ADPS2) | (1 << ADPS0);
const unsigned char PS_64 = (1 << ADPS2) | (1 << ADPS1);
const unsigned char PS_128 = (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);

int ADCexecutionTime;
int sampleRate;
int sampleDelay;
int outMultiplier;
//includes
#include <FFT.h>

void setup()
{
  Serial.begin(115200);
  //Serial.println("\n=====Program Starting=====\n");

  // set up the ADC
  ADCSRA &= ~PS_128;  // remove bits set by Arduino library
  ADCSRA |= PS_32;    // set our own prescaler to 32
  ADCexecutionTime = 30; //about how long a sample takes with prescaler 32
  
  sampleRate = 10000; //can measure 0 through sampleRate/2 hertz
  sampleDelay = (1000000L / (long)sampleRate) - ADCexecutionTime;
  //outMultiplier = (sampleRate >> 1) / (FFT_N >> 1); //frequency range of each bin
}
int high = 0, temp = 0, f = 0;
int t = 0;
void loop()
{
  //start sampling
  for(int i = 0; i < FFT_N; i++)
  {
    fft_input[i << 1] = analogRead(A0) - 512;
    fft_input[(i << 1) + 1] = 0;
    delayMicroseconds(sampleDelay);
  }

  //these 2 functions take 2.8ms to complete with FFT_N = 128
  fft_reorder(); // reorder the data before doing the fft
  fft_run(); // process the data in the fft

  //send data through serial for processing to graph it
  high = 0;
  Serial.write(255); //im using this as the start character
  for(int i = 0; i < (FFT_N >> 1); i++)
  {
    high = ((unsigned int)sq(fft_input[i << 1]) + (unsigned int)sq(fft_input[(i << 1) + 1])) >> 1; //the bin value really = sqrt(sq(re) + sq(im)) but this is faster and is still easy to read
    if(high > 254)high = 254;
    Serial.write((byte)high); //send to processing to view it
  }
}

Because sampleRate is 10,000 we can only map frequencies 0-5,000. With an FFT_N size 128, this gives us 64 bins of relevant data (the last 64 are a copy of the first, thats how fft works). So with 64 bins mapped through 5,000hz you can figure out what each bin represents and what the accuracy of each bin is. Each bin is averaged between every 78hz. You could always double the FFT_N size to half that number, but it is slower and takes alot of ram

Because sampleRate is 10,000 we can only map frequencies 0-5,000.

Because of aliasing, you must make certain that frequencies greater than 5000 Hz are not present in the input to the FFT, or you are guaranteed to see spurious results. So, you need at least a low pass filter between the microphone and the Arduino in any case.

Ps991, Thanks for all of the work!!

I included the library, ran it, and then it complied. But I am looking at the monitor and I am getting random characters... I made sure the baud rate matched. As stated before, I just need to amplify the signal, then bias it, and then bring it to one of the analog pins?

This is why you are getting "random characters":

Serial.write((byte)high); //send to processing to view it

Use Serial.print instead. And don't forget the low pass antialiasing filter.

Here is my graphing code in Processing. Dont hate me for the poor programming techniques...

//Reads 0 - 5 volts

import processing.serial.*;

Serial myPort;  // Create object from Serial class
float val, oldVal;      // Data received from the serial port
String portName;

color lineColor = color(255,0,0);

static final int STARTX = 72;
static final int STARTY = 32;
float heightResolution = 0.01;

int GRAPH_HEIGHT;
float GRAPH_LOW;
float GRAPH_HIGH;
float GRAPH_STEPY;

byte serialPort = 3;

void setup() 
{
  size(800, 400);
  // /dev/tty.usbmodem1411
  portName = Serial.list()[serialPort];
  myPort = new Serial(this, portName, 115200);
  
  GRAPH_LOW = 00;
  GRAPH_HIGH = 254;
  GRAPH_STEPY = 10;
  heightResolution = (GRAPH_HIGH - GRAPH_LOW) / (height - STARTY);
  GRAPH_HEIGHT = height;
  
  textFont(createFont("Consolas", 16));
  
  noStroke();
  
  background(255); // Set background to white
  
  frameRate(120);
  
  fill(0);
  drawGridUnits(GRAPH_LOW, GRAPH_HIGH, GRAPH_STEPY);
}

int x = 0, oldX = 0;
int t = 0;
int temp = 0, temp2 = 0;
void draw()
{
  noStroke();
  if(myPort != null && myPort.available() > 0)
  {  // If data is available,
    //val = getValue();         // read it and store it in val
    
    //fill(255,0,0);
    //rect(x, y, w, h)
    /*rect(oldX + STARTX, ((GRAPH_HEIGHT - (oldVal * (1.0 / heightResolution))) - (12 + 2)) - STARTY, textWidth("888.888"), 12 + 2);
    fill(0);
    text(val, x + STARTX, ((GRAPH_HEIGHT - (val * (1.0 / heightResolution))) - 2) - STARTY);
    graph(x + STARTX, val, lineColor);
    oldX = x;
    oldVal = val;
    x++;
    x %= width - STARTX;*/
    
    fill(255);
    strokeWeight(0);
    rect(STARTX, height - STARTY, width - STARTX, -GRAPH_HEIGHT);
    x = 0;
    if(myPort.read() == 255) //start value
    {
      drawLinearGrid(GRAPH_LOW, GRAPH_HIGH, GRAPH_STEPY);
      temp2++;
      fill(0);
      int Sread;
      while(x < 64)
      {
        while(myPort.available() <= 0)
          delay(1);
        Sread = myPort.read();
        rect(STARTX + x*11, (GRAPH_HEIGHT - (val * (1.0 / heightResolution))) - STARTY, 10, -((Sread - GRAPH_LOW > 0) ? (Sread - GRAPH_LOW) : 0) * (1.0 / heightResolution));
        x++;
      }
    }
    if(millis() - temp > 1000)
    {
      stroke(255);
      strokeWeight(0);
      temp = millis();
      fill(255);
      rect(8, height - 7, textWidth("888 hz "), -(12 + 2));
      fill(0);
      text(temp2 + " hz", 8, height - 8);
      temp2 = 0;
    }
    delay(1);
  }
  
  //print(millis());
  //print("   ");
  //println(myPort);

  if(myPort == null && Serial.list().length >= serialPort && Serial.list()[serialPort].equals(portName))
    myPort = new Serial(this, portName, 115200);
  else if(Serial.list().length < serialPort)
  {
    if(myPort != null)
      myPort.stop();
    myPort = null;
  }
}

void graph(int x, float y, color c)
{
  // draw the line:
  stroke(c);
  line(x, (GRAPH_HEIGHT - STARTY), x, (GRAPH_HEIGHT - STARTY) - (y * (1.0 / heightResolution)));
  stroke(255);
  line(x, 0, x, (GRAPH_HEIGHT - STARTY) - (y * (1.0 / heightResolution)) - 1);
  
  int inc = 1;
  if(red(c) >= 255 && green(c) < 255 && blue(c) <= 0)
  {
    lineColor = color(red(c), green(c) + inc, blue(c));
  }
  else if(green(c) >= 255 && red(c) > 0 && blue(c) <= 0)
  {
    lineColor = color(red(c) - inc, green(c), blue(c));
  }
  else if(green(c) >= 255 && blue(c) < 255 && red(c) <= 0)
  {
    lineColor = color(red(c), green(c), blue(c) + inc);
  }
  else if(green(c) > 0 && blue(c) >= 255 && red(c) <= 0)
  {
    lineColor = color(red(c), green(c) - inc, blue(c));
  }
  else if(blue(c) >= 255 && red(c) < 255 && green(c) <= 0)
  {
    lineColor = color(red(c) + inc, green(c), blue(c));
  }
  else if(red(c) >= 255 && blue(c) > 0 && green(c) <= 0)
  {
    lineColor = color(red(c), green(c), blue(c) - inc);
  }
}

void drawLinearGrid(float low, float high, float interval)
{
  strokeWeight(0);
  stroke(0);
  
  line(STARTX, 0, STARTX, (GRAPH_HEIGHT - STARTY) + 8);
  
  int j = 0;
  for(float i = low; i < high; i += interval)
  {
    line(STARTX - 8, (GRAPH_HEIGHT - STARTY) - j * ((GRAPH_HEIGHT - STARTY) / ((high - low) / interval)), width, (GRAPH_HEIGHT - STARTY) - j * ((GRAPH_HEIGHT - STARTY) / ((high - low) / interval)));
    j++;
  }
}

void drawGridUnits(float low, float high, float interval)
{
  strokeWeight(0);
  stroke(128);
  textAlign(RIGHT, CENTER);
  int j = 0;
  for(float i = low; i < high; i += interval)
  {
    text(i, STARTX - 10, (GRAPH_HEIGHT - STARTY) - j * ((GRAPH_HEIGHT - STARTY) / ((high - low) / interval)));
    j++;
  }
  textAlign(LEFT, BASELINE);
}

float getValue()
{
  float value = 0;
  float digit = 1;
  byte input = 0;
  boolean timeout = false;
  
  while(true)
  {
    while(myPort == null || myPort.available() <= 0)
    {
      if(timeout)
        return -1;
      delay(1);
      timeout = true;
    }
    input = (byte)myPort.read();
    return input * (20.0 / 1023.0);
  }
    /*if(input == 11)
      return value;
    timeout = false;
    
    if(input == 10) // decimal point
    {
      digit = 0.1;
      continue;
    }
    value += (float)input * digit;
    if(digit >= 1)
      digit *= 10;
    else
      digit /= 10;
  }*/
}

This is the circuit I used for the electret microphone, it uses a single supply op amp. The 22kOhm resistor is the gain resistor, I changed it to 200kOhm (which seems very high to me but it seems to be what I needed for it to be moderately sensative).