查看: 7026|回复: 5

8段式FFT音乐频谱显示

[复制链接]

该用户从未签到

发表于 2019-12-4 14:17 | 显示全部楼层 |阅读模式
本帖最后由 vany5921 于 2019-12-4 14:19 编辑

        这是一个基于ESP32声音采样的音乐频谱分析应用,输入端采用了模拟麦克风输入,能够较好的采集到ad数据,但是由于M5底座的底噪问题,结果会稍微有所影响。代码支持串口动态控制显示形式,并且有三种表现形式:VU表、峰值表、波形显示。代码通过简单的修改可以用于M5Stack。程序依赖于FFT库实现,没有用到外部音频IC,比较有学习价值,移植起来比较容易。以下代码用到了adc1,注意,ESP32有两个adc通道,adc2在使用时不可以同时使用WIFI,另外adc2的初始化与adc1是不同的。显示频段为:125Hz 250Hz 500Hz 1KHz 2Khz 4KHz 8Khz 16kHz 请注意,如果使用麦克风-扬声器测试环境,则很有可能(除非带宽和质量很高)使用的麦克风和扬声器的质量都不足以确保快速傅里叶变换可以转换并显示平坦的频率响应。为了确保看到正确结果的唯一方法是将大约50mV至100mV峰峰值的音频通过音频接口直接馈入ADC端口,而没有DC偏移。这不是软件或显示器或FFT的故障-这是物理定律!
未命名.jpg
代码地址https://github.com/tobozo/ESP32-Audio-Spectrum-Waveform-Display/blob/wrover-kit/ESP32_Spectrum_Display_03.ino
[mw_shl_code=arduino,true]#include <driver/adc.h> // 添加adc库
#include <Wire.h>
#include <WiFi.h> // 关闭wifi
#include <arduinoFFT.h> // 标准arduinoFFT库
#include <M5Stack.h>
arduinoFFT FFT = arduinoFFT();

#define min(X, Y) (((X) < (Y)) ? (X) : (Y)) //宏定义min函数
#define max(X, Y) (((X) > (Y)) ? (X) : (Y))//宏定义max函数
#define TFT_WIDTH 320
#define TFT_HEIGHT 240


#define WAVEFORM_YPOS (TFT_HEIGHT/2) - 100 //波形位置
#define WAVEFORM_SQUEEZE 5 // 波形压缩比例1/5


int SAMPLES = 512; // 必须为2的n次方
#define SAMPLING_FREQUENCY 44000 // 采样频率,不能超过ADC转换时间
#define EQBANDS 8  //8段显示

struct eqBand {
  const char *freqname;
  uint16_t amplitude;
  byte bandWidth;
  int peak;
  int lastpeak;
  uint16_t curval;
  uint16_t lastval;
  unsigned long lastmeasured;
};

eqBand audiospectrum[EQBANDS] = {    //放大值和带宽取决于麦克风参数响应

  { "125Hz", 1000, 2,   0, 0, 0, 0, 0},
  { "250Hz", 500,  2,   0, 0, 0, 0, 0},
  { "500Hz", 300,  3,   0, 0, 0, 0, 0},
  { "1KHz",  250,  7,   0, 0, 0, 0, 0},
  { "2KHz",  200,  14,  0, 0, 0, 0, 0},
  { "4KHz",  100,  24,  0, 0, 0, 0, 0},
  { "8KHz",  50,   48,  0, 0, 0, 0, 0},
  { "16KHz", 25,   155, 0, 0, 0, 0, 0}
};

int bandWidth[EQBANDS] = {    //建立数组存储变化的频段参数
  0, 0, 0, 0, 0, 0, 0, 0
};


unsigned int sampling_period_us;   //采样间隔时间
unsigned long microseconds;         
double vReal[1024];                   //数据
double vImag[1024];                 //图像
unsigned long newTime;
bool adcread = false; // 读取adc/模拟口读取

float eQGraph[TFT_WIDTH];    // 波形缓存和设置
float wmultiplier = TFT_WIDTH / SAMPLES;
bool wafeformdirtoggler = false;

