回到首页 返回首页
回到顶部 回到顶部
返回上一页 返回上一页

【Arduino 动手做】Fast Audio 快速音频频谱分析仪 简单

头像 驴友花雕 2025.07.11 6 0

这个设计如果不是惊人的话,也没什么!想象一下 3500 个彩色 LED 以非常高的刷新率随着音乐跳舞。所有这些都由一个 2 美元的微控制器控制,该微控制器使用蓝牙连接馈送音乐。

最重要的是,它是开源的,因此任何人都可以构建它!

蓝牙音频连接
使用 14 个串行连接同时驱动 3584 个 PixelLED
I2S 音频输出
120 帧/秒
背景动画
展示的卡通人物
我有没有提到它真的很快?每秒 120 帧...这意味着微控制器将在 8.3 毫秒内驱动 3584 个 LED。每一帧都会显示输入信号的新实际频率数据。

作为奖励,它还将显示动画背景,同时一些卡通人物出现在屏幕上。
吹牛够了......让我们开始构建吧!

用品
1 个 EPS32 开发套件
14 个 LED 面板 16 x 16 个 LED
1 个电源 I 使用的 MeanWell S-25-5 输出 5V / 5A
1 个 PCB (稍后会详细介绍)
一些插座、接头和电线
14 个保险丝 1AT
1 个 I2S 音频板 CJMCU-5102
如果您想听到音频,您还必须连接一组(放大的)扬声器

作这款最令人印象深刻的音频分析仪。

连接一组(放大的)扬声器或模拟扬声器来收听音频。
我首先使用了一组放大的电脑扬声器。
通过蓝牙建立连接
只需找到并连接到名为 Lightnsound 的分析器
开始在设备上播放音频文件
使用按钮打开和关闭背景,使用 OF 更改颜色
使用电位计设置面板的亮度
如果调暗,某些字符可能会消失。

 

01.jpg
02.jpg
03.jpg
04.jpg
04-.jpg
05.jpg
06.jpg
07.jpg
08.jpg
09.jpg
10.jpg
F6DBAZSM16G2OMI.jpg
FEAC6INM19AYME6.png
FOZ2NWWM1UQKCG2.png

项目代码

 

 

代码
/*
 * Bluetooth spectrum analyzer V1.0
TODO
DONE 4096 (is it even possible?) IT IS POSSIBLE
DONE 128 bands might be possible too at 45 fps. It's actually possible at 115 fps.
- Change backgound dynamically
- Animated backgound
  - Perhaps GIFs

DONE Get samples based on the time the last loop took + 1 %
- Retain both channels so I can analyze them both separately if I wawnt to.
  I'll do the conversion to 16 bits on core 1 instead of core 0.
  This might cost me some microseconds but probably not much since they are just bitwise operations.
  - Print the channels mirrored and not mirrored and reversed.

- Hard code several different images. I can also flip them to get variety.
- Flip analyzer upside down. Maybe I can come up with an elegant solution to all these flips.
  Perhaps I can flip images in place or have four different funtions to flip the images dynamically while I do the conversion from 2D to snakewise panel.
- Slow the falling down
- Add peaks that fall down. Maybe they start slowly falling down right away or wait a while.


*/

/*
    Bluetooth Speaker Spectrum Analyzer
    Copyright (C) 2022 Antti Yliniemi
    
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as
    published by the Free Software Foundation, either version 3 of the
    License, or (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

/*
I use these libraries:
https://github.com/hpwit/I2SClocklessLedDriver                // I use a heavily modified version of this library. Will be putting a pull request on his github repo after I've done more testing.
https://github.com/yliniemi/I2SClocklessLedDriver/tree/dev    // This is my current heavily modified version. Perhaps the changes end up in the hpwit's version some day
https://github.com/fakufaku/esp32-fft                         // This is the fastest FFT library I could find. Analyzing 4096 samples takes 5 milliseconds.
https://github.com/pschatzmann/ESP32-A2DP
*/


/*
FQBN: esp32:esp32:esp32doit-devkit-v1:PartitionScheme=no_ota
Using board 'esp32doit-devkit-v1' from platform in folder: C:\Users\chord\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.6
Using core 'esp32' from platform in folder: C:\Users\chord\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.6
Using library ESP32-A2DP at version 1.7.1 in folder: C:\Users\chord\Documents\Arduino\libraries\ESP32-A2DP-main 
Using library I2SClocklessLedDriver at version 1.0.0 in folder: C:\Users\chord\Documents\Arduino\libraries\I2SClocklessLedDriver-main 
*/

#include "BluetoothA2DPSink.h"
extern "C"
{
   #include "fft.h"
}

struct CRGB {
  union {
    struct {
            union {
                uint8_t r;
                uint8_t red;
            };
            union {
                uint8_t g;
                uint8_t green;
            };
            union {
                uint8_t b;
                uint8_t blue;
            };
        };
    uint8_t raw[3];
  };
};

struct FloatOffset
{
  float x;
  float y;
};

#include "constants.h"

// #define STRESS_RAM



enum MapMode
{
  RAINBOW,
  TEXTURES
};
MapMode mapMode = RAINBOW;
//MapMode mapMode = TEXTURES;

#define TILE_WIDTH 32
#define TILE_HEIGHT 32
#define ENABLE_LEDMAP
#define PANEL_WIDTH 16
#define PANEL_HEIGHT 16
#define NUM_PANELS_PER_ROW 7
#define NUM_PANELS_PER_COLUMN 2
#define SCREEN_WIDTH PANEL_WIDTH * NUM_PANELS_PER_ROW
#define SCREEN_HEIGHT PANEL_HEIGHT * NUM_PANELS_PER_COLUMN
#define ATX_POWER_ON 13
#define DEFAULT_SAMPLE_RATE 44100
#define HIGHEST_FREQUENCY 20000
#define START_SPECTRUM_AT_THIS_BIN 3
#define ADD_TO_DELTA 0.5

