Анализатор аудиоспектра с настраиваемыми режимами на Arduino

В этой статье мы разберём, как собрать и запрограммировать анализатор аудиоспектра — устройство, которое визуализирует распределение частот звукового сигнала — на базе Arduino. Такой анализатор отображает уровни звуковых частот в виде графических столбиков на LCD‑дисплее и позволяет переключать режимы работы, скорость реакции и чувствительность с помощью кнопок.

Анализатор аудиоспектра

Для чего нужен анализатор аудиоспектра

Анализатор аудиоспектра позволяет увидеть, какие частоты присутствуют в звуковом сигнале, например из музыки или микрофона. Вместо обычного VU‑метра (громкость), спектральный анализ показывает **распределение энергии по частотам** — это полезно для визуализации, музыкальных эффектов или образовательных экспериментов с цифровой обработкой сигналов.

Что понадобится для проекта

  • Arduino Nano или аналогичная плата на базе ATmega328
  • Графический LCD 128×64 (контроллер ST7920)
  • 3 кнопки для переключения режимов
  • Резистор 10 кОм
  • Резистор ~4.7 кОм
  • Потенциометр 10 кОм
  • Провода, макетная плата, корпус

Принцип прост: аудио сигнал поступает на аналоговый вход Arduino, где проводится быстрое преобразование Фурье (FFT) для расчёта спектра. Данные затем визуализируются на LCD‑экране.

Анализатор аудиоспектра

Назначение выводов

Компонент Пин Arduino Назначение
Аналоговый вход аудио A0 Вход для звукового сигнала
Кнопка 1 D8 Переключение режима отображения
Кнопка 2 D9 Переключение скорости
Кнопка 3 D10 Переключение чувствительности
LCD EN D6 Enable для LCD
LCD RW D5 Read/Write для LCD
LCD CS D4 Chip Select для LCD

Как это работает

Сигнал с аудио‑источника подаётся на аналоговый вход A0. Arduino считывает значения и помещает их в массив для обработки. Далее происходит быстрое преобразование Фурье (FHT) — это алгоритм для анализа частот. Результаты логарифмируются и нормализуются, после чего отображаются в виде столбиков на графическом экране.

Для каждой из трёх кнопок реализованы циклические режимы:

  • Первая кнопка — режим отображения: обычные столбики, пик‑холд, падающие точки, симметричный режим.
  • Вторая кнопка — скорость реакции визуализации (нормальная, быстрая, медленная).
  • Третья кнопка — чувствительность (нормальная, высокая, низкая).
Теперь можно адаптировать изображение спектра под свои предпочтения без изменения кода.

Основные настройки в коде

В скетче определяются параметры, которые можно регулировать: порог тишины, чувствительность (gain), ширина спектра, длительность удержания пиков и др. Также задаются пины кнопок и инициализируются массивы значений для FHT.

Для работы с LCD используется библиотека U8glib, а сами спектральные вычисления выполняются с помощью библиотеки FHT. Это позволяет Arduino быстро обрабатывать входной сигнал и обновлять графическое изображение.

Пример фрагмента Arduino‑кода

Полный код в оригинале объёмный, но ниже приведён ключевой фрагмент, который показывает работу кнопок, обработку спектра и отрисовку на дисплее (обрезано до основного визуального цикла):

#define AUTO_GAIN 0       // Автоматическая подстройка громкости (отключена при ручном управлении)
#define VOL_THR 25        // Порог тишины (ниже этого значения ничего не выводится)
#define LOW_PASS 20       // Нижний порог чувствительности к шуму (чтобы не было скачков без звука)
#define DEF_GAIN 80       // Стандартный максимальный уровень (игнорируется при автоусилении)
#define FHT_N 256         // Размер спектра (ширина x2)
#define LOG_OUT 1
#define PEAK_HOLD_TIME 2000     // Время удержания пиков, мс

// Пины кнопок
#define BUTTON1 8
#define BUTTON2 9  
#define BUTTON3 10

// Вручную заданный массив частотных полос:
// сначала плавный рост, затем более резкий
byte posOffset[16] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; // до ~1500 Гц
//byte posOffset[16] = {1, 2, 3, 4, 6, 8, 10, 13, 16, 20, 25, 30, 35, 40, 45, 50}; // до ~4000 Гц

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

#include <Wire.h>
#include <U8glib.h>  // Библиотека для графических дисплеев
#include <FHT.h>     // Библиотека быстрого преобразования Хартли

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

//U8GLIB_SH1106_128X64 lcd(U8G_I2C_OPT_DEV_0|U8G_I2C_OPT_FAST);  // I2C-дисплей (закомментировано)
U8GLIB_ST7920_128X64_1X lcd(EN, RW, CS); // Последовательный режим, PSB = GND

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

// Переменные состояния кнопок
bool button1State = false;
bool button2State = false;
bool button3State = false;
unsigned long button1Time = 0;
unsigned long button2Time = 0;
unsigned long button3Time = 0;

