レベルメータ作成のまとめはこちら
目次[非表示]
はじめに
どうもtakaです。
良き父親を目指して日々奮闘中です。ついに体重が減少に転じました。というよりやつれているだけのような・・・
さて、ESP32とWS2812Bでオーディオレベルメーター製作、ついに最終回!
主たる機能はすでにこれまで紹介しておりますので今回はそれらを組み合わせLEDの点灯パターンを決定すれば完成です!
点灯パターンをどうするか?
WS2812BはフルカラーLEDなのでフルカラーを活かしたいですそこで、次のような点灯パターンを実装しました!
・配色:FFT結果の周波数軸を3分割しRGBに割り当てる
・点灯個数:各周波数範囲で最大スペクトルを点灯個数に割り当てる
配色について
低音域:中音域:高音域=0.03:0.2:1で分割します。
具体的には
低音域(R):中音域(G):高音域(B)=0~450:450~3000:3000~15[kHz]です。
点灯個数について
FFT結果のスペクトルはdBmVに対数圧縮し、その結果をそのまま点灯個数に割り当てます。
例えば、0~450[Hz]のスペクトルの最大スペクトルが20[dBmV]であれば、赤色を20個点灯させます。これをRGB同時に重ねることでフルカラーを活かしています。
ウェーバー・フェヒナーの法則について
法則を要約すると人間の五感は全て対数を感じているというものです。聴覚でいうと音量が小さい領域の変化は敏感ですが、大音量領域は大して差を感じません。味覚も痛覚も同じで、刺激が小さいときほど変化を感じやすく、刺激が大きなときは変化を感じにくいという法則です。オーディオのボリュームでは抵抗が非線形カーブ(Aカーブ)の物が使用されますが、これがまさにこの法則から来ているものです。
スペクトルや周波数分割幅を対数圧縮している理由はこれにあります。リニアスケールのまま点灯させると聴覚と視覚の繋がりが悪く音に連動しているようには感じ取れません。
考えた点灯パターンで実際に点灯させてみる
実際に点灯させるとこんな感じになります。
聴覚の低中高音が直観的にRGBに配色できているように見えますか?
また、音量が直観的に点灯個数に割り当てられているように見えますか?
システムの構成
ハード
前回
の投稿をご覧ください。前回使用しなかったVR3は今回使用します。詳しくはプログラムをご覧いただくとして、VR3でLEDの減光速度を調整できるようにしています。
先述しましたがLEDの個数はスペクトルの強さです。ただ、実直に実際の音に連動させてしまうと目が追い付かずとてもせわしないです。そこでVR3の出番です。この機構は音楽ジャンルに合わせて使うこともできます。スローペースな曲では減光速度を遅くし滑らかに移り変わり、アップテンポな曲では減光速度を速めるような使い方ができます。
ソフト
main.c、MCP3208.cpp、MCP3208.hを全て同じ階層に保存しESP32に焼きこんでください。
・main.ino
- #include "arduinoFFT.h"
- #include "MCP3208.h"
- #include <FastLED.h>
- //--------------------------------FFT用定義--------------------------------
- #define SAMPLING_FREQUENCY 30000.0
- #define FFTSMPLES 1024
- double vReal[FFTSMPLES];
- double vImag[FFTSMPLES];
- arduinoFFT FFT = arduinoFFT(vReal, vImag, FFTSMPLES, SAMPLING_FREQUENCY);
- //------------------------------------------------------------------------
- //------------------------------MCP3208用定義------------------------------
- #define MCP3208_VCC 5.0
- #define MCP3208_REF_MILLIVOLT 5000.0
- #define SPI_CSPIN 5
- #define MCP3208_ADCMAX_VAL 4095.0
- #define MCP3208_LCH 0
- #define MCP3208_LREFCH 1
- #define MCP3208_RCH 2
- #define MCP3208_RREFCH 3
- #define MCP3208_DIMMERCH 4
- mtMCP3208 adcObj(MCP3208_VCC, SPI_CSPIN, MCP3208_REF_MILLIVOLT);
- //------------------------------------------------------------------------
- //----------------------------LED制御用------------------------------------
- #define LED_PIN 16
- #define NUM_LEDS 60
- #define BRIGHTNESS 100
- #define LED_TYPE WS2812B
- #define COLOR_ORDER GRB
- CRGB leds[NUM_LEDS];
- //-------------------------------------------------------------------------
- //------------------------------電圧サンプリング-----------------------------
- const unsigned int sampling_period_us = 1000000.0 / SAMPLING_FREQUENCY; //サンプリング間隔[us]
- void sample() {
- for (int n = 0; n < FFTSMPLES; n++) {
- unsigned long t = micros();
- vReal[n] = (double)adcObj.readAdc(MCP3208_LCH);
- vImag[n] = 0;
- while ((micros() - t) < sampling_period_us) ;
- }
- }
- //------------------------------------------------------------------------
- //--------------------------------FFT実行---------------------------------
- void calcFFT() {
- FFT.Windowing(FFT_WIN_TYP_HAMMING, FFT_FORWARD); // 窓関数
- FFT.Compute(FFT_FORWARD); // FFT処理(複素数で計算)
- FFT.ComplexToMagnitude(); // 複素数を実数に変換
- }
- //------------------------------------------------------------------------
- //----------------------------その他サポート関数-----------------------------
- //DC成分を削除する
- void dcOmit() {
- double mean = 0;
- for (int i = 0; i < FFTSMPLES; i++) {
- mean += vReal[i];
- }
- mean /= FFTSMPLES;
- for (int i = 0; i < FFTSMPLES; i++) {
- vReal[i] -= mean;
- }
- }
- //FFT結果をデシベル変換する。単位は[dBmV]。
- void fft2dBmV() {
- for (int n = 0; n < FFTSMPLES / 2; n++) {
- vReal[n] = vReal[n] * 4.0 / FFTSMPLES; //これでAD変換のスケールに戻る(0~4095)
- vReal[n] = vReal[n] * MCP3208_REF_MILLIVOLT / MCP3208_ADCMAX_VAL; //これで電圧値[mV]に戻る。
- vReal[n] = 20.0 * log10(vReal[n]);
- vReal[n] = isinf(vReal[n]) ? 0.0 : vReal[n];
- }
- }
- //FFT結果にフィルタをかける
- void filterFFTres() {
- //とりあえず低周波領域は精度が悪いためスペクトルを捨てる
- for (int n = 0; n < 4; n++) {
- vReal[n] = 0.0;
- }
- }
- //FFT結果をシリアルモニタに表示する。
- void dispFFTres() {
- double dFrq = SAMPLING_FREQUENCY / FFTSMPLES;
- for (int n = 0; n < FFTSMPLES / 2; n++) {
- double frq = (double)n * dFrq;
- double spc = vReal[n];
- Serial.printf("%.0f[Hz] : %.3f[dBmV]¥n", frq, spc);
- }
- Serial.println();
- }
- //********************************点灯パターン1********************************
- //今回はRGBをそれぞれ低音/中音/高音の3に割り当て、点灯させるLEDの個数をスペクトル強度にする。
- //FFT結果の周波数軸(=要素数)を3分割しそれぞれの領域の最大スペクトル値を取得する
- const uint8_t LOW_FRQ_INDEX = 0;
- const uint8_t MID_FRQ_INDEX = 1;
- const uint8_t HI_FRQ_INDEX = 2;
- void getMaxSpec(int *maxSpec) {
- int fftResSmaples = FFTSMPLES / 2;
- int maxBuf[3] = {0};
- for (int n = 0; n < fftResSmaples; n++) {
- //FFT結果のデシベルがそのままLEDの点灯個数になるので負のデシベル(とても小さな電圧)は0にしておく
- vReal[n] = vReal[n] < 0 ? 0.0 : vReal[n];
- //最大値もLEDの個数でカットする
- vReal[n] = vReal[n] > NUM_LEDS ? NUM_LEDS : vReal[n];
- //解析結果の周波数を三等分(RGB)して各帯域の最大値を取得する
- //横軸も対数化する。今回は簡易的に三分割の方法を0.03:0.2:1で分割してみる
- if (n < fftResSmaples * 0.03) {
- maxBuf[LOW_FRQ_INDEX] = maxBuf[LOW_FRQ_INDEX] > vReal[n] ? maxBuf[LOW_FRQ_INDEX] : (int)vReal[n];
- //Serial.printf("maxBuf:%d,vReal:%f = %d¥n",maxBuf[LOW_FRQ_INDEX] ,vReal[n],maxBuf[LOW_FRQ_INDEX] > vReal[n] ? maxBuf[LOW_FRQ_INDEX] : (int)vReal[n]);
- } else if (n < fftResSmaples * 0.2) {
- maxBuf[MID_FRQ_INDEX] = maxBuf[MID_FRQ_INDEX] > vReal[n] ? maxBuf[MID_FRQ_INDEX] : (int)vReal[n];
- } else {
- maxBuf[HI_FRQ_INDEX] = maxBuf[HI_FRQ_INDEX] > vReal[n] ? maxBuf[HI_FRQ_INDEX] : (int)vReal[n];
- }
- }
- *maxSpec=maxBuf[LOW_FRQ_INDEX];
- *(maxSpec+1)=maxBuf[MID_FRQ_INDEX];
- *(maxSpec+2)=maxBuf[HI_FRQ_INDEX];
- }
- //減光速度をボリュームで調整する処理
- int decLed = 70; //減光スピード。この関数を1回呼び出し前回より点灯個数が少なくなる場合は、すぐに消灯せず照度をこの値だけ減光する
- //ジャズのようなスローテンポなら35くらい
- //ポップなら70位か?
- void setDimmer(){
- int dimmerLevel=adcObj.readAdc(MCP3208_DIMMERCH);
- decLed = 255.0*dimmerLevel/MCP3208_ADCMAX_VAL;
- }
- //getMaxSpecでLED点灯個数が決定したので、照度と色の情報に変換していく
- void genColorArray(int *maxSpec) {
- static int ledColor[3][NUM_LEDS]={0};
- for (int rgbCnt = 0; rgbCnt < 3; rgbCnt++) {
- for (int n = 0; n < NUM_LEDS; n++) {
- if (n <= *(maxSpec+rgbCnt)) { //nがスペクトル(個数相当)未満なら照度MAX(255)で点灯
- ledColor[rgbCnt][n] = 255;
- } else if (ledColor[rgbCnt][n] - decLed > 0) {
- ledColor[rgbCnt][n] -= decLed;
- } else {
- ledColor[rgbCnt][n] = 0;
- }
- }
- }
- const uint8_t lowFrqIndex=0 , midFrqIndex=1 , hiFrqIndex=2;
- //RGB毎に点灯個数を決定したのでRGB情報に変換する
- for (int n = 0; n < NUM_LEDS; n++) {
- leds[n] = CRGB( ledColor[lowFrqIndex][n], ledColor[midFrqIndex][n], ledColor[hiFrqIndex][n]);
- }
- }
- //------------------------------------------------------------------------
- void setup() {
- Serial.begin(115200);
- delay(3000);
- //AD変換の初期化
- adcObj.adcInit();
- //FastLEDの初期化
- FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS).setCorrection( TypicalLEDStrip );
- FastLED.setBrightness(BRIGHTNESS);
- FastLED.clear();
- }
- void loop() {
- sample(); //SAMPLING_FREQUENCYでFFTSMPLES回サンプリング実施
- dcOmit(); //直流成分の除去
- calcFFT(); //FFT実施
- filterFFTres(); //低周波側の端っこ数要素は精度が怪しいので捨てる。
- fft2dBmV(); //FFT結果をdBmVへ変換する
- //dispFFTres(); //デバッグ用
- //------LED制御------
- int maxSpec[3] = {0};
- setDimmer(); //減光速度を可変抵抗から読み取る
- //Serial.println(decLed);
- getMaxSpec(maxSpec); //FFT結果(周波数軸)を3分割し各領域の最大スペクトル値を取得する
- //Serial.printf("low:%d,mid:%d,hi:%d¥n",maxSpec[0],maxSpec[1],maxSpec[2]);
- genColorArray(maxSpec); //最大スペクトルに応じたLED点灯用配列に格納する。
- FastLED.show();
- //デバッグ用
- //Serial.println(adcObj.readAdc(MCP3208_LCH));
- //delay(1000);
- }
・MCP3208.cpp
- #include <Arduino.h>
- #include <SPI.h>
- #include "MCP3208.h"
- mtMCP3208::mtMCP3208(uint8_t vcc,uint8_t csPinNo,int refMilliVolt){
- this->vcc=vcc;
- this->csPinNo=csPinNo;
- this->refMilliVolt=refMilliVolt;
- }
- void mtMCP3208::adcInit() const{
- //SPI設定
- SPI.begin(); //SPIを行う為の初期化
- SPI.setBitOrder(MSBFIRST); //bitオーダー
- SPI.setDataMode(SPI_MODE0);
- SPI.setFrequency(vcc == 5 ? 2000000 : 1000000); //MCP3208が5Vの時は最速の2MHzで通信できる
- pinMode(csPinNo, OUTPUT) ;
- digitalWrite(csPinNo, HIGH) ;
- }
- int mtMCP3208::readAdc(uint8_t channel)const{
- int d1 , d2 ;
- // ADCから指定チャンネルのデータを読み出す
- digitalWrite(csPinNo, LOW) ;
- SPI.transfer(0x06 | (channel >> 2) ) ;
- d1 = SPI.transfer( (channel & 0x03) << 6 ) ;
- d2 = SPI.transfer(0x00) ;
- digitalWrite(csPinNo, HIGH) ;
- return ((d1 & 0x0F) << 8 | d2) ;
- }
- int mtMCP3208::readMilliVolt(uint8_t channel)const{
- int adcRes=readAdc(channel);
- return refMilliVolt*adcRes/4095.0;
- }
・MCP3208.h
- class mtMCP3208{
- private:
- uint8_t csPinNo;
- uint8_t vcc;
- int adcRes;
- int refMilliVolt;
- int offsetMillVolt;
- public:
- mtMCP3208(uint8_t,uint8_t,int);
- void adcInit()const;
- int readAdc(uint8_t)const;
- int readMilliVolt(uint8_t)const;
- };
うまく動作しましたか?
ソースコードの詳細はコード中のコメントを参照ください。
若干カット&トライで作成したこともありスパゲティー気味です。ご了承を・・・
0 件のコメント:
コメントを投稿