int isrCounter=0;


// by mark
const uint8_t InterruptPin = 35;
bool Request;
void IRAM_ATTR isr() {
 Request = true;
}


int BRIGHTNESSMARK = 0;
#define BRIGHTNESSPOT 34

#define BRIGHTNESSMAX 100  // read brightness potmeter


// TaskHandle_t task1, task2, task3;

// has to be between 512 - 2048
// more than 2048 and we run out of ram. less than 512 and we run out of time to draw it on the led panel
// 512 => 86 fps, 1024 => 43 fps, 2048 => 21 fps
#define SAMPLES 4096 //4096
// #define WAIT_UNTIL_DRAWING_DONE
#define SECONDS_BETWEEN_DEBUG 60


#define BANDS SCREEN_WIDTH
const float dynamicRange = 5.0;    // in bels, not decibels. bel is ten decibels. it's metric. bel is the base unit. long live the metric.
// #define PRINT_PLOT
#define DEBUG false
// #define DEBUG true
// #define PRINT_BANDS
// #define PRINT_CEILING
#define USE_SERIAL
// #define PRINT_OUTPUT
// #define PRINT_PEAKS
//#define PRINT_FFT_TIME
//#define PRINT_ALL_TIME
//#define PRINT_FASTLED_TIME
//#define PRINT_MAPLEDS_TIME
//#define PRINT_RAM                   // This uses some resources that block the isr and take a long time
#ifdef STRESS_RAM
//#define PRINT_RAM
#endif
// #define PRINT_INDEXES
// #define TEST_FULL_BUFFER
//#define PRINT_SAMPLE_RATE
//#define PRINT_LESSER_SAMPLES
//#define PRINT_BUFFER_FULL

// Kaiser windowing has the best reduction of side lobes and somewhat narrow main lobe. The other windows don't even hold a candle.

int maxCurrent = 15000;
// int maxBrightness = 32;

#define BUFFER_LENGTH 3072   // 2048 is also just fine. maybe 4096 has too much extra space 3078
IRAM_ATTR int32_t bufferRing[BUFFER_LENGTH] = {};
volatile int writeIndex = 0;   // writeIndex will stay 4 behind readIndex. it can't go past
volatile int readIndex = 0;    // readIndex can be equal to writeIndex but cannot advance
volatile int bufferFull = 0;  
volatile int lesserSamples = 0;

IRAM_ATTR int32_t realRing[SAMPLES] = {};

int realRingIndex = 0;

const int numLeds = NUM_PANELS_PER_ROW * NUM_PANELS_PER_COLUMN * PANEL_WIDTH * PANEL_HEIGHT;

// uint8_t *leds = NULL;
CRGB *leds;

uint32_t loopMicros = 0;
uint32_t loopCycles = 0;

float deltaRatio = 0;

static uint16_t ledMappingFunction(uint16_t hardwareLed);
#define __SOFTWARE_MAP
#include "I2SClocklessLedDriver.h"
I2SClocklessLedDriver driver;


char BTname[] = "Lightnsound";
BluetoothA2DPSink a2dp_sink;

OffsetDisplay offd;
FloatOffset groundOffset;
FloatOffset skyOffset;

enum {bluetooth, microphone, artnet} programMode;

__attribute__((always_inline)) IRAM_ATTR static uint16_t ledMappingFunction(uint16_t hardwareLed)
{
  int x, y, moduloRow;
  x = (hardwareLed / PANEL_HEIGHT) % (PANEL_WIDTH * NUM_PANELS_PER_ROW);
  moduloRow = x % 2;

  #ifdef SIGNAL_STARTS_FROM_THE_BOTTOM
  y = (moduloRow * hardwareLed + (1 - moduloRow) * (PANEL_HEIGHT * 12345 - 1 - hardwareLed)) % PANEL_HEIGHT + (hardwareLed / (PANEL_WIDTH * PANEL_HEIGHT * NUM_PANELS_PER_ROW)) * PANEL_HEIGHT;
  #else
  y = ((1 - moduloRow) * hardwareLed + moduloRow * (PANEL_HEIGHT * 12345 - 1 - hardwareLed)) % PANEL_HEIGHT + (hardwareLed / (PANEL_WIDTH * PANEL_HEIGHT * NUM_PANELS_PER_ROW)) * PANEL_HEIGHT;
  #endif
  
  x = (x + driver._offsetDisplay.offsetx) % driver._offsetDisplay.panel_width;

  return x + y * driver._offsetDisplay.panel_width;

}

float sqrtApprox(float number)
{
  union { float f; uint32_t u; } y = {number};
  y.u = 0x5F1FFFF9ul - (y.u >> 1);
  return number * 0.703952253f * y.f * (2.38924456f - number * y.f * y.f);
}

CRGB redToBlue(float hue)
{
  CRGB color;
  color.r = std::max((float)255 * (1 - 2 * hue), (float)0) * 0.094 * 2.5;
  color.g = std::max(min(hue * 2 * 255, (1 - hue) * 2 * 255), (float)0) * 0.113 * 2.5;
  color.b = std::max((float)255 * (hue * 2 - 1), (float)0) * 0.080 * 2.5;
  return color;
}

/*
void mapLeds()
{
  for (int i = 0; i < BANDS; i++)
  {
    for (int j = 0; j < 16; j++)
    {
      if ((bands[i] * 16 - j) >= 0) leds[ledMap[i * 16 + j]] = templateLeds[i * 16 + j];
      else if ((bands[i] * 16 - j) > -1)
      {
        leds[ledMap[i * 16 + j]].r = ((float)templateLeds[i * 16 + j].r + 1.0) * (bands[i] * 16.0 - j + 1.0);   // I add one to the color so that if it's small it doesn't disappear first. now if the color is one, it will disappear in the middle.
        leds[ledMap[i * 16 + j]].g = ((float)templateLeds[i * 16 + j].g + 1.0) * (bands[i] * 16.0 - j + 1.0);
        leds[ledMap[i * 16 + j]].b = ((float)templateLeds[i * 16 + j].b + 1.0) * (bands[i] * 16.0 - j + 1.0);
      }
      else leds[ledMap[i * 16 + j]] = {0, 0, 0};
    }
  }
}
*/