// Переменные режимов
byte displayMode = 0;           // 0=обычный, 1=пики, 2=падающие точки, 3=симметричный
byte speedMode = 0;             // 0=нормальная скорость, 1=быстро, 2=медленно
byte sensitivityMode = 0;       // 0=норма, 1=высокая, 2=низкая
byte peakHold[16];              // Значения удержания пиков
unsigned long peakTimer[16];    // Таймеры затухания пиков

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); // Разрядность АЦП 10 бит
  analogReference(INTERNAL1V024);
  pinMode(A0, INPUT); // Аудиовход
  
  // Инициализация кнопок
  pinMode(BUTTON1, INPUT_PULLUP);
  pinMode(BUTTON2, INPUT_PULLUP);
  pinMode(BUTTON3, INPUT_PULLUP);
  
  // Инициализация массива пиков
  for(int i = 0; i < 16; i++) {
    peakHold[i] = 0;
    peakTimer[i] = 0;
  }
}

void handleButtons() {
  // Кнопка 1 — переключение режима отображения
  if (digitalRead(BUTTON1) == LOW) {
    if (millis() - button1Time > 300) { // Антидребезг
      displayMode = (displayMode + 1) % 4;
      button1Time = millis();
    }
  }
  
  // Кнопка 2 — переключение скорости анимации
  if (digitalRead(BUTTON2) == LOW) {
    if (millis() - button2Time > 300) {
      speedMode = (speedMode + 1) % 3;
      button2Time = millis();
    }
  }
  
  // Кнопка 3 — переключение чувствительности
  if (digitalRead(BUTTON3) == LOW) {
    if (millis() - button3Time > 300) {
      sensitivityMode = (sensitivityMode + 1) % 3;
      button3Time = millis();
      
      // Изменение усиления в зависимости от чувствительности
      switch(sensitivityMode) {
        case 0: gain = DEF_GAIN; break;     // Нормальная
        case 1: gain = DEF_GAIN / 2; break; // Высокая чувствительность
        case 2: gain = DEF_GAIN * 2; break; // Низкая чувствительность
      }
    }
  }
}

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() {
  // Индикаторы режимов в правом верхнем углу
  lcd.setFont(u8g_font_04b_03);
  
  char modeChar = 'N';
  switch(displayMode) {
    case 0: modeChar = 'N'; break; // Обычный
    case 1: modeChar = 'P'; break; // Пики
    case 2: modeChar = 'D'; break; // Точки
    case 3: modeChar = 'S'; break; // Симметрия
  }
  
  char speedChar = 'N';
  switch(speedMode) {
    case 0: speedChar = 'N'; break; // Нормально
    case 1: speedChar = 'F'; break; // Быстро
    case 2: speedChar = 'S'; break; // Медленно
  }
  
  char sensChar = 'N';
  switch(sensitivityMode) {
    case 0: sensChar = 'N'; break; // Норма
    case 1: sensChar = 'H'; break; // Высокая
    case 2: sensChar = 'L'; break; // Низкая
  }
  
  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; // Стартовая анимация
      }
      
      urr[band] = posLevel;
      
      int fallSpeed = 1;
      switch(speedMode) {
        case 0: fallSpeed = 1; break;
        case 1: fallSpeed = 3; break;
        case 2: fallSpeed = 1; if(random(2) == 0) fallSpeed = 0; break;
      }
      
      if(urr[band] < ur[band]) {
        ur[band] = max(ur[band] - fallSpeed, 0);
      } else {
        ur[band] = posLevel;
      }
      
      delayMicroseconds(200);
      
      switch(displayMode) {
        case 0:
          for (int v_pos = 0; v_pos < ur[band] + 4; v_pos += 4)
            lcd.drawBox(pos, 61 - v_pos, 6, 2);
          break;
          
        case 1:
          for (int v_pos = 0; v_pos < ur[band] + 4; v_pos += 4)
            lcd.drawBox(pos, 61 - v_pos, 6, 2);
          if(peakHold[band] > 0)
            lcd.drawBox(pos + 1, 61 - peakHold[band], 4, 1);
          break;
          
        case 2:
          for (int v_pos = 0; v_pos < ur[band]; v_pos += 4)
            lcd.drawBox(pos + 1, 61 - v_pos, 4, 1);
          break;
          
        case 3:
          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);
          }
          break;
      }
    }
    
    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; // Запись выборки в массив
  }
  fht_window();   // Оконная функция
  fht_reorder();  // Перестановка данных
  fht_run();      // Выполнение FHT
  fht_mag_log();  // Логарифмическая амплитуда
}

Код приведён без изменений.
Оригинальный проект опубликован на сайте Arduino Project Hub:
https://projecthub.arduino.cc/

FAQ — Анализатор аудиоспектра на Arduino

Нужен ли предусилитель для аудио?

Да, если амплитуда сигнала низкая, рекомендуется предусилить его для лучшего анализа частот.

Почему на дисплее только шум?

Возможно, входной сигнал слишком слаб, или уровень порога слишком высок. Попробуй увеличить входной уровень или чувствительность.

Можно ли использовать другой дисплей?

Да, можно адаптировать вывод под OLED или TFT, если есть поддержка библиотеки.

Как настроить режимы без кнопок?

Можно использовать последовательный ввод или потенциометры, но это потребует переработки кода.

Какие частоты можно анализировать?

Частотный диапазон ограничен скоростью дискретизации Arduino — примерно до нескольких кГц, подходит для аудио сигналов.

Заключение

Проект аудиоспектра на Arduino — это отличный способ понять спектральный анализ, работу с графическим LCD и организацию нескольких режимов управления. Он может быть основой для визуальных эффектов, аудиоинструментов и образовательных экспериментов по цифровой обработке сигналов.