Cannot resolve low frequencies using ArduinoFFT

Hi folks,
I’m working on a little project about detecting the most dominant frequency when music is playing. Using an Arduino Uno with a MAX9814 mic, also tied a KY-037 with the same results.

In theory, everything should be working just fine as I’ve tested different parts of the code individually, but I’m starting to think that the issue I have is hardware related.

I use the ArduinoFFT library for frequency analysis and with 128 samples and a sampling frequency of 10kHz, which I thought to be about the limits of an Arduino Uno. While playing around with the frequency generator (found here: Online Tone Generator - generate pure tones of any frequency ), I’ve noticed that below ~160Hz the frequency readings become useless. With a 64 sample size that threshold is at around 330Hz, which makes sense, i guess.

My question now is whether I can tweak the code in a way to make it accurately calculate bass frequencies from 50-300+Hz, use a different library or get a faster microcontroller - and if so which one.

#include "arduinoFFT.h"
 
#define SAMPLES 128             //SAMPLES-pt FFT. Must be a base 2 number. Max 128 for Arduino Uno.
#define SAMPLING_FREQUENCY 10000 //Ts = Based on Nyquist, must be 2 times the highest expected frequency.

 
arduinoFFT FFT = arduinoFFT();

//FFT related variable definition
unsigned int sampling_period_us;
unsigned long microseconds;
double vReal[SAMPLES]; //create vector of size SAMPLES to hold real values
double vImag[SAMPLES]; //create vector of size SAMPLES to hold imaginary values

//definition of microphone properties (from MicSetup)
const int Mic = A1;       //Analog Input Pin on Arduino
const float AmpRange = 40;  //value from MicSetup

//additional variables
double sampleCopy[SAMPLES];
double Peak;
float AvgAmp;
float HighAmp;
float LowAmp;
float DerivHigh;
float DerivLow;
float DerivMax;

void setup() {

    Serial.begin(115200); //Baud rate for the Serial Monitor
    sampling_period_us = round(1000000*(1.0/SAMPLING_FREQUENCY)); //Period in microseconds 
    
}
  
void loop() {
  
    //FFT gets samples
    sampling();
    copySamples();
    Peak = hertz();
    
    //checking if highest 10% amplitude is outside thresholds
    AvgAmp = getAvgAmp();    
    bubbleSort(sampleCopy, SAMPLES);
    HighAmp = getHighAmp();
    LowAmp = getLowAmp();
    DerivHigh = HighAmp - AvgAmp;
    DerivLow = AvgAmp - LowAmp;
    DerivMax = max(DerivHigh, DerivLow);
    if(DerivMax < AmpRange){
          Peak = 0;
    }
    
    Serial.println("Frequency: ");
    Serial.println(Peak);
    
    //Serial.println();
    
    //delay(3000);
}

//Sampling
void sampling(){
    
    for(int i=0; i<SAMPLES; i++){  /*SAMPLING*/
        vReal[i] = analogRead(Mic);
        vImag[i] = 0;
        while(micros() - microseconds < sampling_period_us){
        microseconds += sampling_period_us;
        //Serial.println(vReal[i]); //debug
        }
    }
}

//copies samples for threshold check
void copySamples(){
  for(int i=0; i<SAMPLES; i++){
        sampleCopy[i] = vReal[i];
  }
}

//FFT calculates frequency
double hertz(){  

    double peak;
    FFT.Windowing(vReal, SAMPLES, FFT_WIN_TYP_HAMMING, FFT_FORWARD); /*FFT*/
    FFT.Compute(vReal, vImag, SAMPLES, FFT_FORWARD);
    FFT.ComplexToMagnitude(vReal, vImag, SAMPLES);
    peak = FFT.MajorPeak(vReal, SAMPLES, SAMPLING_FREQUENCY);
    peak = peak*0.825;
    
    return peak;
}

//calculates median amplitude
float getAvgAmp(){
  
    float sum = 0;
    float avg;
    for(int i = 0; i < SAMPLES; i++){
        sum = sum + sampleCopy[i];
    }
    return avg = sum/SAMPLES;
    
}

//sorts samples from low to high
void bubbleSort(double a[], int size) {
        for(int i=0; i<(size-1); i++) {
            for(int o=0; o<(size-(i+1)); o++) {
                if(a[o] > a[o+1]) {
                    int t = a[o];
                    a[o] = a[o+1];
                    a[o+1] = t;
                }
        }
    }
}

