Using Arduino FFT library with ESP32

I'm using this example code. With this library GitHub - kosme/arduinoFFT: Fast Fourier Transform for Arduino · GitHub

#include <Arduino.h>
#include <arduinoFFT.h>

#define AUDIO_PIN     35       // ADC1 channel pin
#define SAMPLES       512      // Must be a power of 2
#define SAMPLING_FREQ 16000    // 16 kHz sampling (Nyquist limit = 8 kHz audio)

double vReal[SAMPLES];
double vImag[SAMPLES];

// Create the FFT object using standard ArduinoFFT configuration
ArduinoFFT<double> FFT = ArduinoFFT<double>(vReal, vImag, SAMPLES, SAMPLING_FREQ);

unsigned int sampling_period_us;
unsigned long microseconds;

void setup() {
  Serial.begin(115200);
  sampling_period_us = round(1000000 * (1.0 / SAMPLING_FREQ));
  analogSetAttenuation(ADC_11db); // Sets full-scale voltage range to ~3.3V
}

void loop() {
  // 1. Collect Samples
  for (int i = 0; i < SAMPLES; i++) {
    microseconds = micros();
    
    vReal[i] = analogRead(AUDIO_PIN);
    vImag[i] = 0; // Imaginary part is 0 for real audio input

    // Maintain precise timing interval
    while (micros() - microseconds < sampling_period_us) {
      // Empty loop to delay exactly to the target period
    }
  }

  // 2. Process FFT
  FFT.dcRemoval();                     // Removes the 1.65V DC bias offset
  FFT.windowing(FFTWindow::Hamming, FFTDirection::Forward); // Smooths the signal edges
  FFT.compute(FFTDirection::Forward);  // Calculates FFT
  FFT.complexToMagnitude();            // Computes magnitude vectors
  // 3. Print out to Serial Monitor (Visualizer Output)
  // We only look at the first SAMPLES/2 bins (0Hz to 8kHz) due to Nyquist frequency
  for (int i = 2; i < (SAMPLES / 2); i++) { 
    // Calculate frequency for current bin
    double frequency = (i * SAMPLING_FREQ) / SAMPLES;
    
    // Simple filter to suppress background noise floor
    if (vReal[i] > 100) { 
      Serial.print((int)frequency);
      Serial.print("Hz: ");
      
      // Draw ASCII bar relative to frequency amplitude
      int barLength = map(vReal[i], 0, 5000, 0, 40); 
      for (int j = 0; j < barLength; j++) {
        Serial.print("|");
      }
      Serial.println();
    }
  }
  
  Serial.println("--- Frame ---");
  delay(2000); // Small delay to make the Serial Monitor readable
}

I previously assumed it was one of the examples provided by the library, but it doesn't seem to be. I'm not sure where I got it from, sorry for not keeping better notes. I'm building that code with VS Code + PlatformIO.

I'm feeding this 1kHz sine wave into GPIO 35, as observed on my PicoScope

The red trace is trace B. I'm not showing trace A because that's for the signal generator that's feeding a signal into the opamp that's producing the red trace. That opamp is running off the ESP32's 3.3V pin for power. It's a MCP6022-I/P. The output of the opamp clips at just above 0V and just below 3.3V (where the horizontal cursors are) and it's DC coupled into the ESP32 ADC pin, so I think it can't damage the ESP32.

My question is whether this is known / expected behaviour for the example with that library.

--- Frame ---
62Hz: ||||||||
93Hz: |||||
343Hz: 
1281Hz: |||
1312Hz: ||||
1343Hz: ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
1375Hz: ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
1406Hz: ||||||||||||||||||||||||||||||||||||||||||
1437Hz: ||||
1468Hz: ||
2656Hz: 
2687Hz: 
2718Hz: ||||||||||||||||||||
2750Hz: |||||||||||||||||||||||||||||||||
2781Hz: |||||||||
2812Hz: |
4031Hz: |
4062Hz: |
4093Hz: |||||
4125Hz: |||||||
4156Hz: ||
4187Hz: |
5468Hz: ||
5500Hz: |||
5562Hz: 
6375Hz: |
6406Hz: |
6843Hz: |||
6875Hz: |||
--- Frame ---

I'm not concerned about the signal in the 62Hz / 93Hz bins, as it's just a lash up and it's probably just mains hum being picked up.

What surprised me is how off the main peak at 1375Hz is, for 1kHz input. I suspect it's due to ADC sampling timing. I was just posting in case someone who sees this knows it's a known problem, and may be able to suggest a better way to get more accurate FFT frequency results. I did have a quick scan though FFT related posts on this forum, but didn't see anything for how to best do ADC sampling for the FFT library with an ESP32.

I know the ESP32 ADC has its issues with non linearity and dead spots close to 0V and close to 3.3V. Perhaps that's contributing to the harmonics of 1375Hz seen in the output. I'm not bothered about those harmonics ATM. I just want to get the main peak to show closer to 1KHz.

This is just for a 3 channel (bass, mid, treble) light organ, so it doesn't need to be doing high quality audio processing.

I commented out the ADC stuff, made some minor changes so it would show the entire output and generated a 1000Hz sinewave in the program. This is what it output

