FHT 오디오 스펙트럼 비주얼라이저 - 멋진 LED 오디오 디스플레이 만들기
필요한 부품 및 소모품
| Arduino Nano R3 |
1개 |
| WS2812B 8x32 RGB LED 매트릭스 |
1개 |
| 저항 4.75k 옴 |
3개 |
| 저항 100k 옴 |
2개 |
| 저항 10k 옴 |
2개 |
| 저항 390 옴 (스루홀) |
1개 |
| 1N4007 다이오드 (고전압/대전류) - 1N4004 대체 가능 |
1개 |
| 커패시터 100 nF |
2개 |
| 커패시터 1000 µF |
1개 |
| 푸시버튼 스위치 12mm (패널 버튼 2개로 대체 가능) |
2개 |
| 4x6 cm 범용 프로토타입 보드 |
1개 |
| PCB 스테레오 RCA 암 플러그 |
1개 |
필요한 도구 및 기계
- 납땜 인두 (일반)
- 3.5mm 오디오 잭 - 스테레오 RCA 커넥터 케이블
프로젝트 소개
Arduino FHT 라이브러리
FHT(Fast Hartley Transform)는 더 잘 알려진 FFT(Fast Fourier Transform)와 동일한 역할을 하지만, FFT가 복소수 데이터를 사용하는 반면 FHT는 실수 데이터만 사용하므로 처리 능력과 메모리를 절반만 소모합니다.
이 라이브러리는 FFT 라이브러리처럼 실수 값과 허수 값용 두 개 배열 대신 단일 배열로 데이터 샘플링을 수행하며, 복잡한 부동소수점 로그 계산을 루프로 돌리는 대신 수치 테이블을 사용하여 계산을 단순화합니다.
단점은 샘플링에 사용된 배열 크기의 절반만 값을 반환하므로 오디오 범위 양 끝에서 일정 해상도와 정밀도 손실이 발생합니다. 하지만 측정 기기가 아닌 단순한 가젯을 만드는 것이 목적이라면 극도의 정밀도는 필요 없고, 음악 리듬에 맞춰 LED를 켜는 정도면 충분합니다.
FHT 라이브러리는 매우 빨라서 깜빡임을 줄이기 위해 코드에 지연을 추가해야 할 정도입니다. FFT 라이브러리 버전은 음악을 따라가는 듯 느려 보였던 반면, 이 버전은 훨씬 빠르고 반응성이 뛰어납니다. 시스템 리소스 사용이 적어 추가 기능 구현도 가능했습니다.
새 버전의 추가 기능
이전 버전과 하드웨어는 동일하므로, 기존 제작자는 버튼 하나와 10k 저항만 추가하고 코드만 업데이트하면 됩니다.
FHT 라이브러리 적용 외에 다음과 같은 기능이 추가되었습니다:
- 밝기 조절용 추가 버튼
- 색상 및 밝기 설정을 EEPROM에 저장하여 다음 전원 켤 때 복원
- 버튼 누름 시 및 전원 켤 때 색상/밝기 설정 레벨 표시 (비활성화 가능)
코드
FHT 코드 버전 (Arduino)
/* Copyright (c) 2020 Janux
MIT License - 상업적 이용, 수정, 배포 가능 */
#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;
byte 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; // ADC 프리러닝 모드, 프리스케일러 32
ADMUX = 0b00000000; // A0 핀, 외부 전압 레퍼런스
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); // FHT가 너무 빨라 깜빡임 방지용 지연
}
}
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] = peaks[i] - 1;
if (yvalue > peaks[i]) peaks[i] = yvalue;
yvalue = peaks[i];
setColumn(i, yvalue);
}
pixel.show();
}
void setColumn(byte x, byte y) {
int led, i;
for (i = 0; i < yres; i++) {
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 < sizeof(CONFIGURATION); i++) {
*((char*)&CONFIGURATION + i) = EEPROM.read(CONFIG_START + i);
}
return 1;
}
return 0;
}
void saveConfig() {
CONFIGURATION.displaycolor = displaycolor;
CONFIGURATION.brightness = brightness;
for (unsigned int i = 0; i < sizeof(CONFIGURATION); i++)
EEPROM.write(CONFIG_START + i, *((char*)&CONFIGURATION + i));
}
void showSettings(byte num, bool show) {
if (show) {
pixel.clear();
if (num == 1 || num == 3) {
drawChar(0, 0);
drawChar(displaycolor + 1, 5);
}
if (num == 2 || num == 3) {
drawChar(9, xres - 9);
drawChar(brightness + 1, xres - 4);
}
delay(1000);
pixel.clear();
}
}
void drawChar(byte val, byte pos) {
for (int x = 4; x >= 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();
}
}
}
}
회로도
회로도 이미지 2장 참조