/*
void mapLeds()
{
  for (int x = 0; x < SCREEN_WIDTH; x++)
  {
    for (int y = 0; y < SCREEN_HEIGHT; y++)
    {
        leds[x + y * SCREEN_WIDTH].r = 4;
    }
  }
}
*/

void mapLeds_rainbow(float* bands)
{
  for (int x = 0; x < BANDS; x++)
  {
    CRGB color = redToBlue(((float)x / (BANDS - 1)) * sqrtApprox((float)x / (BANDS - 1)));
    for (int y = 0; y < SCREEN_HEIGHT; y++)
    {
      if ((bands[x] * SCREEN_HEIGHT - (SCREEN_HEIGHT - 1 - y)) >= 0) leds[x + SCREEN_WIDTH * y] = color;
      else if ((bands[x] * SCREEN_HEIGHT - (SCREEN_HEIGHT - 1 - y)) > -1)
      {
        leds[x + SCREEN_WIDTH * y].r = ((float)color.r + 0.999) * (bands[x] * SCREEN_HEIGHT - (SCREEN_HEIGHT - 1 - y) + 0.999);
        leds[x + SCREEN_WIDTH * y].g = ((float)color.g + 0.999) * (bands[x] * SCREEN_HEIGHT - (SCREEN_HEIGHT - 1 - y) + 0.999);
        leds[x + SCREEN_WIDTH * y].b = ((float)color.b + 0.999) * (bands[x] * SCREEN_HEIGHT - (SCREEN_HEIGHT - 1 - y) + 0.999);
      }
      else leds[x + SCREEN_WIDTH * y] = {0, 0, 0};
    }
  }
}

__attribute__((always_inline)) void mapLeds_textures(int x, FloatOffset groundOffset, FloatOffset skyOffset, float* bands)
{
    for (int y = 0; y < SCREEN_HEIGHT; y++)
    {
      CRGB groundColor = ((CRGB*)yellow_sand)[((y + (int)groundOffset.y) % TILE_HEIGHT) * TILE_WIDTH + (x + (int)groundOffset.x) % TILE_WIDTH];
      groundColor.r = groundColor.r / 4;
      groundColor.g = groundColor.g / 64;
      groundColor.b = groundColor.b / 64;
      CRGB skyColor = ((CRGB*)dark_blue_water)[((y + (int)skyOffset.y) % TILE_HEIGHT) * TILE_WIDTH + (x + (int)skyOffset.x) % TILE_WIDTH];
      skyColor.r = skyColor.r / 64;
      skyColor.g = skyColor.g / 64;
      skyColor.b = skyColor.b / 64;

      if ((bands[x] * SCREEN_HEIGHT - (SCREEN_HEIGHT - 1 - y)) >= 0)
      {
        leds[x + SCREEN_WIDTH * y] = groundColor;
      }
      else if ((bands[x] * SCREEN_HEIGHT - (SCREEN_HEIGHT - 1 - y)) > -1)
      {
        float skyPortion = SCREEN_HEIGHT - 1 - y - bands[x] * SCREEN_HEIGHT;
        float groundPortion = 1 - skyPortion;
        leds[x + SCREEN_WIDTH * y].r = (float)groundColor.r * groundPortion + (float)skyColor.r * skyPortion;
        leds[x + SCREEN_WIDTH * y].g = (float)groundColor.g * groundPortion + (float)skyColor.g * skyPortion;
        leds[x + SCREEN_WIDTH * y].b = (float)groundColor.b * groundPortion + (float)skyColor.b * skyPortion;
      }
      else
      {
        leds[x + SCREEN_WIDTH * y] = skyColor;
      }
    }
}

void drawCharacter(const CRGB* character, float startingColumn, float offSet, float columnsPerFrame, int numberOfFrames, int frameWidth, int frameHeight)
{

  int frame = (int)((startingColumn + offSet) / columnsPerFrame) % numberOfFrames;
 
  for (int x = (int)(startingColumn + 100) - 100; x < (int)startingColumn + frameWidth; x++)
  {
    if (x >= 0 && x < SCREEN_WIDTH)
    {
      for (int y = max(SCREEN_HEIGHT - frameHeight, 0); y < SCREEN_HEIGHT; y++)
      {
        int characterPixel = x - (int)(startingColumn + 100) + 100 + frame * frameWidth + (y - SCREEN_HEIGHT + frameHeight) * (numberOfFrames * frameWidth);
        // if (character[characterPixel].r != 0xFE && character[characterPixel].g != 0xFD && character[characterPixel].b != 0xFC)
        if ((*(uint32_t*)&(character[characterPixel]) & 0x00FFFFFF) != 0x00FCFDFE)    // I don't know why it's not 0xFEFDFC. It's because esp32 is little endian.
        {
          leds[x + y * SCREEN_WIDTH].r = character[characterPixel].r / 16;
          leds[x + y * SCREEN_WIDTH].g = character[characterPixel].g / 16;
          leds[x + y * SCREEN_WIDTH].b = character[characterPixel].b / 16;
        }
      }
    }
  }
  
}



