Arduino Frequency Detection

I'm working on a project with Arduino where I need to detect frequencies between 19kHz and about 25kHz from an audio input.

Is there a shield or something that can help me do this easily? I notice that things get a lot more complicated when you need more than a 8928 Hz sampling rate. If it's easier for Arduino to record less than 5 seconds of audio, analyze the frequencies in the audio clip, and from there determine if the desired frequencies were reached, that would work for me as well.

In case something I said didn't already give it away, I am very new to Arduino and circuits so thank you in advance for your patience. With that in mind, here's what I've done so far:

I've read/watched a few guides relating to this subject already:

  1. http://www.instructables.com/id/Arduino-Frequency-Detection/

  2. Fast sampling from analog input - Yet Another Arduino Blog

  3. Arduino Spectrum Analyizer - YouTube

I've read into more, but these are the main ones I've drawn from.

The first source gave me the best results, but could only detect frequencies up to approximately 19kHz (my guess is because of the sampling frequency set in the original code). I tried increasing the sampling rate with information from the second source but I still had the same problem. I'm sure I made mistakes in the code, but I also noticed I was experiencing a lot of clipping so I was wondering if my circuit needed to be set up differently.

Currently my circuit is identical to the one shown in source 3 but to a 3.5mm audio jack instead of directly to a microphone and with the clipping indicator LED hooked up to 12. However, I noticed a few guides showed a more complicated circuit for audio input (http://www.instructables.com/id/Arduino-Audio-Input/). Being new to circuits I had trouble following it and was unsure what I'd be getting from the more complicated circuit. At this point I thought maybe there was a shield that could take the place of that more complicated circuit.

Here is the code I got from the first source including the changes I made to try to get the sampling rate I need. What I found with this version is that it often spikes up to its new maximum of 38.4 kHz in the same way the previous version would spike to 19 kHz. Note that I didn't bother to change all the references to 38.4 kHz in the comments.

//generalized wave freq detection with sampling rate and interrupts
//by Amanda Ghassaei
//https://www.instructables.com/id/Arduino-Frequency-Detection/
//Sept 2012

/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
*/


//clipping indicator variables
boolean clipping = 0;

//data storage variables
byte newData = 0;
byte prevData = 0;
unsigned int time = 0;//keeps time and sends vales to store in timer[] occasionally
int timer[10];//sstorage for timing of events
int slope[10];//storage for slope of events
unsigned int totalTimer;//used to calculate period
unsigned int period;//storage for period of wave
byte index = 0;//current storage index
float frequency;//storage for frequency calculations
int maxSlope = 0;//used to calculate max slope as trigger point
int newSlope;//storage for incoming slope data

//variables for decided whether you have a match
byte noMatch = 0;//counts how many non-matches you've received to reset variables if it's been too long
byte slopeTol = 3;//slope tolerance- adjust this if you need
int timerTol = 10;//timer tolerance- adjust this if you need

//variables for amp detection
unsigned int ampTimer = 0;
byte maxAmp = 0;
byte checkMaxAmp;
byte ampThreshold = 30;//raise if you have a very noisy signal

void setup(){
  
  Serial.begin(9600);
  
  pinMode(13,OUTPUT);//led indicator pin
  pinMode(12,OUTPUT);//output pin
  
  cli();//diable interrupts
  
  //set up continuous sampling of analog pin 0 at 38.5kHz
 
  //clear ADCSRA and ADCSRB registers
  ADCSRA = 0;
  ADCSRB = 0;
  ADMUX |= (0 & 0x07);    // set A0 analog input pin
  ADMUX |= (1 << REFS0); //set reference voltage
  ADMUX |= (1 << ADLAR); //left align the ADC value- so we can read highest 8 bits from ADCH register only
  
  ADCSRA |= (1 << ADPS2); //set ADC clock with 32 prescaler- 16mHz/32=500kHz
  ADCSRA |= (1 << ADATE); //enabble auto trigger
  ADCSRA |= (1 << ADIE); //enable interrupts when measurement complete
  ADCSRA |= (1 << ADEN); //enable ADC
  ADCSRA |= (1 << ADSC); //start ADC measurements
  
  sei();//enable interrupts
}

ISR(ADC_vect) {//when new ADC value ready
  
  PORTB &= B11101111;//set pin 12 low
  prevData = newData;//store previous value
  newData = ADCH;//get value from A0
  if (prevData < 127 && newData >=127){//if increasing and crossing midpoint
    newSlope = newData - prevData;//calculate slope
    if (abs(newSlope-maxSlope)<slopeTol){//if slopes are ==
      //record new data and reset time
      slope[index] = newSlope;
      timer[index] = time;
      time = 0;
      if (index == 0){//new max slope just reset
        PORTB |= B00010000;//set pin 12 high
        noMatch = 0;
        index++;//increment index
      }
      
      else if (abs(timer[0]-timer[index])<timerTol && abs(slope[0]-newSlope)<slopeTol){//if timer duration and slopes match
        //sum timer values
        totalTimer = 0;
        for (byte i=0;i<index;i++){
          totalTimer+=timer[i];
        }
        period = totalTimer;//set period
        //reset new zero index values to compare with
        timer[0] = timer[index];
        slope[0] = slope[index];
        index = 1;//set index to 1
        PORTB |= B00010000;//set pin 12 high
        noMatch = 0;
      }
      else{//crossing midpoint but not match
        index++;//increment index
        if (index > 9){
          reset();
        }
      }
    }
    else if (newSlope>maxSlope){//if new slope is much larger than max slope
      maxSlope = newSlope;
      time = 0;//reset clock
      noMatch = 0;
      index = 0;//reset index
    }
    else{//slope not steep enough
      noMatch++;//increment no match counter
      if (noMatch>9){
        reset();
      }
    }
  }
    
  if (newData == 0 || newData == 1023){//if clipping
    PORTB |= B00100000;//set pin 13 high- turn on clipping indicator led
    clipping = 1;//currently clipping
  }
  
  time++;//increment timer at rate of 38.5kHz
  
  ampTimer++;//increment amplitude timer
  if (abs(127-ADCH)>maxAmp){
    maxAmp = abs(127-ADCH);
  }
  if (ampTimer==1000){
    ampTimer = 0;
    checkMaxAmp = maxAmp;
    maxAmp = 0;
  }
  
}

void reset(){//clea out some variables
  index = 0;//reset index
  noMatch = 0;//reset match couner
  maxSlope = 0;//reset slope
}


void checkClipping(){//manage clipping indicator LED
  if (clipping){//if currently clipping
    PORTB &= B11011111;//turn off clipping indicator led
    clipping = 0;
  }
}


void loop(){
  
  checkClipping();
  
  if (checkMaxAmp>ampThreshold){
    frequency = 76924/float(period);//calculate frequency timer rate/period
  
    //print results
    Serial.print(frequency);
    Serial.println(" hz");
  }
  
}

If I need to provide any more information, let me know, and I'll gladly post it. Any and all help would be appreciated, thanks.

The [u]analogRead() reference[/u] says you can sample up to about 10kHz, and that means you can't sample a signal above 5KHz. (Nyquist says you have to sample the top-half and the bottom-half of the wave at least once per cycle.)

Some choices are...

  • An external ADC.
  • A (analog) high-pass or band-pass filter followed by a peak detector, or voltage-follower, then DC into the Arduino. (Maybe not useful depending on what information you need.)
  • A different microcontroller.

I see. From what I had read, I was under the impression that you could sample up to 76.8 kHz by going around the analogRead() command (here's one of the sources I read that from Fast sampling from analog input - Yet Another Arduino Blog) unless I am misunderstand how those commands are being used.

At this point I'd like to avoid looking into another microcontroller (unless there is one with programming ready and available that would greatly simplify my problem).

As for an external ADC, I would definitely like to learn more about this option. Are there any external ADCs you'd recommend for this application? Sorry if this is a dumb question, I'm an ME who's just getting into this kind of thing.

For my problem, I just need to detect when a sound that has a frequency between 19 kHz and 24 kHz occurs. Would the second option you gave still be useful for this problem? I was planning to use a high pass filter since frequencies higher than 24 kHz are very unlikely in the environment I will be using this sensor in.

Thank you for the help and fast response.

For my problem, I just need to detect when a sound that has a frequency between 19 kHz and 24 kHz occurs. Would the second option you gave still be useful for this problem? I was planning to use a high pass filter since frequencies higher than 24 kHz are very unlikely in the environment I will be using this sensor in

Yes, that should work. [u]Here is a peak detector circuit[/u]. I've used a similar circuit several times to detect audio "loudness".

The big advantage is that you don't have to sample the audio waveform thousands of times per second. In my sound-activated lighting effects I sample about 10 times per second, freeing-up the processor to do other things. (I've never used a filter with it.)

I'm not exactly sure how fast it will respond with those particular components... It depends on how much current the op-amp can "pump" into the capacitor. If you have just a couple of cycles at 20kHz, the capacitor probably won't charge-up to the peak.

You can build analog filters with op-amps. No filter is perfect and a steep analog filter can be complicated. Oh... I just remembered that there are some switching/digital filter-chips and they are probably simpler to use.

P.S.
When I make an audio peak detector, I power it from a bipolar power supply so it can go down to zero volts. BTW - Putting the diode inside the feedback loop compensates for the 0.7V drop across the diode... With just a diode, low-level signals won't get through the diode.

And, if you're using a microphone you'll need a preamp because microphones only put-out a few millivolts, and with "normal" sounds the signal will be lower than that if you filter-out everything below 19kHz.

I think what you need to do is the use a simple analog bandpass filter that has corner frequencies of 19KHz and 25KHz which is then fed to an envelope detector (a.k.a. peak detector). The output of the envelope detector will show you (through a single analogRead() call), whether or not your signal has any substantial frequency components within the 19KHz-25KHz range.

This is much more accurate than using only an envelope detector and requires much less processing than DSP.

Chris_Force:
For my problem, I just need to detect when a sound that has a frequency between 19 kHz and 24 kHz occurs.

That requires sampling at something like 60kSPS or so, giving only 16us to read an ADC, run a multi-pole digital
bandpass filter and detect the resulting amplitude. That's fairly demanding.

Perhaps another way is to sample at 48kSPS, and invert the sign of every other sample, which will
invert the spectrum so that 24kHz maps to DC, 19kHz to 5kHz. A somewhat simpler low-pass
filter can then be used. Note that this approach will detect anything from 19kHz to 29kHz.

[ Actually a cheekier way is to sample at 43kSPS, invert alternate samples and low-pass
filter to a 2.5kHz bandwidth, which will only detect 19 to 24 range and is somewhat less
work ]