--- Frame ---
0Hz: 
125Hz: 
250Hz: 
375Hz: 
500Hz: 
625Hz: 
750Hz: 
875Hz: ||||||||||||||
1000Hz: ||||||||||||||||||||||||||||||||||
1125Hz: ||||||||||||||
1250Hz: 
1375Hz: 
1500Hz: 
1625Hz: 
1750Hz: 
1875Hz: 
2000Hz: 
2125Hz: 
2250Hz: 
2375Hz: 
2500Hz: 
.........

Jim, thank you very much for spending time on this. This confirms, as I suspected, that my incorrect frequency peak isn't due to the FFT library. It's got to be something dodgy with the method used to decide when it's time to collect the next sample.
Since my post from yesterday, I've done a bit more research and have an alternative solution for collecting data from the ADC, avoiding the busy waiting on micros(). I'll report back with the results, but first I want to build the analogue part on protoboard. I'm getting fed up with intermittent connections on the breadboard lash up.

Your FFT is fine, your sample rate isn't. A 1 kHz tone landing at ~1375 Hz means you're really sampling at about 11.6 kHz, so each loop iteration (almost certainly analogRead()) takes ~86 µs, not 62.5 µs, and the busy-wait never actually waits. Time the whole 512-sample loop with micros() and you'll see it. Either feed the measured rate into the FFT, lower SAMPLING_FREQ to something analogRead can keep up with, or use the continuous/DMA ADC (analogContinuous() on core 3.x, I2S-ADC mode on the 2.x core PlatformIO ships) for a true 16 kHz. The ~2718 Hz peak is just a harmonic from the clipping.

Yes.
I'm not sure how to do it on an ESP but you need to setup a timer with interrupts to start an ADC conversion.

Thanks for your explanation. That makes total sense. The last time I needed to use an FFT library was ~35 years ago when I was working on the signal processing part of a passive SONAR. So I'm well rusty with FFTs.

I think I'll start by timing an analogRead(). I can add code to set/clear a spare GPIO, with nothing in-between, and see what the overhead is, by looking on a scope.
Then I'll stick an analogRead() between the set/clear and see how long that takes, minus the previously measured overhead.
Then I'll time the whole 512 sample loop with a set/clear around the loop.
Because FreeRTOS is running by default on the ESP32, and loop() is just a FreeRTOS task, I'd like to confirm it's not stealing time from loop(). I can't think what might cause that in the example code, but I'd like to confirm nothing is.
Hopefully, doing the above measurements will show that the time taken to grab 512 samples is close to the time taken to grab one sample x 512.
I'll report back here when I've done those tests. It might be a few days because first I plan to replace the breadboard hardware with a soldered protoboard version.

I'm not sure what you are trying to do with the FFT but I don't think analogRead will work at 16K, however, analogContinuous should.
ADC - - — Arduino ESP32 latest documentation.

Don't forget to include an antialiasing filter in your hardware.

Thanks for the tip / link.

I won't. I'm currently thinking of an RC filter on the input to the opamp, and a capacitor in parallel with the opamp feedback resistor. I've not yet decided on the best cut off frequency to get enough of the treble and to not have any noticeable aliasing. ATM I'm thinking perhaps 4kHz. I'll experiment and see what gives acceptable visual results.
I don't want to add another opamp package, specifically to implement the filter, due to being short of space. I'm using both opamps in the one package already. First stage has gain 100, which works well with an electret capsule for relatively loud music, and the second stage has an additional gain of 10, giving 1000 total which is sensitive enough for relatively quiet music. I'll use 2 ESP32 ADC channels and the software will decide which to use. e.g. if the x1000 input is clipping it will use the x100.

I'll be interested in the final outcome when you are finished

Not the final outcome, but this is what I'm going to try.


I've just added the filters with the components (3 Cs and 1 R) whose reference numbers are > 100 and marked with a red dash. Previously pin 7 of the opamp went directly to ESP32 G32 ADC pin.
I'm (obviously?) not a specialist in analogue design. I did a BSc in Electronics in the 70s. Then worked in digital hardware design for a few years before switching to embedded software until I retired. Now I do microcontroller code and hardware design as a hobby.
I was able to draft the basic circuit diagram for this part with no help, but then tried chatting to AI about it and got some tips which seemed to be reasonable. e.g. AI suggested this filter arrangement that I've just added.
The tbd resistor values for the 1.65V dividers are just in case I need to tune the offset so that the opamp doesn't clip at one rail before the other. I've left space for them on the protoboard, but IDK yet if I'll use them.

I realised something about this. I'm just splitting the output of the FFT into 3 bands (bass, middle, treble) I should have a filter to significantly attenuate everything above 1/2 the sampling rate. So sampling at let's say 8kHz for now, if a significant amount of 9kHz got through the filter, it would appear as 7kHz in the FFT output. Now normally that would be a bad thing. However, in this case, the 9KHz would be aliased back into the treble band. So long as nothing from the high treble range past 1/2 the sampling frequency can be aliased back in to the middle or bass bands, maybe that's okay. It needs more thought.

This may be of interest:

I've seen other people on the forum use it.

Thanks for letting me know about that. It is of interest, but it's a bit late for me to change tack now. I'm interested / looking forward to doing some DSP on the ESP32 for the first time. I think that learning about ADC DMA, max sample rates, etc will be useful in the future perhaps. I've also spent a few days finalising the board layout, case design, and even gathered all the components ready to start soldering. If I binned the current design now, it would feel like a failure. I'm not necessarily after the easiest / cheapest way to do it. I'll probably get more enjoyment from the learning, design, build and test process than I will from having the unit finished to play with.