/*
void mapLeds()     // upside down
{
  for (int x = 0; x < BANDS; x++)
  {
    CRGB color = redToBlue(((float)x / (BANDS - 1)) * sqrtApprox((float)x / (BANDS - 1)));
    for (int y = 0; y < SCREEN_HEIGHT; y++)
    {
      if ((bands[x] * SCREEN_HEIGHT - y) >= 0) leds[x + SCREEN_WIDTH * y] = color;
      else if ((bands[x] * 32 - y) > -1)
      {
        leds[x + SCREEN_WIDTH * y].r = ((float)color.r + 0.999) * (bands[x] * SCREEN_HEIGHT - y + 0.999);
        leds[x + SCREEN_WIDTH * y].g = ((float)color.g + 0.999) * (bands[x] * SCREEN_HEIGHT - y + 0.999);
        leds[x + SCREEN_WIDTH * y].b = ((float)color.b + 0.999) * (bands[x] * SCREEN_HEIGHT - y + 0.999);
      }
      else leds[x + SCREEN_WIDTH * y] = {0, 0, 0};
    }
  }
}
*/

void substractAverage(float* inputReal)
{
  float average = 0;
  for (int i = 0; i < SAMPLES; i++)
  {
    average += inputReal[i];
  }
  average /= SAMPLES;
  for (int i = 0; i < SAMPLES; i++)
  {
    inputReal[i] = inputReal[i] - average;
  }
}

void doWindowing(float* inputReal)
{
  for (int i = 0; i < SAMPLES; i++)
  {
    inputReal[i] = inputReal[i] * windowingArray[i];
  }
}

void audio_data_callback(const uint8_t *data, uint32_t byteLen)
{
  int actualLen = byteLen / 4;
  if ((readIndex - writeIndex + BUFFER_LENGTH - 1) % BUFFER_LENGTH <= actualLen)
  {
    bufferFull++;
  }
  else
  {
    for (int i = 0; i < actualLen; i++)
    {
      // bufferRing[(writeIndex + i) % BUFFER_LENGTH] = *(int16_t*)&(data[i * 4]) / 2 + *(int16_t*)&(data[i * 4 + 2]) / 2;
      bufferRing[(writeIndex + i) % BUFFER_LENGTH] = ((int)*(int16_t*)&(data[i * 4]) + (int)*(int16_t*)&(data[i * 4 + 2]));
    }
  }
  writeIndex = (writeIndex + actualLen) % BUFFER_LENGTH;
  
  #ifdef PRINT_INDEXES
  Serial.print("Got ");
  Serial.print(actualLen);
  Serial.print(" data, writeIndex = ");
  Serial.println(writeIndex);
  #endif
}

void startAudio()
{
  i2s_config_t i2s_config = {
    .mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_TX),
    .sample_rate = DEFAULT_SAMPLE_RATE, // updated automatically by A2DP
    .bits_per_sample = (i2s_bits_per_sample_t)16,
    .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
    .communication_format = (i2s_comm_format_t) (I2S_COMM_FORMAT_STAND_I2S),
    .intr_alloc_flags = 0, // default interrupt priority
    .dma_buf_count = 8,
    .dma_buf_len = 64,
    .use_apll = true,
    .tx_desc_auto_clear = true // avoiding noise in case of data unavailability
  };
  a2dp_sink.set_i2s_config(i2s_config);
  a2dp_sink.set_i2s_port(I2S_NUM_1);
  a2dp_sink.set_stream_reader(audio_data_callback);
  

  i2s_pin_config_t my_pin_config = {
      .bck_io_num = 4,
      .ws_io_num = 15,
      .data_out_num = 16,
      .data_in_num = I2S_PIN_NO_CHANGE };

  a2dp_sink.set_pin_config(my_pin_config);
  // a2dp_sink.set_auto_reconnect(false);    // maybe this helps with my compatibility problem
  
  a2dp_sink.start(BTname);
  Serial.println(String("Started Bluetooth audio receiver with the name ") + BTname);
}

bool readBuffer(float* inputReal)
{
  int newSamples = min(min(SAMPLES, a2dp_sink.sample_rate() * (int)loopMicros / 990000), BUFFER_LENGTH);
  if ((writeIndex - readIndex + BUFFER_LENGTH) % BUFFER_LENGTH < newSamples / 2)   // i only ask for half as many samples to be present
  {
    // delay(1);
    return false;
  }
  if ((writeIndex - readIndex + BUFFER_LENGTH) % BUFFER_LENGTH < newSamples)
  {
    // delay(1);
    newSamples = (writeIndex - readIndex + BUFFER_LENGTH) % BUFFER_LENGTH;
    lesserSamples = newSamples;
  }
  
  {
    for (int i = 0; i < newSamples; i++)
    {
      realRing[realRingIndex] = (bufferRing[readIndex]);
      realRingIndex = (realRingIndex + 1) % SAMPLES;
      readIndex = (readIndex + 1) % BUFFER_LENGTH;
    }
    #ifdef PRINT_INDEXES
    Serial.println(String("readIndex = ") + readIndex + ", realRingIndex = " + realRingIndex);
    #endif
  }

  // DC-offset removal goes here. I don't yet know how. I need a few variables of ram.
  // But since I do overlapping fft, I need to do the DC removal again but the variables are continous.
  // I'm not going to save a history of them. Perhaps I need to calculate them again
  // by first calculating them to the point where I delete the circular buffer and save them for use the next loop.
  // only after that I'll read the buffer. I'll need to do the calculations like 7 times more than I should if I had an extra 16 kB.
  // I know the most probable amount of new samples I will use.
  // Only when I reach the head of the ring buffer I need to get variable amount of samples.
  // This only saves 15 % of calculating the DC-offset. Perhaps it's not worth it.

  // I could separate the channels at this point too. I can get them for free now since they are in IRAM.
  for (int i = 0; i < SAMPLES; i++)
  {
    inputReal[i] = (float)realRing[(realRingIndex + i) % SAMPLES];
  }
  return true;
}

