亿迅智能制造网
工业4.0先进制造技术信息网站!
首页 | 制造技术 | 制造设备 | 工业物联网 | 工业材料 | 设备保养维修 | 工业编程 |
home  MfgRobots >> 亿迅智能制造网 >  >> Manufacturing Technology >> 制造工艺

FHT 音频频谱可视化 - 打造绚丽 LED 音乐灯光秀

核心组件清单 (BOM)

组件规格/型号数量关键参数
MCUArduino Nano R3 (ATmega328P)116MHz, 2KB RAM, 32KB Flash
LED 矩阵WS2812B 8×32 RGB (256 颗)1单线控制, GRB 顺序, 800kHz
音频输入立体声 RCA 母座 + 3.5mm 转 RCA 线1左右声道合并单声道输入
前置放大/偏置电路见原理图1将音频信号偏移至 0-5V 范围
关键无源器件1N4007 二极管、1000µF/100nF 电容、390Ω/10kΩ/4.75kΩ/100kΩ 电阻、12mm 按键×2各若干电源保护、信号调理、按键消抖
原型板4×6cm 万用板1

核心技术:FHT 与 FFT 的区别

FHT (Fast Hartley Transform,快速哈特莱变换) 与 FFT 同为时频域转换算法,但 FHT 仅处理实数数据,而 FFT 需处理复数。优势在于:

本项目采用 OpenMusicLabs FHT 库,FHT_N = 128 采样点,输出 64 个频率箱,映射到 32 列显示,每列取 2 个箱平均。

电路设计要点

音频前端电路

  1. 分压偏置:两个 100kΩ 电阻分压提供 2.5V 虚拟地,音频信号叠加在其上
  2. 保护二极管:1N4007 限幅保护 ADC 引脚防过压
  3. 去耦电容:100nF 滤高频噪声,1000µF 稳定 WS2812B 供电
  4. 数据线保护:390Ω 串联电阻抑制 WS2812B 数据线过冲

供电设计

功能特性

完整代码 (FHT 版)

/* FHT Audio Spectrum Visualizer - Janux 2020
 * MIT License | Based on OpenMusicLabs FHT Library
 * Hardware: Arduino Nano, WS2812B 8x32 Matrix
 */
#define LIN_OUT 1
#define FHT_N 128
#include <FHT.h>

#define XRES 32
#define YRES 8
#define LED_PIN 6
#define NUM_LEDS (XRES * YRES)
#include <Adafruit_NeoPixel.h>

#define COLOR_PIN 5
#define BRIGHT_PIN 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; } config_t;
config_t CONFIG = { CONFIG_VERSION, 0, 1 };

byte yValue;
int peaks[XRES];
byte btnState = HIGH, lastBtnState = LOW;
unsigned long lastDebounce = 0;
const unsigned long debounceDelay = 100;
int dataAvgs[XRES];

Adafruit_NeoPixel strip = Adafruit_NeoPixel(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);

// EQ 补偿曲线 (32 段)
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;

// 8 套配色方案 (每列 8 行颜色索引)
byte colors[][8] = {
  {170,160,150,140,130,120,1,1},    // 0: 红橙黄绿渐变
  {1,5,10,15,20,25,90,90},          // 1: 蓝青渐变
  {90,85,80,75,70,65,1,1},          // 2: 绿黄红逆序
  {90,90,90,30,30,30,1,1},          // 3: 紫粉混合
  {170,160,150,140,130,120,110,0},  // 4: 自定义(0=白)
  {0}, {0}, {0}                     // 5:按列变色 6:按行变色 7:国旗模式
};

