RGB 32밴드 오디오 스펙트럼 비주얼라이저 - 아두이노 LED 매트릭스 프로젝트
부품 및 소모품
| 부품 | 수량 |
| 아두이노 나노 R3 | 1 |
| WS2812B 8x32 RGB LED 매트릭스 | 1 |
| 저항 4.75k 옴 | 3 |
| 저항 100k 옴 | 2 |
| 저항 10k 옴 | 1 |
| 스루홀 저항 390 옴 | 1 |
| 커패시터 100 nF | 2 |
| 커패시터 1000 µF | 1 |
| 스파크펀 푸시버튼 스위치 12mm | 1 |
| 4x6 cm 멀티홀 프로토타입 보드 | 1 |
필수 도구 및 장비
- 납땜 인두 (일반)
- 3.5mm 잭 스플리터 케이블
프로젝트 개요
프로젝트 소개
이 프로젝트는 아두이노 나노와 8x32 WS2812B RGB LED 매트릭스를 사용하여 RGB 32밴드 오디오(음악) 주파수 스펙트럼 비주얼라이저를 만드는 것입니다.
영감을 준 원본 프로젝트
Shajeeb 님이 MAX72xx LED 매트릭스 기반으로 만든 원본 프로젝트 32-Band Audio Spectrum Visualizer Analyzer에 큰 감사를 드립니다. 저는 LED 매트릭스 구동 부분만 RGB WS2812B LED 매트릭스에 맞게 수정했습니다.
WS2812B RGB LED 매트릭스
5050 SMD 고휘도 LED 기반 RGB LED 매트릭스를 사용하려면 외부 전원 공급 장치가 필요합니다. LED 하나당 10mA 이상 소모할 수 있어 모든 LED가 최대 밝기로 켜지면 2.5A 이상 소모할 수 있기 때문입니다.
이 때문에 +5V 라인에 다이오드를 직렬로 삽입하여 USB 케이블이 연결되지 않았을 때 아두이노가 단독으로 전원 공급받을 수 있게 하고, 아두이노가 RGB 매트릭스의 전원 공급원이 되지 않게 하여 보드 내부 회로가 감당할 수 없는 전류로 과부하되는 것을 방지했습니다.
원본 프로젝트에 입력 다이오드 외에도 LED 매트릭스 입력을 전압 피크로부터 보호하기 위해 아두이노 핀 D6과 데이터 입력 사이에 390옴 저항을 직렬로 추가했고, 아두이노 전원 전압 안정성을 위해 1000µF 12V 커패시터를 추가했습니다.
하드웨어 조립
메인 사진에서 볼 수 있듯 첫 프로토타입은 4x6cm 멀티홀 보드에 두 개의 RCA 오디오 소켓(보드에 직접 납땜, 3.5mm 암 잭 소켓으로 대체 가능)을 장착해 만들었습니다. 험(hum) 노이즈를 피하려면 소스와 보드 오디오 입력 간 연결에 차폐 케이블을 사용하는 것이 중요합니다. 또 아두이노와 LED 매트릭스 간 연결은 최대한 짧게 유지하는 것이 좋습니다.
코드 설명
결국 모든 소프트웨어는 FFT 라이브러리를 통한 샘플링 절차와 Shajeeb 님의 결정적인 구현이라는 훌륭한 작업에 기반합니다.
두 가지 함수를 추가했습니다:
- GetLedFromMatrix(...): 매트릭스를 행과 열로 매핑하여 256개 LED 각각을 행/열 좌표로 주소 지정 가능하게 함
- SetColumn(...): 오디오 디지털화로 얻은 피크 값(0~7 범위)과 2차원 배열에 미리 설정된 색상에 기반해 각 열의 LED를 켬. 색상 값을 마음껏 변경해 보세요. Adafruit Neopixel 라이브러리 데모에서 가져온
Wheel() 서브루틴을 사용하여 0~255 값에서 32비트 색상 값을 반환받아 setPixelColor에 직접 전달합니다. 메모리 제약 때문에 가능한 32비트 변수로 RGB 색상 값을 저장하는 것은 피했습니다.
오디오 이퀄라이제이션
PC 마더보드 내장 사운드 카드에서 나오는 오디오로 테스트했는데, 주파수 응답을 개선하기 위해 32개 값의 바이트 배열을 추가해 베이스는 감쇠시키고 트레블을 강화하는 이퀄라이제이션 커브를 구성했습니다. 필요 없으면 EQ_ON 변수를 false로 설정하거나 eq[32] 배열의 32개 값을 변경해 감쇠 레벨을 조정하세요. 100이면 진폭 변화 없음, 100 미만이면 감쇠, 100 초과면 해당 주파수 대역 강조.
LED 밝기
매트릭스 밝기는 코드에서 32(BRIGHTNESS 상수)로 프리셋되어 있습니다. WS2812B 매트릭스 최대 밝이(이론상)는 255지만 100 이상이면 LED 빛이 흰색에서 연한 노란색으로 변합니다. 매트릭스 우측 커넥터가 아닌 중앙의 빨강/검정 와이어로 전원 공급하면 개선될 수 있습니다. 아직 실험 중입니다.
최대 밝기 64 사용 시 1A 전원 공급기로 충분하지만, 그 이상이면 2A 필수입니다.
향후 업데이트
OpenMusicLabs FHT 라이브러리를 사용하는 새 버전을 작업 중입니다. 아두이노 FFT보다 수십 배 빠릅니다. 기대해 주세요! :)
영어 실력이 부족해 구글 번역기를 사용했습니다. 양해 바랍니다.
소스 코드
Spectrum_Analyzer_WS2812B.ino
/* Copyright (c) 2019 Shajeeb TM
* MIT 라이선스 - 자유 사용, 복사, 수정, 병합, 배포, 서브라이선스, 판매 허용
* WS2812B LED 매트릭스 버전 by Janux
*/
#include <arduinoFFT.h>
#include <SPI.h>
#include <Adafruit_NeoPixel.h>
#define SAMPLES 64 // 2의 거듭제곱이어야 함
#define xres 32 // 디스플레이 총 열 수, SAMPLES/2 이하여야 함
#define yres 8 // 디스플레이 총 행 수
#define ledPIN 6 // LED 매트릭스 제어 핀
#define NUM_LEDS (xres * yres)
#define BRIGHTNESS 32
#define buttonPin 5 // 디스플레이 색상 모드 변경용 푸시버튼 핀
byte yvalue;
byte displaycolumn, displayvalue;
int peaks[xres];
byte state = HIGH;
byte previousState = LOW;
byte displaycolor = 0;
// 샘플링용 배열
double vReal[SAMPLES];
double vImag[SAMPLES];
byte data_avgs[xres];
arduinoFFT FFT = arduinoFFT();
unsigned long lastDebounceTime = 0;
unsigned long debounceDelay = 100;
// 파라미터 1 = 매트릭스 LED 수
// 파라미터 2 = 핀 번호
// 파라미터 3 = 픽셀 타입 플래그
// NEO_KHZ800 800KHz 비트스트림 (WS2812 LED 제품 대부분)
// NEO_GRB GRB 비트스트림 배선 (NeoPixel 제품 대부분)
Adafruit_NeoPixel pixel = Adafruit_NeoPixel(NUM_LEDS, ledPIN, NEO_GRB + NEO_KHZ800);
// EQ 필터: 베이스 감쇠, 트레블 향상
// PC 사운드 카드처럼 베이스 많고 고주파 약한 경우 유용
bool EQ_ON = true; // false로 설정 시 EQ 비활성화
byte eq[32] = {50, 55, 60, 70, 75, 80, 85, 95, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 115, 125, 140, 160, 185, 200, 225, 255 };
// 단일 LED용 색상 정의, setColumn 함수에서 사용, 0이면 커스텀 색상
// 색상은 Wheel 함수로 계산 (아래 참조)
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, 0, 0},
{170, 160, 150, 140, 130, 120, 1, 1},
{170, 160, 150, 140, 130, 120, 1, 1}
};
void setup() {
pixel.begin();
pixel.setBrightness(BRIGHTNESS);
// FFT 연산 시작
ADCSRA = 0b11100101; // ADC 프리러닝 모드, 프리스케일러 32 (0xe5)
ADMUX = 0b00000000; // A0 핀 사용, 외부 전압 레퍼런스
}
void loop() {
// ++ 샘플링
for (int i = 0; i < SAMPLES; i++) {
while (!(ADCSRA & 0x10)); // ADC 변환 완료 대기 (ADIF 비트 설정)
ADCSRA = 0b11110101; // ADIF 비트 클리어하여 다음 변환 가능 (0xf5)
int value = ADC - 512; // ADC 읽기 및 DC 오프셋 제거
vReal[i] = value / 8; // 압축 후 빈에 복사
vImag[i] = 0;
}
FFT.Windowing(vReal, SAMPLES, FFT_WIN_TYP_HAMMING, FFT_FORWARD);
FFT.Compute(vReal, vImag, SAMPLES, FFT_FORWARD);
FFT.ComplexToMagnitude(vReal, vImag, SAMPLES);
// -- FFT
// ++ FFT 결과를 디스플레이 열 수(xres)에 맞게 재배열
int step = (SAMPLES / 2) / xres;
int c = 0;
for (int i = 0; i < (SAMPLES / 2); i += step) {
data_avgs[c] = 0;
for (int k = 0; k < step; k++) {
data_avgs[c] += vReal[i + k];
}
data_avgs[c] /= step;
c++;
}
// ++ 측정된 값에 따라 디스플레이에 전송
for (int i = 0; i < xres; i++) {
if (EQ_ON) data_avgs[i] = data_avgs[i] * (float)(eq[i]) / 100; // EQ 필터 적용
data_avgs[i] = constrain(data_avgs[i], 0, 80); // 버킷 최대/최소 0-80
data_avgs[i] = map(data_avgs[i], 0, 80, 0, yres); // yres 0-8로 재매핑
yvalue = data_avgs[i];
peaks[i]--; // 피크 1씩 감소 (감쇠 효과)
if (yvalue > peaks[i]) peaks[i] = yvalue; // 이전 피크보다 크면 저장
yvalue = peaks[i];
displaycolumn = i;
displayvalue = yvalue;
setColumn(displaycolumn, displayvalue); // 버킷 그리기
}
pixel.show(); // 버킷 표시
displaycolorChange(); // 버튼 눌림 확인하여 색상 모드 변경
}
//-----------------------------------------------------------------
// x 열의 LED를 y 값에 따라 점등
void setColumn(byte x, byte y) {
byte led, i;
for (i = 0; i < yres; i++) {
led = GetLedFromMatrix(x, i); // x,y 좌표로 현재 LED 번호 조회
if (peaks[x] > i) {
switch (displaycolor) {
case 4: // 배열 값에 0을 넣어 피크 색상 커스텀
if (colors[displaycolor][i] > 0) {
pixel.setPixelColor(led, Wheel(colors[displaycolor][i]));
} else {
pixel.setPixelColor(led, 255, 255, 255); // 피크만 흰색
}
break;
case 5: // 열별 색상 변경
pixel.setPixelColor(led, Wheel(x * 16));
break;
case 6: // 행별 색상 변경
pixel.setPixelColor(led, Wheel(i * 36));
break;
default: // 디스플레이 색상 세트 (displaycolor 0~3)
pixel.setPixelColor(led, Wheel(colors[displaycolor][i]));
}
} else {
pixel.setPixelColor(led, 0); // 끄기
}
}
}
//======================================================================
// x,y 좌표로 LED 번호 계산
// 우측에 입력 커넥터가 있는 수평 배치된 지그재그(serpentine) 레이아웃 WS2812B용
// 입력: x=0-31, y=0-7, 반환: 0-255 LED 번호
//========================================================================
byte GetLedFromMatrix(byte x, byte y) {
x = xres - x - 1;
if (x & 0x01) { // 홀수 열은 역방향 증가
return ((x + 1) * yres - y - 1);
} else { // 짝수 열은 정방향 증가
return ((x + 1) * yres - yres + y);
}
}
//========================================================================
void displaycolorChange() {
int reading = digitalRead(buttonPin);
if (reading == HIGH && previousState == LOW && millis() - lastDebounceTime > debounceDelay) {
displaycolor++;
if (displaycolor > 6) displaycolor = 0;
lastDebounceTime = millis();
}
previousState = reading;
}
/* Adafruit Neopixel 데모 스케치의 유틸리티 함수
* 0~255 값 입력 시 색상 값 반환
* 색상 전이: R → G → B → R
*/
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);
}
커스텀 파트 및 인클로저
- 3D 프린트 파일:
arduino_rgb_audio_analyzer_lX4Dv4Pta2.zip
- 회로도 이미지 2개 포함
회로도
회로도 이미지 참조