void powerOfTwo(float* output)
{
  for (int index = 0; index < SAMPLES / 2; index++)
  {
    output[index] = output[index * 2] * output[index * 2] + output[index * 2 + 1] * output[index * 2 + 1];
  }
}

void zeroSmallBins(float* output)
{
  float biggest = 0;
  for (int i = 0; i < SAMPLES / 2; i++)
  {
    if (biggest < output[i]) biggest = output[i];
  }
  for (int i = 0; i < SAMPLES / 2; i++)
  {
    // we get rid of the unwanted frequency side lobes this way. kaiser 2 is quite eficient and we shouldn't see more than -60 dB on the side lobes
    // at the same time we make sure that we don't take logarithm out of zero. that would be -infinite
    output[i] = std::max(output[i] - biggest * 0.0000003, 0.0000000001);
    }
}

/*
void powTwoBands(float* output, float* bands)
{
  for (int i = 0; i < BANDS; i++)
  {
    bands[i] = 0;
    #if SAMPLES == 4096
    #if BANDS == 112
    for (int j = bins_4096_112[i]; j < bins_4096_112[i + 1]; j++)
    #endif
    #if BANDS == 64
    for (int j = bins_4096_64[i]; j < bins_4096_64[i + 1]; j++)
    #endif
    #if BANDS == 7
    for (int j = bins_4096_7[i]; j < bins_4096_7[i + 1]; j++)
    #endif
    #endif 
    #if SAMPLES == 2048
    for (int j = bins_2048_64[i]; j < bins_2048_64[i + 1]; j++)
    #endif
    
    {
      bands[i] += output[j];
    }
  }
}
*/

void logBands(float* bands, float* peakBands)
{
  for (int i = 0; i < BANDS; i++)
  {
    bands[i] = log10f(bands[i]);
    #ifdef PRINT_PEAKS
    if (peakBands[i] < bands[i]) peakBands[i] = bands[i];
    #endif
    #if BANDS == 7
    bands[i] = bands[i] - substract_7[i] + 6;
    #endif
    #if BANDS == 16
    bands[i] = bands[i] - substract_16[i] + 6;
    #endif
    #if BANDS == 32
    bands[i] = bands[i] - substract_32[i] + 6;
    #endif
    #if BANDS == 48
    bands[i] = bands[i] - substract_48[i] + 6;
    #endif
    #if BANDS == 64
    bands[i] = bands[i] - substract_64[i] + 6;
    #endif
    #if BANDS == 112
    bands[i] = bands[i] - substract_112[i] + 6;
    #endif
  }
}

void normalizeBands(float* bands)
{
  static float bandCeiling = 0.1;
  #ifdef PRINT_CEILING
  Serial.println(String("bandCeiling = ") + bandCeiling);
  #endif
  bandCeiling -= 0.00005;          // now it takes 200 seconds to come 10 dB down
  for (int i = 0; i < BANDS; i++)
  {
    if (bands[i] > bandCeiling) bandCeiling = bands[i];
  }
  for (int i = 0; i < BANDS; i++)
  {
    bands[i] = (bands[i] - bandCeiling + dynamicRange) / dynamicRange;
  }
}

  /* These will be combined to one function
  powTwoBands(output, bands);
  logBands(bands, peakBands);
  normalizeBands(bands);
  */
void powerBinsToBands(float* output, float* bands)
{
  int32_t startingPoint = START_SPECTRUM_AT_THIS_BIN;
  for (int32_t i = 0; i < BANDS; i++)
  {
    bands[i] = 0;
    int32_t delta = (startingPoint * deltaRatio) + ADD_TO_DELTA;
    if (delta <= 0) delta = 1;
    for (int32_t j = 0; j < delta; j++)
    {
      bands[i] += output[startingPoint + j];
    }
    startingPoint += delta;
    bands[i] = log10f(bands[i]);
    #ifdef PRINT_PEAKS
    if (peakBands[i] < bands[i]) peakBands[i] = bands[i];
    #endif
    // if (delta < 1) delta = 1;
    if (delta > 6) delta = 6;
    bands[i] -= substract_universal[delta - 1];
  }
  static float bandCeiling = -1000000;
  #ifdef PRINT_CEILING
  Serial.println(String("bandCeiling = ") + bandCeiling);
  #endif
  bandCeiling -= 0.00002;          // now it takes 300 seconds to come 10 dB down
  for (int i = 0; i < BANDS; i++)
  {
    if (bands[i] > bandCeiling) bandCeiling = bands[i];
  }
  for (int i = 0; i < BANDS; i++)
  {
    bands[i] = (bands[i] - bandCeiling + dynamicRange) / dynamicRange;
  }
}

float __attribute__ ((noinline)) checkDeltaRatio(float ratio, float startingPoint, int32_t rounds)
{
    for (int32_t i = 0; i < rounds; i++)
    {
        float delta = floorf((startingPoint * ratio) + ADD_TO_DELTA);
        if (delta <= 0) delta = 1;
        startingPoint += delta;
    }
    return startingPoint;
}

float __attribute__ ((noinline)) findDeltaRatio(int32_t maxDepth, float low, float high, float lowestValid, float startingPoint, int32_t endGoal, int32_t rounds)
{
    float middle = (low + high) * 0.5;
    float result = checkDeltaRatio(middle, startingPoint, rounds);
    // Serial.printf("%.17f : %f\n", middle, result);
    if (maxDepth <= 0) return lowestValid;    
    if (result > endGoal)
    {
        return findDeltaRatio(maxDepth - 1, low, middle, lowestValid, startingPoint, endGoal, rounds);
    }
    else
    {
        return findDeltaRatio(maxDepth - 1, middle, high, middle, startingPoint, endGoal, rounds);
    }
}