// 自定义字符位图 (用于显示设置数字)
byte charBitmap[] = {
  0x1C,0x10,0x10,0x10,0x10,0x1C,  // 0
  0x08,0x18,0x08,0x08,0x08,0x1C,  // 1
  0x0C,0x12,0x04,0x08,0x10,0x1E,  // 2
  0x0C,0x12,0x02,0x06,0x12,0x0C,  // 3
  0x10,0x10,0x10,0x14,0x1E,0x04,  // 4
  0x1E,0x10,0x1E,0x02,0x12,0x0C,  // 5
  0x1E,0x10,0x10,0x1E,0x12,0x1E,  // 6
  0x1E,0x12,0x1E,0x1E,0x02,0x04,  // 7
  0x08,0x08,0x08,0x0C,0x12,0x0C,  // 8
  0x12,0x12,0x0C,0x1C,0x12,0x1C   // 9
};

void setup() {
  strip.begin();
  // ADC 设置: 自由运行模式, 预分频 32 (16MHz/32=500kHz 采样率)
  ADCSRA = 0b11100101;  // ADEN=1, ADSC=1, ADATE=1, ADIF=1, ADIE=0, ADPS=101(32)
  ADMUX = 0b00000000;   // REFS=00(外部参考), ADLAR=0(右对齐), MUX=0000(A0)

  if (loadConfig()) { displayColor = CONFIG.displayColor; brightness = CONFIG.brightness; }
  strip.setBrightness(brightness * 24 + 8);  // 映射 0-7 -> 8-176
  showSettings(3, true);  // 开机显示配色+亮度
}

void loop() {
  while (1) {
    sampling();         // 采样 128 点
    rearrangeFHT();     // FHT 后重排映射到 32 列
    sendToDisplay();    // 点亮 LED
    colorChange();      // 检查配色按键
    brightnessChange(); // 检查亮度按键
    delay(10);          // 降低刷新率减少闪烁
  }
}

void sampling() {
  for (int i = 0; i < FHT_N; i++) {
    while (!(ADCSRA & 0x10));  // 等待 ADIF 置位
    ADCSRA = 0b11110101;       // 清 ADIF (写 1 清零)
    byte m = ADCL;
    byte j = ADCH;
    int val = (j << 8) | m;    // 10 位 ADC 结果
    val -= 0x0200;             // 转有符号 (以 512 为零点)
    val <<= 6;                 // 扩展为 16 位有符号
    fht_input[i] = val / 8;    // 压缩至 8 位有符号输入 FHT
  }
  fht_window();   // 加窗
  fht_reorder();  // 重排
  fht_run();      // 变换
  fht_mag_lin();  // 线性幅度输出
}

void rearrangeFHT() {
  int step = (FHT_N / 2) / XRES;  // 64/32 = 2
  for (int c = 0, i = 0; i < FHT_N/2; i += step, c++) {
    dataAvgs[c] = 0;
    for (int k = 0; k < step; k++) dataAvgs[c] += fht_lin_out[i + k];
    dataAvgs[c] /= step;
  }
}

void sendToDisplay() {
  for (int x = 0; x < XRES; x++) {
    if (EQ_ON) dataAvgs[x] = dataAvgs[x] * eq[x] / 100;
    dataAvgs[x] = constrain(dataAvgs[x], 0, 80);
    yValue = map(dataAvgs[x], 0, 80, 0, YRES);
    peaks[x] = max(peaks[x] - 1, yValue);  // 峰值保持+回落
    yValue = peaks[x];
    setColumn(x, yValue);
  }
  strip.show();
}

// 蛇形布局坐标映射: DIN 在右下角, 第一列向上, 第二列向下...
int getLedIndex(byte x, byte y) {
  x = XRES - 1 - x;  // 镜像翻转, 使频谱从左向右
  if (x & 1) return (x + 1) * YRES - y - 1;  // 奇列向下
  else return (x + 1) * YRES - YRES + y;     // 偶列向上
}

void setColumn(byte x, byte y) {
  for (byte row = 0; row < YRES; row++) {
    int idx = getLedIndex(x, row);
    if (row < y) {
      uint32_t color;
      switch (displayColor) {
        case 4: color = (colors[4][row] == 0) ? strip.Color(255,255,255) : Wheel(colors[4][row]); break;
        case 5: color = Wheel(x * 16); break;
        case 6: color = Wheel(row * y * 3); break;
        case 7:  // 国旗模式
          if (row < YRES - 2) color = (x & 1) ? strip.Color(0,0,255) : strip.Color(255,0,0);
          else color = strip.Color(255,255,255);
          break;
        default: color = Wheel(colors[displayColor][row]);
      }
      strip.setPixelColor(idx, color);
    } else {
      strip.setPixelColor(idx, 0);
    }
  }
}

