Mirko Pavleski
Published

How to Build a Simple Audio Spectrum Analyzer

Simple Audio Spectrum Analyzer with multiple adjustable display, speed, and sensitivity modes for a customizable audio visualization

BeginnerFull instructions provided2 hours22
How to Build a Simple Audio Spectrum Analyzer

Things used in this project

Hardware components

LGT8F328P MCU Board
×1
128x64 LCD Display (ST7920 chip)
×1
Resistor 10k ohm
Resistor 10k ohm
×1
Resistor 4.75k ohm
Resistor 4.75k ohm
×1
Trimmer Potentiometer, 10 kohm
Trimmer Potentiometer, 10 kohm
×1
Pushbutton Switch, Momentary
Pushbutton Switch, Momentary
×3

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
Solder Wire, Lead Free
Solder Wire, Lead Free

Story

Read more

Schematics

Schematic

...

Code

Code

C/C++
...
#define AUTO_GAIN 0       // Auto volume adjustment (disabled for manual control)
#define VOL_THR 25        // Silence threshold (no display on matrix below this)
#define LOW_PASS 20       // Lower sensitivity threshold for noise (no jumps when no sound)
#define DEF_GAIN 80       // Default maximum threshold (ignored when GAIN_CONTROL is active)
#define FHT_N 256         // Spectrum width x2
#define LOG_OUT 1
#define PEAK_HOLD_TIME 2000     // Peak hold time in ms

// Button pins
#define BUTTON1 8
#define BUTTON2 9  
#define BUTTON3 10

// Manually defined array of tones, first smooth, then steeper
byte posOffset[16] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; // 1500 Hz
//byte posOffset[16] = {1, 2, 3, 4, 6, 8, 10, 13, 16, 20, 25, 30, 35, 40, 45, 50}; // 4000 Hz

#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))

#include <Wire.h>
#include <U8glib.h>  // http://rcl-radio.ru/wp-content/uploads/2023/04/U8glib.zip
#include <FHT.h>     // http://forum.rcl-radio.ru/misc.php?action=pan_download&item=297&download=1

#define EN 6
#define RW 5
#define CS 4

//U8GLIB_SH1106_128X64 lcd(U8G_I2C_OPT_DEV_0|U8G_I2C_OPT_FAST);  // Dev 0, Fast I2C / TWI
U8GLIB_ST7920_128X64_1X lcd(EN, RW, CS); // serial use, PSB = GND

byte gain = DEF_GAIN;   
unsigned long gainTimer, times;
byte maxValue, maxValue_f;
float k = 0.1;
byte ur[16], urr[16];

// Button state variables
bool button1State = false;
bool button2State = false;
bool button3State = false;
unsigned long button1Time = 0;
unsigned long button2Time = 0;
unsigned long button3Time = 0;

// Mode variables
byte displayMode = 0;           // 0=normal, 1=peak hold, 2=falling dots, 3=symmetrical
byte speedMode = 0;             // 0=normal, 1=fast, 2=slow
byte sensitivityMode = 0;       // 0=normal, 1=high, 2=low
byte peakHold[16];              // Peak hold values for each band
unsigned long peakTimer[16];    // Timer for peak decay

void setup() {
  delay(100); 
  sbi(ADCSRA, ADPS2);
  cbi(ADCSRA, ADPS1);
  sbi(ADCSRA, ADPS0);
  Serial.begin(9600);
  Wire.begin(); 
  Wire.setClock(800000L);
  lcd.begin();
  // lcd.setRot180();
  lcd.setFont(u8g_font_profont11r);
  analogReadResolution(10); // ADC 10 BIT
  analogReference(INTERNAL1V024);
  pinMode(A0, INPUT); // INPUT AUDIO
  
  // Initialize button pins
  pinMode(BUTTON1, INPUT_PULLUP);
  pinMode(BUTTON2, INPUT_PULLUP);
  pinMode(BUTTON3, INPUT_PULLUP);
  
  // Initialize peak hold array
  for(int i = 0; i < 16; i++) {
    peakHold[i] = 0;
    peakTimer[i] = 0;
  }
}

void handleButtons() {
  // Button 1 - Display Mode Cycle
  if (digitalRead(BUTTON1) == LOW) {
    if (millis() - button1Time > 300) { // Debounce
      displayMode = (displayMode + 1) % 4; // Cycle through 4 modes
      button1Time = millis();
    }
  }
  
  // Button 2 - Speed Mode Cycle  
  if (digitalRead(BUTTON2) == LOW) {
    if (millis() - button2Time > 300) {
      speedMode = (speedMode + 1) % 3; // Cycle through 3 speed modes
      button2Time = millis();
    }
  }
  
  // Button 3 - Sensitivity Cycle
  if (digitalRead(BUTTON3) == LOW) {
    if (millis() - button3Time > 300) {
      sensitivityMode = (sensitivityMode + 1) % 3; // Cycle through 3 sensitivity modes
      button3Time = millis();
      
      // Adjust gain based on sensitivity
      switch(sensitivityMode) {
        case 0: gain = DEF_GAIN; break;    // Normal
        case 1: gain = DEF_GAIN / 2; break; // High sensitivity
        case 2: gain = DEF_GAIN * 2; break; // Low sensitivity
      }
    }
  }
}