uint16_t bands = EQBANDS;    // EQ 频段设置
uint16_t bands_width = floor( TFT_WIDTH / bands );  //显示宽度
uint16_t bands_pad = bands_width - (TFT_WIDTH / 16);  //间距
uint16_t colormap[255]; // 波段表的调色板(预填充设置)
uint16_t bands_line_vspacing = 4; //线宽
uint16_t bandslabelheight = 10; //标签高度
uint16_t bandslabelpos = TFT_HEIGHT - bandslabelheight; //标签位置
uint16_t bandslabelmargin = bandslabelheight * 2;  //标签边距
float bands_vratio = 2; // 所有收集的值都将除以这个值,这将影响频带显示的高度
uint16_t audospectrumheight = TFT_HEIGHT / bands_vratio; //频带高度
uint16_t asvstart = TFT_HEIGHT - bandslabelmargin; //频带绘制起点坐标
uint16_t asvend = asvstart - audospectrumheight;    //频带绘制终点坐标

bool displayvolume = true;  // 设置布尔量管理音量显示状态
bool displaywaveform = true; // 设置布尔量管理波形显示状态
bool displayspectrometer = true; // 设置布尔量管理频谱显示状态


long signalAvg = 0, signalMax = 0, signalMin = 4096;  // 设置音量等级
long max1 = 0, max2 = 0, then, now, nowthen;
bool isSaturating = false; //当前音量饱和
bool wasSaturating = false; //音量已经饱和
uint16_t vol = 0; // 存储音量值 (0-4096)
uint16_t volWidth = 0; // 存储音量条显示宽度
float volMod = 1; // fps维护:波形倍增,用更细的线绘制
float lastVolMod = 1; //同上


void drawAudioSpectrumGrid() {   //绘制频谱表
  M5.Lcd.setTextColor(YELLOW);     

  audospectrumheight = TFT_HEIGHT / bands_vratio;   //频带高度
  asvstart = TFT_HEIGHT - bandslabelmargin;             //频带绘制起点
  asvend = asvstart - audospectrumheight - bandslabelmargin;//频带绘制终点

  Serial.println( "Audio Spectrum Height(px): " + String(audospectrumheight) + " Start at:" + String(asvstart) + " End at:" + String(asvend));

  /*
    for(uint8_t i=0;i<TFT_HEIGHT;i++) {
      // debug: prefill with blue
      colormap = M5.Lcd.color565(0, 0, 255);
    }
  */
  for (uint16_t i = asvstart; i >= asvend; i--) {    //更新绘制坐标
    uint16_t projected = map(i, asvend - 1, asvstart + 1, 0, 127);  //
    //Serial.println("pixel[" + String(i) + "] + map(" + String(projected) + ") = rgb(" + String(128 + projected) + ", " + String(255 - projected) + ", 0)");
    colormap = M5.Lcd.color565(255 - projected / 2, 128 + projected, 0);映射坐标修改颜色
  }

  for (byte band = 0; band < bands; band++) {     //绘制标签名称
    M5.Lcd.setCursor(bands_width * band + 2, bandslabelpos);  
    M5.Lcd.print(audiospectrum[band].freqname);
  }
}

void displayBand(int band, int dsize) {   //在指定频段更新显示
  uint16_t hpos = bands_width * band + (bands_pad / 2); //频带高度为
  int dmax = (TFT_HEIGHT / bands_vratio) - 20; // 频带显示最大值
  dsize /= bands_vratio;
  if (dsize > audospectrumheight) {
    dsize = audospectrumheight; // leave some hspace for text
  }
  if (dsize < audiospectrum[band].lastval) {    // 小于最后数值删除上方的线段

    uint8_t bardelta = dsize % bands_line_vspacing;
    for (int s = dsize - bardelta; s <= audiospectrum[band].lastval; s = s + bands_line_vspacing) {
      M5.Lcd.drawFastHLine(hpos, TFT_HEIGHT - (s + 20), bands_pad, BLACK);
    }
  }
  if (dsize > dmax) dsize = dmax;
  for (int s = 0; s <= dsize; s = s + bands_line_vspacing) { //刷新显示
    uint8_t vpos = TFT_HEIGHT - (s + 20);
    M5.Lcd.drawFastHLine(hpos, vpos, bands_pad, colormap[vpos]);
  }
  if (dsize > audiospectrum[band].peak) {   
    audiospectrum[band].peak = dsize;
  }
  audiospectrum[band].lastval = dsize;    //记录最后音量
  audiospectrum[band].lastmeasured = millis(); //记录时间
}