// 色轮函数: 0-255 映射 R->G->B->R
uint32_t Wheel(byte pos) {
  pos = 255 - pos;
  if (pos < 85) return strip.Color(255 - pos*3, 0, pos*3);
  if (pos < 170) { pos -= 85; return strip.Color(0, pos*3, 255 - pos*3); }
  pos -= 170; return strip.Color(pos*3, 255 - pos*3, 0);
}

void colorChange() {
  byte reading = digitalRead(COLOR_PIN);
  if (reading == HIGH && lastBtnState == LOW && millis() - lastDebounce > debounceDelay) {
    displayColor = (displayColor + 1) % 8;
    showSettings(1, true);
    saveConfig();
    lastDebounce = millis();
  }
  lastBtnState = reading;
}

void brightnessChange() {
  byte reading = digitalRead(BRIGHT_PIN);
  if (reading == HIGH && lastBtnState == LOW && millis() - lastDebounce > debounceDelay) {
    brightness = (brightness + 1) % 8;
    strip.setBrightness(brightness * 24 + 8);
    showSettings(2, true);
    saveConfig();
    lastDebounce = millis();
  }
  lastBtnState = reading;
}

void showSettings(byte mode, bool show) {
  if (!show) return;
  strip.clear();
  if (mode == 1 || mode == 3) { drawChar(0, 0); drawChar(displayColor + 1, 5); }
  if (mode == 2 || mode == 3) { drawChar(9, XRES - 9); drawChar(brightness + 1, XRES - 4); }
  delay(1000);
  strip.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) {
        strip.setPixelColor(getLedIndex(4 - x + pos, y + 1), Wheel((pos > 10) * 170));
        strip.show();
      }
    }
  }
}

int loadConfig() {
  for (int i = 0; i < 5; i++) if (EEPROM.read(CONFIG_START + i) != CONFIG_VERSION[i]) return 0;
  for (unsigned int i = 0; i < sizeof(CONFIG); i++) *((char*)&CONFIG + i) = EEPROM.read(CONFIG_START + i);
  return 1;
}

void saveConfig() {
  CONFIG.displayColor = displayColor; CONFIG.brightness = brightness;
  for (unsigned int i = 0; i < sizeof(CONFIG); i++) EEPROM.write(CONFIG_START + i, *((char*)&CONFIG + i));
}

制作与调试建议

扩展创意

原理图与效果图

详见项目附件:原理图 (含音频前端、WS2812B 供电、按键电路)、实物接线图、运行效果照片。

制造工艺

  1. 32 频段 Arduino 音频频谱分析仪:线性与伪对数双模式
  2. 智能缺水报警器 – DIY植物土壤湿度监测装置
  3. 意大利语单词时钟:Arduino Nano + NeoPixel 144 灯珠 + LCD + RTC 打造
  4. 基于Arduino MKR Fox 1200构建可靠Sigfox千瓦时电表
  5. 蓝牙温度监测仪:Arduino Nano + NTC 热敏电阻 + HC-06 无线传输
  6. 非接触手势门锁:APDS-9960 + Arduino 实现安全门禁
  7. Arduino Nano 多功能伴侣套件 —— 电阻/电容测量、二极管测试、RTC时钟一体化
  8. Arduino Micro/Leonardo USB MIDI 适配器套件 —— 完整元件清单与固件指南
  9. 为Arduino构建隔离模拟输入电路
  10. RGB 32 频段音频频谱可视化器:Arduino Nano 驱动 WS2812B 8×32 LED 矩阵
  11. 两个电阻自制 Arduino UNO 视频输出线(RCA/TVout)
  12. Arduino UNO 精准视觉反应时间测试仪