void updatePeakHold() {
  for (int i = 0; i < 16; i++) {
    int posLevel = map(fht_log_out[posOffset[i]], LOW_PASS, gain, 0, 60);
    posLevel = constrain(posLevel, 0, 60);
    
    if (posLevel > peakHold[i]) {
      peakHold[i] = posLevel;
      peakTimer[i] = millis();
    } else if (millis() - peakTimer[i] > PEAK_HOLD_TIME) {
      if (peakHold[i] > 0) peakHold[i]--;
    }
  }
}

void drawModeIndicators() {
  // Display mode indicators at top right
  lcd.setFont(u8g_font_04b_03);
  
  // Display mode indicator (N, P, D, S)
  char modeChar = 'N';
  switch(displayMode) {
    case 0: modeChar = 'N'; break; // Normal
    case 1: modeChar = 'P'; break; // Peak
    case 2: modeChar = 'D'; break; // Dot
    case 3: modeChar = 'S'; break; // Symmetrical
  }
  
  // Speed mode indicator (N, F, S)
  char speedChar = 'N';
  switch(speedMode) {
    case 0: speedChar = 'N'; break; // Normal
    case 1: speedChar = 'F'; break; // Fast
    case 2: speedChar = 'S'; break; // Slow
  }
  
  // Sensitivity indicator (N, H, L)
  char sensChar = 'N';
  switch(sensitivityMode) {
    case 0: sensChar = 'N'; break; // Normal
    case 1: sensChar = 'H'; break; // High
    case 2: sensChar = 'L'; break; // Low
  }
  
  // Draw all three indicators at top right
  lcd.drawStr(100, 5, String(modeChar).c_str());
  lcd.drawStr(110, 5, String(speedChar).c_str());
  lcd.drawStr(120, 5, String(sensChar).c_str());
}

void drawSpectrum() {
  lcd.firstPage();  
  do {
    for (int pos = 0; pos < 128; pos += 8) {
      int band = pos / 8;
      int posLevel = map(fht_log_out[posOffset[band]], LOW_PASS, gain, 0, 60);
      posLevel = constrain(posLevel, 0, 60);
      
      if(millis() - times < 2000) { 
        posLevel = 60; // Startup animation
      }
      
      urr[band] = posLevel;
      
      // Apply speed mode to falling effect
      int fallSpeed = 1;
      switch(speedMode) {
        case 0: fallSpeed = 1; break; // Normal
        case 1: fallSpeed = 3; break; // Fast fall
        case 2: fallSpeed = 1; if(random(2) == 0) fallSpeed = 0; break; // Slow/random
      }
      
      if(urr[band] < ur[band]) {
        ur[band] = max(ur[band] - fallSpeed, 0);
      } else {
        ur[band] = posLevel;
      }
      
      delayMicroseconds(200);
      
      // Draw based on display mode
      switch(displayMode) {
        case 0: // Normal bars
          for (int v_pos = 0; v_pos < ur[band] + 4; v_pos += 4) {
            lcd.drawBox(pos, 61 - v_pos, 6, 2);
          }
          break;
          
        case 1: // Peak hold with bars
          for (int v_pos = 0; v_pos < ur[band] + 4; v_pos += 4) {
            lcd.drawBox(pos, 61 - v_pos, 6, 2);
          }
          // Draw peak dots
          if(peakHold[band] > 0) {
            lcd.drawBox(pos + 1, 61 - peakHold[band], 4, 1);
          }
          break;
          
        case 2: // Falling dots
          for (int v_pos = 0; v_pos < ur[band]; v_pos += 4) {
            lcd.drawBox(pos + 1, 61 - v_pos, 4, 1);
          }
          break;
          
        case 3: // Symmetrical mode
          for (int v_pos = 0; v_pos < ur[band] + 4; v_pos += 4) {
            lcd.drawBox(pos, 61 - v_pos, 6, 2);
            lcd.drawBox(pos, 3 + v_pos, 6, 2); // Mirror at top
          }
          break;
      }
    }
    
    // Draw mode indicators at top right
    drawModeIndicators();
    
  } while(lcd.nextPage());
}

void loop() {
  analyzeAudio();
  handleButtons();
  updatePeakHold();
  drawSpectrum();
  
  if (AUTO_GAIN) {
    maxValue_f = maxValue * k + maxValue_f * (1 - k);
    if (millis() - gainTimer > 1500) {
      if (maxValue_f > VOL_THR) gain = maxValue_f;
      else gain = 100;
      gainTimer = millis();
    }
  }
}

void analyzeAudio() {
  for (int i = 0 ; i < FHT_N ; i++) {
    int sample = analogRead(A0);
    fht_input[i] = sample; // put real data into bins
  }
  fht_window();  // window the data for better frequency response
  fht_reorder(); // reorder the data before doing the fht
  fht_run();     // process the data in the fht
  fht_mag_log(); // take the output of the fht
}

Credits

Mirko Pavleski
199 projects • 1497 followers

Comments