void setup()
{

  
  #ifdef USE_SERIAL
  Serial.begin(115200);
  #endif
  programMode = bluetooth;
  Serial.printf("programMode = %d\n\r", programMode);
  Serial.println("Just booted up");
  Serial.print("ESP.getFreeHeap() = ");
  Serial.println(ESP.getFreeHeap());
  Serial.print("heap_caps_get_largest_free_block(MALLOC_CAP_8BIT) = ");
  Serial.println(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT));
  Serial.print("heap_caps_get_largest_free_block(MALLOC_CAP_32BIT) = ");
  Serial.println(heap_caps_get_largest_free_block(MALLOC_CAP_32BIT));
  delay(100);
  int32_t lastBin = min(SAMPLES / 2 - 1, HIGHEST_FREQUENCY * SAMPLES / DEFAULT_SAMPLE_RATE);
  deltaRatio = findDeltaRatio(25, 0, 0.2, 0, START_SPECTRUM_AT_THIS_BIN, lastBin, BANDS);
  Serial.printf("deltaRatio = %f\n", deltaRatio);
  Serial.printf("numLeds = %d\n", numLeds);
  Serial.printf("SCREEN_WIDTH = %d\n", SCREEN_WIDTH);
  Serial.printf("SCREEN_HEIGHT = %d\n", SCREEN_HEIGHT);
  
 
  int pins[] = {32, 33, 25, 26, 27, 14, 12, 23, 22, 21, 19, 18, 5, 17};              // esp32 dev kit v1
  

  #ifndef WAIT_UNTIL_DRAWING_DONE
  driver.__displayMode = NO_WAIT;
  #endif
  
  #ifdef STRESS_RAM
  leds = (CRGB*)calloc(numLeds * 2, sizeof(CRGB));      // testing if I have enough ram for a 112 x 64 screen
  driver.initled((uint8_t*)leds, pins, NUM_PANELS_PER_ROW * NUM_PANELS_PER_COLUMN, PANEL_WIDTH * PANEL_HEIGHT * 2, ORDER_GRB);    // simulating screen twice as big and half as fast
  #else
  leds = (CRGB*)calloc(numLeds, sizeof(CRGB));
  driver.initled((uint8_t*)leds, pins, NUM_PANELS_PER_ROW * NUM_PANELS_PER_COLUMN, PANEL_WIDTH * PANEL_HEIGHT, ORDER_GRB);
  #endif
  // These two lines have to be after driver.initled()
  driver._offsetDisplay.panel_width=SCREEN_WIDTH;
  driver._offsetDisplay.panel_height=SCREEN_HEIGHT;
  
  offd = driver.getDefaultOffset();
  offd.panel_width=SCREEN_WIDTH;
  offd.panel_height=SCREEN_HEIGHT;

  Serial.printf("driver._offsetDisplay.panel_width = %d\n", driver._offsetDisplay.panel_width);
  Serial.printf("driver._offsetDisplay.panel_height = %d\n", driver._offsetDisplay.panel_height);
  
  groundOffset.x = 0;
  groundOffset.y = 0;
  skyOffset.x = 0;
  skyOffset.y = 0;
  
  // driver.setMapLed(mapLedsOnPanel);
  
  startAudio();
  Serial.print("ESP.getFreeHeap() = ");
  Serial.println(ESP.getFreeHeap());
  Serial.print("heap_caps_get_largest_free_block(MALLOC_CAP_8BIT) = ");
  Serial.println(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT));
  Serial.print("heap_caps_get_largest_free_block(MALLOC_CAP_32BIT) = ");
  Serial.println(heap_caps_get_largest_free_block(MALLOC_CAP_32BIT));
  delay(100);
  
  pinMode(ATX_POWER_ON, OUTPUT);
  digitalWrite(ATX_POWER_ON, LOW);
  // gpio_set_direction((gpio_num_t)13, GPIO_MODE_OUTPUT);
  // gpio_set_level((gpio_num_t)13, 0);
  
  Serial.printf("ledMappingFunction(0) = %d\n\r", ledMappingFunction(0));
  Serial.printf("ledMappingFunction(1790) = %d\n\r", ledMappingFunction(1790));

BRIGHTNESSMARK = map(analogRead(BRIGHTNESSPOT), 0, 4093, 1,BRIGHTNESSMAX);  // read brightness potmeter
  driver.setBrightness(BRIGHTNESSMARK);
pinMode(InterruptPin, INPUT);
 attachInterrupt(InterruptPin, isr, RISING);
  
}