void setBandwidth() {       //设置频带宽度
  byte multiplier = SAMPLES / 256;
  bandWidth[0] = audiospectrum[0].bandWidth * multiplier;
  for (byte j = 1; j < bands; j++) {
    bandWidth[j] = audiospectrum[j].bandWidth * multiplier + bandWidth[j - 1];
  }
  wmultiplier = ((float)TFT_WIDTH / (float)SAMPLES) * 2;
}


byte getBand(int i) {
  for (byte j = 0; j < bands; j++) {
    if (i <= bandWidth[j]) return j;
  }
  return bands;
}


void peakWaveForm() {       //获取峰值
  for (uint16_t i = 0; i < SAMPLES / 2; i++) {
    if (eQGraph >= 0.00005) {
      eQGraph /= 2; //(2+getBand(i));
    }
  }
}

void displayWaveForm(uint16_t color) {  //波形显示
  uint16_t lastx = 1;
  uint16_t lasty = WAVEFORM_YPOS;
  float wSqueeze = WAVEFORM_SQUEEZE;
  uint8_t maxWaveFormHeight = WAVEFORM_YPOS;

  if (color == BLACK) {    //根据音量改变波形颜色
    //isSaturating = wasSaturating;
    //volMod = lastVolMod;
  } else {
    wasSaturating = isSaturating;
    lastVolMod = volMod;
    wafeformdirtoggler = !wafeformdirtoggler;
    uint red = vol / 16;
    color = M5.Lcd.color565(red, 255-red, 0);
  }

  float toLog = 1.5-lastVolMod*1.5;
  if(toLog!=0.00) {
    // https://www.google.com/search?q=y%3D(-log(1.5-x*1.5))*8
    float volSqueezer = (-log(toLog))*8; //计算压缩大小,对数运算
    if(volSqueezer > 0.00) {
      wSqueeze += volSqueezer/*+WAVEFORM_SQUEEZE*/;   //大于0进行压缩
    } else {
      wSqueeze /= -volSqueezer;  //小于0进行放大
    }
  }

  byte wafeformdirection = wafeformdirtoggler ? 1 : 0;  //判断波形方向
  for (uint16_t i = 1; i < SAMPLES / 2; i++) {

    if (eQGraph >= 0.00005) {
      uint tmpy;
      uint nextx = i * wmultiplier;
      uint eQGraphPos = eQGraph //波形绘制位置

      if (eQGraphPos > maxWaveFormHeight) { //
        eQGraphPos = maxWaveFormHeight;
      }

      if (i % 2 == wafeformdirection) {     //波形Y坐标位置
        tmpy = WAVEFORM_YPOS + eQGraphPos;
      } else {
        tmpy = WAVEFORM_YPOS - eQGraphPos;
      }

      if (lasty != 0) {
        //color = color==BLACK ? color : colormap[(byte)TFT_HEIGHT-(eQGraph)];
        M5.Lcd.drawLine(lastx, lasty, nextx, tmpy, color);
      }
      lastx = nextx;
      lasty = tmpy;
    }
  }
  M5.Lcd.drawLine(lastx, lasty, lastx + 1, WAVEFORM_YPOS, color);
  if (lastx < TFT_WIDTH && color != BLACK) {
    M5.Lcd.drawLine(lastx + 1, WAVEFORM_YPOS, TFT_WIDTH, WAVEFORM_YPOS, color);  //绘制直线
  }
}


void captureSoundSample() {   //捕获声音采样
  signalAvg = 0;
  signalMax = 0;
  signalMin = 4096;

  for (int i = 0; i < SAMPLES; i++) {    //读取ADC存入采样数组
    newTime = micros();
    if ( adcread ) {
      vReal = adc1_get_raw( ADC1_CHANNEL_0 ); // adc转换大概花费20us
      delayMicroseconds(20);
    } else {
      vReal = analogRead(36); // 读取模拟量大概花费1us
    }

    vImag = 0;
    if (displayvolume) {    //取得音量平均值
      signalMin = min(signalMin, vReal);
      signalMax = max(signalMax, vReal);
      signalAvg += vReal;
    }

    while ((micros() - newTime) < sampling_period_us) {   //
      // do nothing to wait
      yield();
    }
  }

  FFT.Windowing(vReal, SAMPLES, FFT_WIN_TYP_HAMMING, FFT_FORWARD); //计算频率
  FFT.Compute(vReal, vImag, SAMPLES, FFT_FORWARD);
  FFT.ComplexToMagnitude(vReal, vImag, SAMPLES);
}





