FHT Аудио-спектр: Создаем потрясающий LED-дисплей для музыки
Компоненты и материалы
| Arduino Nano R3 | ×1 |
| WS2812B 8×32 RGB LED Matrix (256 светодиодов) | ×1 |
| Резистор 4,75 кОм | ×3 |
| Резистор 100 кОм | ×2 |
| Резистор 10 кОм | ×2 |
| Резистор 390 Ом (просверленный) | ×1 |
| Диод 1N4007 (подойдет 1N4004) | ×1 |
| Конденсатор 100 нФ | ×2 |
| Конденсатор 1000 мкФ | ×1 |
| Тактильная кнопка 12 мм (или панельные для коробка) | ×2 |
| Монтажная плата 4×6 см | ×1 |
| RCA-разъем (стерео, мама) для PCB | ×1 |
Необходимые инструменты
- Паяльник
- Кабель 3,5 мм Jack → 2×RCA (для подключения аудиоисточника)
О проекте
Библиотека Arduino FHT
FHT (Fast Hartley Transform) выполняет ту же задачу, что и FFT (Fast Fourier Transform), но работает только с вещественными данными, в то время как FFT оперирует комплексными числами. Поэтому FHT требует в 2 раза меньше процессорного времени и памяти.
Библиотека использует один массив для выборки вместо двух (вещественная и мнимая части), упрощает математику, избегая циклов с комплексными логарифмическими вычислениями, заменяя их табличными значениями.
Недостаток: возвращается половина значений от размера массива, что снижает разрешение на краях аудио-диапазона. Но для визуализации музыки этого достаточно — нам нужно просто плавно зажигать светодиоды в такт.
Подробнее о работе FHT см. на Open Music Labs — там документация, описание функций и примеры для Arduino.
FHT настолько быстра, что в код пришлось добавить задержку для уменьшения мерцания. Результат — дисплей реагирует на музыку мгновенно, без заметного лага, характерного для FFT-версий. Освобожденные ресурсы позволили добавить новые функции.
Новая версия
Проект идентичен предыдущему, нужно только добавить вторую кнопку с подтягивающим резистором 10 кОм и обновить прошивку.
Добавленные функции:
- Вторая кнопка для регулировки яркости.
- Сохранение выбранного цвета и яркости в EEPROM (восстановление при включении).
- Визуальная индикация текущего цвета и яркости при нажатии кнопок и при старте (можно отключить).
Код
FHT Code Version (Arduino)
/* Copyright (c) 2020 Janux. MIT License.
Based on MAX72xx LED matrix + FFT lib by Shajeeb.
Config section based on work of Ragnar Ranøyen Homb (Norwegian Creations). */
#define LIN_OUT 1
#define FHT_N 128
#include <FHT.h>
#define xres 32
#define yres 8
#define ledPIN 6
#define NUM_LEDS (xres * yres)
#include <Adafruit_NeoPixel.h>
#define colorPIN 5
#define brightnessPIN 10
byte displaycolor = 0;
byte brightness = 1;
#include <EEPROM.h>
#define CONFIG_START 32
#define CONFIG_VERSION "VER01"
typedef struct {
char version[6];
byte displaycolor;
byte brightness;
} configuration_type;
configuration_type CONFIGURATION = { CONFIG_VERSION, displaycolor, brightness };
byte yvalue;
int peaks[xres];
byte state = HIGH, previousState = LOW;
unsigned long lastDebounceTime = 0;
unsigned long debounceDelay = 100;
byte data_avgs[xres];
Adafruit_NeoPixel pixel = Adafruit_NeoPixel(NUM_LEDS, ledPIN, NEO_GRB + NEO_KHZ800);
byte eq[32] = {
60, 65, 70, 75, 80, 85, 90, 95, 100, 100, 100, 100, 100, 100, 100, 100,
100, 100, 100, 100, 100, 100, 100, 100, 115, 125, 140, 160, 185, 200, 200, 200
};
bool EQ_ON = true;
byte colors[][8] = {
{170, 160, 150, 140, 130, 120, 1, 1},
{1, 5, 10, 15, 20, 25, 90, 90},
{90, 85, 80, 75, 70, 65, 1, 1},
{90, 90, 90, 30, 30, 30, 1, 1},
{170, 160, 150, 140, 130, 120, 110, 0}
};
byte charBitmap[] = {
0x1C, 0x10, 0x10, 0x10, 0x10, 0x1C,
0x08, 0x18, 0x08, 0x08, 0x08, 0x1C,
0x0C, 0x12, 0x04, 0x08, 0x10, 0x1E,
0x0C, 0x12, 0x02, 0x06, 0x12, 0x0C,
0x10, 0x10, 0x10, 0x14, 0x1E, 0x04,
0x1E, 0x10, 0x1E, 0x02, 0x12, 0x0C,
0x1E, 0x10, 0x10, 0x1E, 0x12, 0x1E,
0x1E, 0x02, 0x04, 0x08, 0x08, 0x08,
0x0C, 0x12, 0x0C, 0x12, 0x12, 0x0C,
0x1C, 0x12, 0x1C, 0x12, 0x12, 0x1C
};
void setup() {
pixel.begin();
ADCSRA = 0b11100101; // free running, prescaler 32
ADMUX = 0b00000000; // A0, external reference
if (loadConfig()) {
displaycolor = CONFIGURATION.displaycolor;
brightness = CONFIGURATION.brightness;
}
pixel.setBrightness(brightness * 24 + 8);
showSettings(3, true);
}
void loop() {
while (1) {
Sampling();
RearrangeFHT();
SendToDisplay();
colorChange();
brightnessChange();
delay(10);
}
}
void Sampling() {
for (int i = 0; i < FHT_N; i++) {
while (!(ADCSRA & 0x10));
ADCSRA = 0b11110101;
byte m = ADCL;
byte j = ADCH;
int value = (j << 8) | m;
value -= 0x0200;
value <<= 6;
fht_input[i] = value / 8;
}
fht_window();
fht_reorder();
fht_run();
fht_mag_lin();
}
void RearrangeFHT() {
int step = (FHT_N / 2) / xres;
int c = 0;
for (int i = 0; i < (FHT_N / 2); i += step) {
data_avgs[c] = 0;
for (int k = 0; k < step; k++)
data_avgs[c] += fht_lin_out[i + k];
data_avgs[c] /= step;
c++;
}
}
void SendToDisplay() {
for (int i = 0; i < xres; i++) {
if (EQ_ON) data_avgs[i] = data_avgs[i] * (float)eq[i] / 100;
data_avgs[i] = constrain(data_avgs[i], 0, 80);
data_avgs[i] = map(data_avgs[i], 0, 80, 0, yres);
yvalue = data_avgs[i];
peaks[i]--;
if (yvalue > peaks[i]) peaks[i] = yvalue;
yvalue = peaks[i];
setColumn(i, yvalue);
}
pixel.show();
}
void setColumn(byte x, byte y) {
for (byte i = 0; i < yres; i++) {
int led = GetLedFromMatrix(x, i);
if (peaks[x] > i) {
switch (displaycolor) {
case 4:
if (colors[displaycolor][i] == 0)
pixel.setPixelColor(led, 255, 255, 255);
else
pixel.setPixelColor(led, Wheel(colors[displaycolor][i]));
break;
case 5:
pixel.setPixelColor(led, Wheel(x * 16));
break;
case 6:
pixel.setPixelColor(led, Wheel(i * y * 3));
break;
case 7:
if (i < yres - 2) {
if (x & 0x01) pixel.setPixelColor(led, 0, 0, 255);
else pixel.setPixelColor(led, 255, 0, 0);
} else pixel.setPixelColor(led, 255, 255, 255);
break;
default:
pixel.setPixelColor(led, Wheel(colors[displaycolor][i]));
}
} else {
pixel.setPixelColor(led, pixel.Color(0, 0, 0));
}
}
}
int GetLedFromMatrix(byte x, byte y) {
int led;
x = xres - x - 1;
if (x & 0x01) led = ((x + 1) * yres - y - 1);
else led = ((x + 1) * yres - yres + y);
return constrain(led, 0, NUM_LEDS);
}
void colorChange() {
int reading = digitalRead(colorPIN);
if (reading == HIGH && previousState == LOW && millis() - lastDebounceTime > debounceDelay) {
displaycolor++; if (displaycolor > 7) displaycolor = 0;
showSettings(1, true); saveConfig(); lastDebounceTime = millis();
}
previousState = reading;
}
void brightnessChange() {
int reading = digitalRead(brightnessPIN);
if (reading == HIGH && previousState == LOW && millis() - lastDebounceTime > debounceDelay) {
brightness++; if (brightness > 7) brightness = 0;
pixel.setBrightness(brightness * 24 + 8);
showSettings(2, true); saveConfig(); lastDebounceTime = millis();
}
previousState = reading;
}
unsigned long Wheel(byte WheelPos) {
WheelPos = 255 - WheelPos;
if (WheelPos < 85) return pixel.Color(255 - WheelPos * 3, 0, WheelPos * 3);
if (WheelPos < 170) { WheelPos -= 85; return pixel.Color(0, WheelPos * 3, 255 - WheelPos * 3); }
WheelPos -= 170; return pixel.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
}
int loadConfig() {
if (EEPROM.read(CONFIG_START+0)==CONFIG_VERSION[0] &&
EEPROM.read(CONFIG_START+1)==CONFIG_VERSION[1] &&
EEPROM.read(CONFIG_START+2)==CONFIG_VERSION[2] &&
EEPROM.read(CONFIG_START+3)==CONFIG_VERSION[3] &&
EEPROM.read(CONFIG_START+4)==CONFIG_VERSION[4]) {
for (unsigned int i=0; i=0; x--) {
for (int y=5; y>=0; y--) {
if ((charBitmap[val*6+5-y] >> x) & 0x01) {
pixel.setPixelColor(GetLedFromMatrix(4-x+pos, y+1), Wheel((pos>10)*170));
pixel.show();
}
}
}
}
Схемы


Производственный процесс
- 32-полосный Arduino аудио-спектральный анализатор — линейный и псевдологарфмический дисплей
- Умный датчик полива «Thirst Alert» на Arduino — DIY сигнализация засухи для растений
- Итальянские словесные часы на Arduino Nano, NeoPixel, LCD и RTC
- Надежный счетчик кВт·ч на Sigfox с Arduino MKR Fox 1200
- Bluetooth-монитор температуры на Arduino Nano с NTC-термистором
- Бесконтактный жестовый замок: APDS-9960 + Arduino для безопасного доступа
- Набор-компаньон для Arduino Nano — необходимые компоненты и инструменты для DIY-электроники
- Набор USB-MIDI адаптера для Arduino Micro / Leonardo — полный список деталей
- Гальванически разделённый аналоговый вход для Arduino на LM331 и оптопаре 4N25
- Точный тест времени реакции на Arduino UNO