void loopBluetooth()
{
  
  static float* inputReal = (float*)calloc(SAMPLES, sizeof(float));
  static float* output = (float*)calloc(SAMPLES, sizeof(float));
  static fft_config_t* fftReal = fft_init(SAMPLES, FFT_REAL, FFT_FORWARD, inputReal, output);
  static float* bands = (float*)calloc(BANDS, sizeof(float));
  #ifdef PRINT_PEAKS
  static float* peakBands = (float*)calloc(BANDS, sizeof(float));
  #else
  static float* peakBands;
  #endif
  
  bool charactersOn = true;
  static unsigned int beebBoob = 178;
  static uint32_t previousMicros = 0;
  static uint32_t previousCycles = 0;
  uint32_t newMicros = micros();
  uint32_t newCycles = xthal_get_ccount();
  static uint32_t previousDebugMillis = 0;
  loopMicros = newMicros - previousMicros;
  loopCycles = newCycles - previousCycles;
  previousMicros = newMicros;
  previousCycles = newCycles;

  if (previousDebugMillis == 0)
  {
    previousDebugMillis = 1;
    Serial.print("ESP.getFreeHeap() = ");
    Serial.println(ESP.getFreeHeap());
    Serial.print("heap_caps_get_largest_free_block(MALLOC_CAP_8BIT) = ");
    Serial.println(heap_caps_get_largest_free_block(MALLOC_CAP_8BIT));
    Serial.print("heap_caps_get_largest_free_block(MALLOC_CAP_32BIT) = ");
    Serial.println(heap_caps_get_largest_free_block(MALLOC_CAP_32BIT));
  }
  
  while (true)
  {
    if (readBuffer(inputReal)) break;
    else
    {
      vTaskDelay(1);
      previousMicros = micros();
      previousCycles = xthal_get_ccount();
    }
  }


  #ifdef PRINT_FFT_TIME
  uint32_t fftMicros = micros();
  uint32_t fftCycles = xthal_get_ccount();
  #endif
  
  // substractAverage(inputReal);
  doWindowing(inputReal);
  fft_execute(fftReal);
  powerOfTwo(output);   // if we end up doing log() of the output anyways there is no need to do costly sqrt() because it's the same as dividing log() by 2
  // sqrtBins();     // we don't actually need this since we are dealing with the power of the signal and not the amplitude
  zeroSmallBins(output);  // we do this because there are plenty of of reflections in the surrounding bins

  /* These will be combined to one function
  powTwoBands(output, bands);
  logBands(bands, peakBands);
  normalizeBands(bands);
  */
  powerBinsToBands(output, bands);
  
  groundOffset.x += 0.03;
  groundOffset.y += 0.5;
  skyOffset.x += 0.23;
  skyOffset.y += 0.05;
  if (groundOffset.x < 0) groundOffset.x += TILE_WIDTH;
  if (groundOffset.y < 0) groundOffset.y += TILE_HEIGHT;
  if (skyOffset.x < 0) skyOffset.x += TILE_WIDTH;
  if (skyOffset.y < 0) skyOffset.y += TILE_HEIGHT;
  
  #ifdef PRINT_FFT_TIME
  fftMicros = micros() - fftMicros;
  fftCycles = xthal_get_ccount() - fftCycles;
  #endif
  
  #ifndef WAIT_UNTIL_DRAWING_DONE
  while (driver.isDisplaying == true) vTaskDelay(1);
  #endif
  
  #ifdef PRINT_RAM
  // I had to move these commands here because they bothered I2SCloclessLedDriver
  // They do something that blocks the interruots or something
  // I was getting pretty garbled results just because of this
  // I had to wait until I2SCloclessLedDriver was done drawing the leds
  static int heap_caps_get_largest_free_block_8bit_min = 1000000000;
  static int heap_caps_get_largest_free_block_32bit_min = 1000000000;
  int newValue = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT);
  if (newValue < heap_caps_get_largest_free_block_8bit_min) heap_caps_get_largest_free_block_8bit_min = newValue;
  newValue = heap_caps_get_largest_free_block(MALLOC_CAP_32BIT);
  if (newValue < heap_caps_get_largest_free_block_32bit_min) heap_caps_get_largest_free_block_32bit_min = newValue;
  #endif
  
  uint32_t mapLedsMicros = micros();
  uint32_t mapLedsCycles = xthal_get_ccount();
    BRIGHTNESSMARK = map(analogRead(BRIGHTNESSPOT), 0, 4093, 1,BRIGHTNESSMAX);  // read brightness potmeter
  driver.setBrightness(BRIGHTNESSMARK);
  // mapLeds();
  // mapLeds_mario();
  // driver.ledToDisplay will be needed next