void renderSpectrometer() {   //量化显示音量
  if (displayvolume) {
    signalAvg /= SAMPLES;
    vol = (signalMax - signalMin);
    volWidth = map(vol, 0, 4096, 1, TFT_WIDTH);
    volMod = (float) map(vol, 0, 4096, 1, 1000) / 1000;
    M5.Lcd.drawFastHLine(0, 20, volWidth, GREEN);
    M5.Lcd.drawFastHLine(volWidth, 20, TFT_WIDTH - volWidth, BLACK);
    if (volMod >= .25) isSaturating = true;
    else isSaturating = false;
  }

  if (displaywaveform) {
    displayWaveForm(BLACK);
    peakWaveForm();
  }

  for (int i = 2; i < (SAMPLES / 2); i++) {   // 采样不能为0,每个数组元素表示一个频率和振幅
    if (vReal > 512) { // 滤除噪音,10倍于采样
      byte bandNum = getBand(i);
      if (bandNum != bands) {
        audiospectrum[bandNum].curval = (int)vReal / audiospectrum[bandNum].amplitude;
        if (displayspectrometer) {
          displayBand(bandNum, audiospectrum[bandNum].curval);
        }
        if (displaywaveform) {
          eQGraph += audiospectrum[bandNum].curval;
        }
      }
    }
  }

  if (displaywaveform) {
      displayWaveForm(GREEN);
  }

  if (displayspectrometer) {    // 显示频谱,50ms刷新一次
long vnow = millis();
    bool peakchanged = false;
    for (byte band = 0; band < bands; band++) {
      if (vnow - audiospectrum[band].lastmeasured > 50) {
        displayBand(band, audiospectrum[band].lastval > bands_line_vspacing ? audiospectrum[band].lastval - bands_line_vspacing : 0);

      }
      if (audiospectrum[band].peak > 0) {  //
        audiospectrum[band].peak -= bands_line_vspacing;//(band/3)+2;
        if (audiospectrum[band].peak <= 0) {
          audiospectrum[band].peak = 0;
}
      }


if (audiospectrum[band].lastpeak != audiospectrum[band].peak) {  //删除最后的峰值
        peakchanged = true;

        M5.Lcd.drawFastHLine(bands_width * band + (bands_pad / 2), TFT_HEIGHT - audiospectrum[band].lastpeak - 20, bands_pad, BLACK);
        audiospectrum[band].lastpeak = audiospectrum[band].peak;
        M5.Lcd.drawFastHLine(bands_width * band + (bands_pad / 2), TFT_HEIGHT - audiospectrum[band].peak - 20, bands_pad, colormap[TFT_HEIGHT - audiospectrum[band].peak - 20]);
      }
    }
  }
}


void handleSerial() {      //串口控制
  if (Serial.available()) {
    // toggle display modes
    char c = Serial.read();
    if (displaywaveform) {
      displayWaveForm(BLACK);
      memset(eQGraph, 0, TFT_WIDTH);
    }
    switch (c) {
      case 'a':
        adcread = !adcread;
        Serial.println("Use adc RAW " + String(adcread));
        M5.Lcd.fillScreen(BLACK);
        break;
      case 'v':
        displayvolume = !displayvolume;
        Serial.println("Volume " + String(displayvolume));
        M5.Lcd.fillScreen(BLACK);
        break;
      case 'w':
        displaywaveform = !displaywaveform;
        Serial.println("Waveform " + String(displaywaveform));
        M5.Lcd.fillScreen(BLACK);
        break;
      case 's':
        displayspectrometer = !displayspectrometer;
        Serial.println("Spectro " + String(displayspectrometer));
        M5.Lcd.fillScreen(BLACK);
        break;
      case '+':
        SAMPLES *= 2;
        Serial.println("Sampling buffer " + String(SAMPLES));
        break;
      case '-':
        if (SAMPLES / 2 > 32) {
          SAMPLES /= 2;
        }
        Serial.println("Sampling buffer " + String(SAMPLES));
        break;
      case '*':
        if ( bands + 1 <= 8 ) bands++;
        bands_width = floor( TFT_WIDTH / bands );
        bands_pad = bands_width - (TFT_WIDTH / 16);
        M5.Lcd.fillScreen(BLACK);
        Serial.println("EQ Bands " + String(bands));
        break;
      case '/':
        if ( bands - 1 > 0 ) bands--;
        bands_width = floor( TFT_WIDTH / bands );
        bands_pad = bands_width - (TFT_WIDTH / 16);
        M5.Lcd.fillScreen(BLACK);
        Serial.println("EQ Bands " + String(bands));
        break;
      case '@':
        int SAMPLESIZE = SAMPLES / 2;
        int i = 0;

        for (int mag = 1; mag < SAMPLESIZE; mag = mag * 2) {
          byte magmapped = map(mag, 1, SAMPLESIZE, 1, TFT_WIDTH);
          Serial.println("#" + String(i) + " " + String(magmapped) + " " + String(mag)  );
          i++;
        }
        /*
          for (int i = 2; i < (SAMPLES/2); i++){ // Don't use sample 0 and only first SAMPLES/2 are usable. Each array element represents a frequency and its value the amplitude.
          if (vReal > 512) { // Add a crude noise filter, 10 x amplitude or more
            //byte bandNum = getBand(i);
          }
          }*/

        break;

    }
    if (displayspectrometer) {
      drawAudioSpectrumGrid();
    }
    setBandwidth();
    max1 = 0;
    max2 = 0;
  }
}


