In this tutorial you will learn how to build a simple Frequency Spectrum Analyzer with an ESP32. We are going to use the MAX4466 microphone and the arduinoFFT library to measure the frequencies in the audio signal and display the frequency spectrum on an OLED.
Let’s get started with the parts you will need for this project.
Required Parts
You will need an ESP32, and OLED screen and the MAX4466 Microphone Module. I am using the ES32 lite as a microprocessor, since it is has a battery charging interface and you could run the Spectrum Analyzer then easily on a battery to make it portable. But any other ESP32 will work as well.
ESP32 lite
USB Data Cable
MAX4466 Microphone
OLED Display
Dupont Wire Set
Breadboard
Makerguides is a participant in affiliate advertising programs designed to provide a means for sites to earn advertising fees by linking to Amazon, AliExpress, Elecrow, and other sites. As an Affiliate we may earn from qualifying purchases.
Frequency Spectrum Analyzer
A Frequency Spectrum Analyzer is a device that visualizes and analyzes the spectral content of signals and shows the amplitude or power of a signal across its frequency range. It essentially tells you how much power of the signal is distributed over specific frequencies or frequency bands.
The following picture shows such a frequency spectrum. The frequency range is on the x-axis and the magnitude of the signal is measured in Decibel as shown on the y-axis:
Depending on the accuracy and the frequency range, Spectrum Analyzers can be very expensive. Below a picture of the Siglent Technologies SSA3021X Spectrum Analyzer that can analyze radio frequencies from 9 kHz to 2.1 GHz.
Audio Frequency Spectrum
A common application of Spectrum Analyzers is to visualize the frequency distribution within an audio signal. The frequency or hearing range of an audio signal that humans can hear ranges from 20 to 20,000 Hz (=20KHz). However, those are the extremes and the hearing range decreases with age. For adults the upper limit is typically around 15000 Hz. If you want to know your upper limit, you can try this little hearing test.
The following picture shows the display of a typical Audio Spectrum Analyzer. You can see the different frequency bands (green bars) ranging from 20Hz to 20KHz.
In this tutorial, we are going to build such an Audio Spectrum Analyzer ourselves. However, don’t expect too much. Since we are using a cheap ESP32, a tiny OLED and a simple MAX4466 microphone our Spectrum Analyzer will not be very accurate.
It’s frequency range will be only from 125Hz to 8000 Hz and we will only have 7 frequency bands, but it will be good enough to visualize and animate the frequency content of music.
Workings of a Spectrum Analyzer
How does our little Audio Spectrum Analyzer work. The microphone converts sound waves into electrical impulses. These weak impulses are amplified by the MAX4466 and fed into the Analog Digital Converter (ADC) of the ESP32. If you read the output of the ADC and plot it, the detected audio signal might look like this:
On the x-axis is the time and on the y-axis is the amplitude of the signal. This signal is called a Time domain signal, since it is reported over time but it does not show the frequencies within the signal.
The signal above is actually composed of cosine signals with the frequencies 10, 20, 30, 40 and 50 Hz with increasing amplitudes. If we apply a mathematical method named Fast Fourier Transformation (FFT), we can extract these frequencies from the signal. The plot below shows the same signal after the FFT was applied:
The signal is now in the Frequency Domain with frequency on the x-axis (instead of time). You can clearly see the peaks at 10, 20, 30, 40 and 50 Hz, which reveal the frequencies the input signal is composed of. To perform the FFT we let the ESP32 sample the audio signal retrieved from the ADC and then call the arduinoFFT library to extract the frequencies. The resulting frequency spectrum is then displayed on the OLED.
However, we will not display the individual frequencies in the spectrum but group them into frequency bands that are easier to read and commonly used in Audio Spectrum Analyzers. The frequency bands will be around 125, 250, 500, 1000, 2000, 4000 and 8000 Hz, as shown below:
MAX4466 Microphone Module
The MAX4466 Microphone module is a breakout board with a 20-20KHz electret microphone and the MAX4466 preamplifier IC. On the back of the board is a small trimmer that allows you to adjust the gain from 25x to 125x. The picture below shows the front and the back of the board:
The module runs on 2.4…5.5V for VCC with a very low quiescent power supply current of <24μA. The output will have a bias of VCC/2. So when it’s perfectly quiet, the output voltage will be at VCC/2 V.
If you want to learn more about the MAX4466 Microphone Module have a look at the Detect sound with MAX4466 and Arduino tutorial.
Connecting MAX4466 and OLED to ESP32
In this section we first connect the MAX4466 microphone module to the ESP32 and test its function. Then we add the OLED to the circuit and write the code for the Spectrum Analyzer:
Connecting MAX4466 to ESP32
To connect the MAX4466 to an ESP32, start by connecting the ground (GND) of the ESP32 and the MAX4466 module (blue wire). Then connect VCC of the MAX4466 to the 3.3V output of the ESP32 (red wire). Finally, connect GPIO 4 of the ESP32 to the OUT pin of the MAX4466 (orange wire):
Note the instead of GPIO 4 you can use any other pin as long as it can read an analog signal.
Test code for MAX4466
After connecting the MAX4466 microphone to the ESP32 let’s run a little test to see if everything is working as expected. The following code reads the microphone signal on pin 4 and prints it to the Serial Monitor:
const byte micPin = 4; void setup() { Serial.begin(115200); pinMode(micPin, INPUT); } void loop() { int value = analogRead(micPin); Serial.println(value); delay(100); }
If you upload this code, open the Serial Plotter and make some noise, you should be able to see fluctuations in the audio signal related to the volume of the noise:
If you don’t see any change in the audio signal then either your wiring is wrong, or the GPIO pin for the microphone in the code is incorrect.
Adding OLED to ESP32
In the next step we add the OLED to the ESP32. First, connect GND and VCC of the OLED to the existing power supply lines (blue and red wires). Then connect SCL to pin 23 and SDA to pin 18 of the ESP32, as shown below:
GPIO 23 and 18 are the hardware I2C pins for the ESP32 lite. If you use a different ESP32 board your pins for hardware I2C may differ. If you are not sure, which pins to use, have a look at the Find I2C and SPI default pins tutorial. The picture below shows the completed circuit on a breadboard:
Code to display Frequency Spectrogram
Before we can write the code to compute and display the Frequency Spectrogram, we need to install the arduinoFFT library. Open the LIBRARY MANGER, type “arduinoFFT” in the search bar and press the INSTALL
button. After a successful installation you should see the following:
Furthermore, we will need to install the Adafruit_SSD1306 Library to control the OLED. Type “Adafruit_SSD1306” in the search bar and as before press INSTALL
to install the library:
With that out of the way, we can now start to write the code. The following code for an audio spectrum visualizer reads audio input from a microphone, performs a Fast Fourier Transform (FFT) to analyze the frequency spectrum, and displays the results on an OLED. The following picture shows the output it creates and the meaning of the visual elements:
In the next sections we will look at this code function by function. The code is based on the ESP32_FFT_VU example but has been broken into smaller functions and slightly modified for readability.
#include "arduinoFFT.h" #include "Adafruit_SSD1306.h" const int micPin = 4; const int volume = 300; // Adjust depending on audio volume const int noise = 2000; // Noise filter, smaller signal is ignored const int nSamples = 256; // Must be a power of 2 const int sampleFreq = 40000; // Hz, 40000 or less double vReal[nSamples]; double vImag[nSamples]; byte peaks[] = { 0, 0, 0, 0, 0, 0, 0 }; ArduinoFFT<double> FFT = ArduinoFFT<double>(vReal, vImag, nSamples, sampleFreq); Adafruit_SSD1306 oled(128, 64, &Wire, -1); void displayScale() { oled.clearDisplay(); oled.setCursor(0, 0); oled.print(".1 .2 .5 1K 2K 4K 8K"); } void displayBands() { for (int i = 2; i < (nSamples / 2); i++) { if (vReal[i] > noise) { if (i <= 2) displayBand(0, vReal[i]); // 125Hz if (i > 3 && i <= 5) displayBand(1, vReal[i]); // 250Hz if (i > 5 && i <= 7) displayBand(2, vReal[i]); // 500Hz if (i > 7 && i <= 15) displayBand(3, vReal[i]); // 1000Hz if (i > 15 && i <= 30) displayBand(4, vReal[i]); // 2000Hz if (i > 30 && i <= 53) displayBand(5, vReal[i]); // 4000Hz if (i > 53 && i <= 200) displayBand(6, vReal[i]); // 8000Hz if (i > 200) displayBand(7, vReal[i]); // 16000Hz } } } void calcFFT() { FFT.windowing(vReal, nSamples, FFT_WIN_TYP_HAMMING, FFT_FORWARD); FFT.compute(vReal, vImag, nSamples, FFT_FORWARD); FFT.complexToMagnitude(vReal, vImag, nSamples); } void sampleAudio() { static unsigned long newTime, oldTime; long period_us = round(1000000.0 / sampleFreq); for (int i = 0; i < nSamples; i++) { newTime = micros() - oldTime; oldTime = newTime; vReal[i] = analogRead(micPin); vImag[i] = 0; while (micros() < (newTime + period_us)) ; } } void displayPeaks() { for (byte band = 0; band <= 6; band++) { oled.drawFastHLine(18 * band, 64 - peaks[band], 14, WHITE); } } void decayPeaks() { if (millis() % 4 == 0) { for (byte band = 0; band <= 6; band++) { peaks[band] = max(0, peaks[band] - 1); } } } void displayBand(int band, double magnitude) { int dmax = 50; int dsize = min((int)(magnitude / volume), dmax); if (band == 7) { oled.drawFastHLine(18 * 6, 0, 14, WHITE); } for (int s = 0; s <= dsize; s += 2) { oled.drawFastHLine(18 * band, 64 - s, 14, WHITE); } if (dsize > peaks[band]) { peaks[band] = dsize; } } void setup() { oled.begin(SSD1306_SWITCHCAPVCC, 0x3C); oled.setTextSize(1); oled.setTextColor(WHITE); pinMode(micPin, INPUT); } void loop() { displayScale(); sampleAudio(); calcFFT(); displayBands(); displayPeaks(); decayPeaks(); oled.display(); delay(1); }
Libraries
The code starts by including the required libraries. There is the arduinoFFT
library to compute the Fast Fourier Transformation (FFT) and the Adafruit_SSD1306
libary, which is needed to control the OLED:
#include "arduinoFFT.h" #include "Adafruit_SSD1306.h"
Constants
We then define some constants. micPin
defines the GPIO pin the MAX4466 output is connected to. The volume
constant allows you to adjust the magnitude of the displayed frequency spectrum according to the audio volume. If you don’t see any bars, lower this value and if the bars are very large, even for weak sound, increase this value.
Similarly, the noise constant allows you to filter out low level noise. If you see too much noise in the frequency spectrum (random fluctuation even if there is no sound) then increase this value.
nSamples
defines how many audio samples are taken and sampleFreq
is the sampling frequency. For microcontrollers slower than the ESP32 you may have to lower these values.
const int micPin = 4; const int volume = 300; // Adjust depending on audio volume const int noise = 2000; // Noise filter, smaller signal is ignored const int nSamples = 256; // Must be a power of 2 const int sampleFreq = 40000; // Hz, 40000 or less
Variables
There are three variables. vReal
and vImag
are arrays that store the real and imaginary part of the signal and the peaks
array keeps track of the maximum magnitudes for each frequency bar.
double vReal[nSamples]; double vImag[nSamples]; byte peaks[] = { 0, 0, 0, 0, 0, 0, 0 };
Objects
Finally we have the objects. The FFT
object holds the functions needed to compute the Fast Fourier Transformation, and the oled
object holds the functions to control the OLED display.
ArduinoFFT<double> FFT = ArduinoFFT<double>(vReal, vImag, nSamples, sampleFreq); Adafruit_SSD1306 oled(128, 64, &Wire, -1);
displayScale
The displayScale()
function prepares the OLED display by clearing any previous content and printing labels for frequency bands. This helps the viewer understand which band corresponds to which frequency. The function uses this code:
void displayScale() { oled.clearDisplay(); oled.setCursor(0, 0); oled.print(".1 .2 .5 1K 2K 4K 8K"); }
The displayBands()
function processes the FFT result stored in the vReal
array and decides which frequency band each FFT bin falls into. It filters out signals below a certain noise threshold and then calls displayBand()
with the index of the band and the signal’s magnitude:
void displayBands() { for (int i = 2; i < (nSamples / 2); i++) { if (vReal[i] > noise) { if (i <= 2) displayBand(0, vReal[i]); if (i > 3 && i <= 5) displayBand(1, vReal[i]); if (i > 5 && i <= 7) displayBand(2, vReal[i]); if (i > 7 && i <= 15) displayBand(3, vReal[i]); if (i > 15 && i <= 30) displayBand(4, vReal[i]); if (i > 30 && i <= 53) displayBand(5, vReal[i]); if (i > 53 && i <= 200) displayBand(6, vReal[i]); if (i > 200) displayBand(7, vReal[i]); } } }
The calcFFT()
function performs the actual FFT analysis. It applies a Hamming window to smooth the signal and then computes the FFT, transforming the real and imaginary parts into magnitudes stored back in vReal
:
void calcFFT() { FFT.windowing(vReal, nSamples, FFT_WIN_TYP_HAMMING, FFT_FORWARD); FFT.compute(vReal, vImag, nSamples, FFT_FORWARD); FFT.complexToMagnitude(vReal, vImag, nSamples); }
The sampleAudio()
function collects nSamples
worth of analog data from the microphone. It reads the signal at regular intervals, calculated using sampleFreq
, and stores it in the vReal
array. vImag
is set to zero since the input is purely real:
void sampleAudio() { static unsigned long newTime, oldTime; long period_us = round(1000000.0 / sampleFreq); for (int i = 0; i < nSamples; i++) { newTime = micros() - oldTime; oldTime = newTime; vReal[i] = analogRead(micPin); vImag[i] = 0; while (micros() < (newTime + period_us)) ; } }
The displayPeaks()
function shows persistent peak values of each frequency band by drawing short horizontal lines where the peak last occurred. These lines help visualize maximum recent volume levels in each band:
void displayPeaks() { for (byte band = 0; band <= 6; band++) { oled.drawFastHLine(18 * band, 64 - peaks[band], 14, WHITE); } }
The decayPeaks()
function slowly reduces the peak value over time, simulating a decay effect. It runs roughly every 4 milliseconds and lowers each band’s peak by 1, down to a minimum of 0:
void decayPeaks() { if (millis() % 4 == 0) { for (byte band = 0; band <= 6; band++) { peaks[band] = max(0, peaks[band] - 1); } } }
The displayBand()
function draws vertical bars for each band on the OLED. It scales the bar size based on the signal’s magnitude divided by the volume
parameter. If the band index is 7, a special bar is drawn at the top of the screen. The function also updates the peaks
array if the current bar exceeds the previous peak:
void displayBand(int band, double magnitude) { int dmax = 50; int dsize = min((int)(magnitude / volume), dmax); if (band == 7) { oled.drawFastHLine(18 * 6, 0, 14, WHITE); } for (int s = 0; s <= dsize; s += 2) { oled.drawFastHLine(18 * band, 64 - s, 14, WHITE); } if (dsize > peaks[band]) { peaks[band] = dsize; } }
The setup()
function initializes the OLED screen and sets the microphone pin as an input. This prepares the system to begin sampling and displaying audio data:
void setup() { oled.begin(SSD1306_SWITCHCAPVCC, 0x3C); oled.setTextSize(1); oled.setTextColor(WHITE); pinMode(micPin, INPUT); }
Finally, the loop()
function is the main execution cycle. It repeatedly performs all necessary steps to capture audio, process it through FFT, visualize the data, and update the OLED screen:
void loop() { displayScale(); sampleAudio(); calcFFT(); displayBands(); displayPeaks(); decayPeaks(); oled.display(); delay(1); }
Together, these functions form a complete loop that continuously analyzes audio input and displays a visual spectrum on the OLED display.
Test Tones
You can test the Spectrum Analyzer by playing some test tones. Here are links to Youtube videos that play tones with specific frequencies:
The Spectrum Analyzer should shown a larger/growing bar for the frequency band the test tone falls into. For instance, for a 2000 Hz tone you should see a larger bar in the 2KHz frequency band:
The height of the bar will depend on the volume. You probably will have to adjust the volume
constant or the gain control
for the MAX4466 to get a nice signal. You can find the potentiometer to adjust the gain at the back of the MAX4466 module:
Conclusions
In this tutorial you learned how to build a Frequency Spectrum Analyzer using the MAX4466 microphone module, an OLED and an ESP32.
If you want to improve the accuracy and frequency range of the Spectrum Analyzer, you could use an external Analog Digital Converter for higher resolution and an I2C Microphone for a better range.
Similarly, if you want a bigger display and colors, you could replace the OLED by a TFT screen. Have a look at the Interface TFT ILI9341 Touch Display with ESP32 and maybe the How to configure TFT_eSPI Library for TFT display tutorials.
The ESP32 lite board has a LiPo battery interface, which makes it easy to run the Spectrum Analyzer on a 4.2V LiPo battery. However, the board is a bit large, especially when compared to the tiny OLED we used. Instead of the ESP32 lite, you could use an ESP32-C SuperMini to build a very small, cute and portable Spectrum Analyzer.
Happy Tinkering ; )
Links
In the following some links I found useful when writing this tutorial:
- arduinoFFT Examples
- esp32-mic-fft
- ESP32_FFT_VU
- ESP32-8266-Audio-Spectrum-Display
- ESP32_Music_Spectrum_Analyzer
- ESP32 spectrum analyser VU meter using arduinoFFT and a FastLED matrix
Stefan is a professional software developer and researcher. He has worked in robotics, bioinformatics, image/audio processing and education at Siemens, IBM and Google. He specializes in AI and machine learning and has a keen interest in DIY projects involving Arduino and 3D printing.