if (Request){
   isrCounter++;
   if(isrCounter>20){
    if (mapMode==RAINBOW)mapMode=TEXTURES;
    else mapMode=RAINBOW;
    Request = false;
    Serial.println(mapMode);
    isrCounter=0;
   }
 }



 
  switch (mapMode)
  {
    case TEXTURES:
    for (int x = 0; x < BANDS; x++)
    {
      mapLeds_textures(x, groundOffset, skyOffset, bands);
    }
    break;
    
    case RAINBOW:
    mapLeds_rainbow(bands);
    break;
  }
  
  if (charactersOn == true)
  {
    // drawCharacter(CRGB* character, float startingColumn, float offset, int columnsPerFrame, int numberOfFrames, int frameWidth, int frameHeight)
    static uint32_t characterNumber = 0;
    static int frameWidth = 32;
    static int frameHeight = 32;
    static int numberOfFrames = 8;
    static const CRGB* currentCharacter = (const CRGB*)tuxes_walking;
    static float columnsPerFrame = 1.5;
    static float offset = 100.4 + ((double)esp_random() / (double)4194304);
    static float startingColumn = 0 - frameWidth;
    // [frameHeight * 2 * (characterNumber % 11) * 3 * frameWidth * numberOfFrames]
    drawCharacter(currentCharacter, startingColumn, offset, columnsPerFrame, numberOfFrames, frameHeight, frameWidth);
    startingColumn += 0.2;
    if (startingColumn > SCREEN_WIDTH + frameWidth + 4)
    {
      characterNumber = (characterNumber + 1) % 24;
      {
        currentCharacter = (const CRGB*)tuxes_walking + characterNumber * frameHeight * frameWidth * numberOfFrames;
        frameWidth = 32;
        frameHeight = 32;
        numberOfFrames = 8;
      }
      // offset = 100.4 + (float)esp_random() / 1000000.4;
      offset = 100.4 + ((double)esp_random() / (double)4194304);
      if ((characterNumber % 2) == 0)
      {
        columnsPerFrame = 1.5;
      }
      else
      {
        columnsPerFrame = 2;
      }
      if (characterNumber == 22)
      {
        currentCharacter = (const CRGB*)guido_walking;
        frameWidth = 32;
        frameHeight = 32;
        numberOfFrames = 8;
        columnsPerFrame = 2; 
      }
      if (characterNumber == 23)
      {
        currentCharacter = (const CRGB*)squirrel_walking;
        frameWidth = 20;
        frameHeight = 20;
        numberOfFrames = 6;
        columnsPerFrame = 2;
      }
      startingColumn = 0 - frameWidth - 4;
      // Serial.printf("Color of the first pixel is %u\n\r", (*(uint32_t*)&(currentCharacter[0])));
      // it should be 16711164;
      // it's actually 4277992958 because the cpu is little endian
      // that means that the last byte of the number is stored in the beginning
      // all the bytes in a number are in reverse order
      // back in the day when the bit width of the memory lane was less than the bit width of the cpu
      // it allowed the cpu to start the calculations one clock cycle before it got the whole number
    }
  }
  mapLedsMicros = micros() - mapLedsMicros;
  mapLedsCycles = xthal_get_ccount() - mapLedsCycles;
  
  uint32_t showPixelsMicros = micros();
  uint32_t showPixelsCycles = xthal_get_ccount();
  
  // driver.showPixels((uint8_t *)leds);
  // driver.showPixels(offd);
  // leds[8 * SCREEN_WIDTH + 51].r = 255;
  // leds[8 * SCREEN_WIDTH + 51].g = 255;
  // leds[8 * SCREEN_WIDTH + 51].b = 255;
  driver.showPixels(NO_WAIT);

  showPixelsMicros = micros() - showPixelsMicros;
  showPixelsCycles = xthal_get_ccount() - showPixelsCycles;
  
  #ifdef PRINT_PLOT
  plot();
  #endif
  #ifdef PRINT_BANDS
  printBands();
  #endif
  
  if (millis() - previousDebugMillis > SECONDS_BETWEEN_DEBUG * 1000)
  {
    previousDebugMillis = millis();
    Serial.printf("driver._offsetDisplay.panel_width = %d\n", driver._offsetDisplay.panel_width);
    Serial.printf("driver._offsetDisplay.panel_height = %d\n", driver._offsetDisplay.panel_height);

    #ifdef PRINT_FFT_TIME
    //Serial.print("FFT took ");
    //Serial.print(fftMicros);
    //Serial.print(" ");
    //Serial.print((fftCycles) / 240);
    //Serial.println(" μs");
    #endif
    #ifdef PRINT_RAM
    Serial.print("ESP.getFreeHeap() = ");
    Serial.println(ESP.getFreeHeap());
    Serial.print("heap_caps_get_largest_free_block(MALLOC_CAP_8BIT) = ");
    Serial.println(heap_caps_get_largest_free_block_8bit_min);
    Serial.print("heap_caps_get_largest_free_block(MALLOC_CAP_32BIT) = ");
    Serial.println(heap_caps_get_largest_free_block_32bit_min);
    heap_caps_get_largest_free_block_8bit_min = 1000000000;
    heap_caps_get_largest_free_block_32bit_min = 1000000000;
    #endif
    #ifdef PRINT_FASTLED_TIME
    //Serial.print("driver.showPixels() took ");
    //Serial.print(showPixelsMicros);
    //Serial.print(" ");
    //Serial.print((showPixelsCycles) / 240);
    //Serial.println(" μs ");
    #endif
    #ifdef PRINT_MAPLEDS_TIME
    //Serial.print("MapLeds took ");
    //Serial.print(mapLedsMicros);
    //Serial.print(" ");
    //Serial.print((mapLedsCycles) / 240);
    //Serial.println(" μs ");
    #endif
    #ifdef PRINT_SAMPLE_RATE
    //Serial.print("a2dp_sink.sample_rate() = ");
    //Serial.println(a2dp_sink.sample_rate());
    #endif
    #ifdef PRINT_ALL_TIME
    //Serial.print("everything took ");
    //Serial.print(loopMicros);
    //Serial.print(" ");
    //Serial.print((loopCycles) / 240);
    //Serial.println(" μs");
    #endif
    #ifdef PRINT_PEAKS
    //Serial.print("Peak bands: ");
   // for (int i = 0; i < BANDS; i++)
   // {
   //   Serial.printf("%f, ", peakBands[i] - 6.0);
   // }
   // Serial.println();
    #endif
    #ifdef PRINT_LESSER_SAMPLES
   // if (lesserSamples > 0)
   // {
   //   Serial.print("lesserSamples = ");
  //    Serial.println(lesserSamples);
  //    lesserSamples = 0;
  //  }
    #endif
    #ifdef PRINT_BUFFER_FULL
   // if (bufferFull > 0)
   // {
    //  Serial.print("BUFFER FULL. DISCARDING DATA. BUFFER FULL. DISCARDING DATA. BUFFER FULL. bufferFull = ");
     // Serial.println(bufferFull);
     // bufferFull = 0;
   // }
    #endif
  }
  #ifdef TEST_FULL_BUFFER
  //delay(150);
  #endif
}

void loopMicrophone()
{
  
}

void loopArtnet()
{
  
}

void loop()
{
  

  switch(programMode)
  {
    case bluetooth:
      while(true) loopBluetooth();
      break;
    case microphone:
      while(true) loopMicrophone();
      break;
    case artnet:
      while(true) loopArtnet();
      break;
  }
}

【Arduino 动手做】Fast Audio  快速音频频谱分析仪
项目链接:https://www.instructables.com/Fast-Audio-Spectrum-Analyzer/
项目作者:emdee401

项目视频:https://www.youtube.com/watch?v=C-ics1zhJkE
项目代码:https://github.com/donnersm/BluetoothAnalyzer

Antti Yliniemi 编写了原始代码并允许我使用和调整它。
https://github.com/yliniemi/bluetooth-spectrum-analyzer
https://www.youtube.com/@acidangel162

Antti 使用了 Yves Bazin 编写的 Led 驱动器
https://github.com/hpwit/I2SClocklessLedDriver
https://www.youtube.com/@TekWit

将 ESP32 DOIT devkit 1 连接到 USB 端口,并使用 Chome 或 Edge 访问:
donnersm.github.io/BluetoothAnalyzer/flash.html
 

00187-.gif

评论

user-avatar
icon 他的勋章
    展开更多