//gets median amplitude from highest 10% of samples
float getHighAmp(){
    
    int Highs = round(0.1*SAMPLES)+1;
    //Serial.println(Highs);
    float sum = 0;
    float Avg;
    for(int i = SAMPLES-1; i > SAMPLES-Highs-1; i--){
        sum = sum + sampleCopy[i];
    }
    return Avg = sum/Highs;
}

//gets median amplitude from lowest 10% of samples
float getLowAmp(){
    
    int Lows = round(0.1*SAMPLES)+1;
    //Serial.println(Lows);
    float sum = 0;
    float Avg;
    for(int i = 0; i < Lows; i++){
        sum = sum + sampleCopy[i];
    }
    return Avg = sum/Lows;
}

Code explanation:
The important part for this issue is the sampling and FFT part. I implemented the rest so the frequency will be set to zero if the amplitude is too low. While causing a significant drop in speed, this part should not influence the frequency calculation part as i have tried it without threshold-checking. I use a separate program to determine the amplitude threshold. I got the FFT sample code from an example on the internet, sadly I don’t know the exact source.

My understanding is that to resolve lower frequencies, I need a high sampling rate and ideally a lot of samples and bins. When a 100Hz tone is being played, the frequency output isn’t stable, but repetitive, which is due to the resolution limitations?

I’m a newbie when it comes to Arduino and sound analysis, although I have previous experience with coding.
Thanks in advance!

analogRead() will have a DC offset which will show up in the zeroth bin and probably bleed into the next bin or two due to windowing. You should take the average of the full sample array and then subtract that from each element after samplng and before computing the FFT.

My understanding is that to resolve lower frequencies, I need a high sampling rate and ideally a lot of samples and bins. When a 100Hz tone is being played, the frequency output isn't stable, but repetitive, which is due to the resolution limitations?

The FFT bin width, hence frequency resolution, is (SampleFrequency / NumberOfSamples). Thus you get higher frequency resolution by using a lower sample rate (subject to Nyquist limitations) or more samples (subject to available memory), so that's the trade space.

This is simple mathematics:

best frequency resolution = sample rate / FFT size.

You're using 10kSPS and 128 FFT bins, so resolution is 10000/128 = 78Hz, and allowing for
windowing spectral leakage, in practive your resolution will be quite a bit worse.

Another issue you have to worry about is aliasing - if you sample at 10kSPS and the music
contains a 10100Hz tone, it will simply alias to 100 Hz, mucking up your spectrum

Its best to sample at say 50kSPS, to avoid issues of aliasing altogether. You'll then need
something like 2048 point FFT to get a resolution like 30 to 50 Hz.

Basically you need enough memory for many more samples to do a decent job of this,
or to adopt another approach for the bass spectrum, such as decimating down to a lower
sample rate and then using an FFT with fewer points. However decimation involves digital
filtering to remove aliases as well.

From what I've seen here on the forum people trying to make a guitar tuner have better luck with Autocorrelation than FFT.

...And it still seems like most people are unsuccessful (in making a guitar tuner). :frowning:

MrMark:
analogRead() will have a DC offset which will show up in the zeroth bin and probably bleed into the next bin or two due to windowing. You should take the average of the full sample array and then subtract that from each element after samplng and before computing the FFT.

This ended up solving the problem for me. The extra code I used is the following:

//substracts the medium amplitude level from each sample
void valueConversion(float avgamp){
    
    for(int i=0; i<SAMPLES; i++){  /*SAMPLING*/
        vReal[i] = vReal[i] - avgamp;        
    }
}

I also got rid of the second sample array because of memory limitations. The threshold check is now just done with the highest and lowest analog value in each sample set.

MarkT:
Its best to sample at say 50kSPS, to avoid issues of aliasing altogether. You’ll then need
something like 2048 point FFT to get a resolution like 30 to 50 Hz.

Basically you need enough memory for many more samples to do a decent job of this,
or to adopt another approach for the bass spectrum, such as decimating down to a lower
sample rate and then using an FFT with fewer points. However decimation involves digital
filtering to remove aliases as well.

I switched to 50kSPS with a sample size of 128, which seems to work quite reliably so far. As I mainly want to resolve between 50-4000Hz, with Nyquist that would mean a minimum sampling frequency of 7900Hz. I tried using 8kSPS, but this just somehow resulted in more noise in the frequency readings.
So to get a resolution of 30-50Hz, 8-10kSPS should about be the sweet spot if I understood correctly.

Thank you for the tips!

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.