void setup() {
  WiFi.mode(WIFI_MODE_NULL);
  M5.begin();
  adc1_config_width(ADC_WIDTH_12Bit);   //ad数值范围 0-1023
  adc1_config_channel_atten(ADC1_CHANNEL_0, ADC_ATTEN_11db); //11db电压为 0-3.6V
  sampling_period_us = round(1000000 * (1.0 / SAMPLING_FREQUENCY));

  M5.begin();
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextColor(YELLOW);
  M5.Lcd.setRotation( 3 );
  M5.Lcd.setCursor(98, 42);
  M5.Lcd.print("Sampling at: " + String(sampling_period_us) + "uS");

  delay(1000);

  M5.Lcd.fillScreen(BLACK);

  drawAudioSpectrumGrid();
  setBandwidth();
  memset(eQGraph, 0, TFT_WIDTH);
}




void loop() {

  handleSerial();
  captureSoundSample();
  renderSpectrometer();

}[/mw_shl_code]

该用户从未签到

发表于 2020-7-25 14:16 | 显示全部楼层
good article

该用户从未签到

发表于 2020-7-30 00:18 | 显示全部楼层
adc.h这个头文件在哪个库里面,找不到呢

该用户从未签到

 楼主| 发表于 2020-7-30 09:06 | 显示全部楼层
13580445117 发表于 2020-7-30 00:18
adc.h这个头文件在哪个库里面,找不到呢

ESP32自带的库

该用户从未签到

发表于 2020-8-1 00:21 | 显示全部楼层
很复杂,老哥我要请教

该用户从未签到

发表于 2020-8-3 09:45 | 显示全部楼层
本帖最后由 13580445117 于 2020-8-3 19:51 编辑

请教:如何将数据控制在1-32这个范围,我只要32频率段就行,该调整哪个参数
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

热门推荐

Arduino数字与字母字体应该如何设置?
Arduino数字与字母字体应
尝试用Arduino uno 做了个小工具,用来控制电脑水冷的运行。 洞洞板已经测试成功,完
WEMOS LOLIN32Lite(ESP32v1.0.0Rev1)入手+引脚图
WEMOS LOLIN32Lite(ESP32v
突发奇想做个精致的蓝牙遥控平衡小车(大学时做过一次,比较笨,用洞洞板焊电路做的比
【Arduino】168种传感器模块系列实验(129)---BH1750光照传感器
【Arduino】168种传感器模
37款传感器与模块的提法,在网络上广泛流传,其实Arduino能够兼容的传感器模块肯定是
16度双足机器人舵机驱动板pca9685连接舵机,舵机没反应
16度双足机器人舵机驱动板
各位同为arduino爱好者的大佬们大家好!想请教一下大家有关舵机驱动板pca9685
pca9685+arduino驱动舵机失败
pca9685+arduino驱动舵机
我网上别人的代码,编译通过且上传,但舵机不转,怎么解决?Arduino uno使用PCA9685模
Copyright   ©2015-2016  Arduino中文社区  Powered by©Discuz!   
快速回复 返回顶部 返回列表