オーディオ用スペアナ製作

スイッチサイエンスで販売されているチップを眺めていたら、スペクトラムアナライザMSGEQ7LEDドライバが並んでいて、「あー、スペアナ作れるよってことね」と思い、オーディオ用のスペクトラムアナライザを製作。


ベース基板。MSGEQ7を2つ積んで、ステレオ2ch × 7バンド(特性は下記)のレベルを取得します。
左から16ch LEDドライバ、Arduino Pro、MSGEQ7×2と周辺パーツが並びます。裏面にステレオミニジャックとACアダプタジャックがついてます。


ベース基板の上に乗る表示基板。表示用には秋月のバーLED(緑×5、黄×3、赤×2のタイプ)をレールで購入しました。左右対称に、真ん中から順に、左右chの63Hz、 160Hz、400Hz…用のレベルメータです。
あとはひたすらLED140個分をハンダ付け…(o´Д`)=з。バーLEDは基板に刺す前に、アノード側の足を全部根元で折り曲げてハンダ付けし、アノードを1つにまとめてしまったりとか、割と無茶して並べてあります。


MSGEQ7は、ストローブ信号を送ると順番にバンドが切り替わって、対応する電圧が出力されるので、それをAruduinoのADコンバータで読み取り、10レベル(1目盛りで約3dBのlogスケール)に変換します。

変換した値に基づいて、各バンドに対応したLED10個を順にダイナミック点灯していきます。購入したLEDドライバは16chを同時に駆動できるため、余った6ch分もダイナミック点灯用のトランジスタをドライブするのに流用しています(こんな使い方していいのかはよくわからないけど)。ダイナミック点灯に必要なpinは、左右ch選択 2 + バンド選択 7の9pin。 残りの3つはArduinoのデジタルポートから直接ドライブします。


他に、ピーク検出&表示(一定時間max値を維持した後ドロップするアニメーション)を入れてみたり、起動アニメーションで文字らしきものを表示してみたりして遊んでいます。


スモークグレーのアクリルケースに入れると結構きれい。


参考までに回路図(適当なので間違いがいっぱいあるかも(汗))。表示基板部分は同じ構造の繰り返しのため大幅に省略してあります。

[リンク先の「オリジナルサイズを表示」で拡大]


スケッチは本エントリの最後に。これとは別にLEDドライバ用のライブラリ Tlc5940 が必要です(リンク先の説明は分かりやすく参考になる)。
ただし、デフォルトの状態では14chでダイナミック点灯するには動作が遅すぎて、表示が激しくちらついてしまうので、ライブラリの設定を変更して動作を高速にする必要があります。
ライブラリの tcl_config.h というファイルを開くと TLC_PWM_PERIOD という定数が 8192 になっていますので、これを下記のように小さくします。

#define TLC_PWM_PERIOD    64

作ってくれた方が。

ケース苦労しますよね。

http://ehbtj.com/electronics/171/

スケッチ

#include "Tlc5940.h"

const byte leftOut = 8; // LEDダイナミック点灯; 左チャンネル選択の出力ピン
const byte rightOut = 7; // LEDダイナミック点灯; 右チャンネル選択の出力ピン

const byte ch16 = 6; // ダイナミック点灯のバンド選択のうち、LEDドライバに入らなかった分の出力ピン
const byte matrix[] = {15,0,2,14,1,3,16,
                       15,0,2,3,14,16,1}; // 各バンド(63Hz,150Hz,...)を選択するためのLEDドライバの出力チャンネル。16はArduinoのポート
const int level_v[] = {203,256,322,406,512,645,813,1024,1290,1625}; // 電圧値から10レベル変換用のテーブル (logスケール)

const int spectrumReset = 5;  // MSGEQ7へのリセット信号の出力ピン
const int spectrumStrobe = 4;  // MSGEQ7へのストローブ信号の出力ピン 
const int spectrumOutL = 1;  // MSGEQ7からの出力電圧の入力ピン(アナログポート); 左ch
const int spectrumOutR = 0;  // MSGEQ7からの出力電圧の入力ピン(アナログポート); 右ch

const byte max_keep = 48; // ピーク表示の維持時間(サイクル数)
const byte max_dwn_speed = 4; // ピーク表示のドロップアニメーションの速度


int spectrum[7*2];
int skip_cnt = 0;   // counter for slowing down update cycle
byte max[7*2] = {0};     // current max indicator level
byte max_cnt[7*2] = {0}; // duration to keep max indicator
byte max_dwn[7*2] = {0}; // 1 while drop-down animation is going on

byte band = 13; // 0-6:left 7-13:right

void setup() {
  Serial.begin(115200);

  Tlc.init();

  pinMode(leftOut, OUTPUT);
  pinMode(rightOut, OUTPUT);
  pinMode(ch16, OUTPUT);

  pinMode(spectrumReset, OUTPUT);
  pinMode(spectrumStrobe, OUTPUT);

  //Init spectrum analyzer
  digitalWrite(spectrumStrobe,LOW);
  delay(1);
  digitalWrite(spectrumReset,HIGH);
  delay(1);
  digitalWrite(spectrumStrobe,HIGH);
  delay(1);
  digitalWrite(spectrumStrobe,LOW);
  delay(1);
  digitalWrite(spectrumReset,LOW);
  delay(5);
}

void display(byte band, unsigned bitmap) {
  Tlc.clear();
  for (int i = 0; i < 10; i++) {
    Tlc.set(i+4, bitmap & 1 ? 4095 : 0);
    bitmap >>= 1;
  }

  if (matrix[band] != 16) {
    Tlc.set(matrix[band], 4095);
  }
  
  digitalWrite(ch16, HIGH);
  digitalWrite(rightOut, HIGH);
  digitalWrite(leftOut, HIGH);
  
  Tlc.update();

  if (band < 7) {
    digitalWrite(leftOut, LOW);
  } else {
    digitalWrite(rightOut, LOW);
  }
  if (matrix[band] == 16)
    digitalWrite(ch16, LOW);
}

static inline unsigned level_bitmap(byte level) {
  return (1<<level)-1;
}

void animation() {
  byte level;
  unsigned bitmap;
  if (millis() < 40*14*2) {
    byte peak = millis()/40 % 14;
    if (millis() >= 40*14)
      peak = 14 - peak;
    level = band == peak ? 10 : band-1 == peak || band+1 == peak ? 6 :
            band-2 == peak || band+2 == peak ? 2 :  1;
    bitmap = level_bitmap(level);
  } else if (millis() < 7500) {
    const unsigned bitmaps[] = {0x3f3, 0x33f, 0x3ff, 0x3f0, 0x3ff, 0x333, 0xeff, 0x303, 0x300, 0x3ff, 0x300, 0x3ff, 0x003, 0x3ff, 0x333, 0, 0x3ff, 0x330, 0x3ff, 0x3ff, 0x0c0, 0x3ff, 0x3ff, 0x330, 0x3ff, 0x3ff, 0x003, 0x3c3, 0x3ff, 0x31f, 0x3e3, 0x3ff, 0x333, 0x3ff, 0x33c, 0x1e3};
    int start = millis()/120 - 24;
    bitmap = band+start < 0 || band+start >= sizeof(bitmaps)/2 ? 0 : bitmaps[band+start];
  } else {
    bitmap = 1;
  }
  display(band, bitmap);
}


byte update_level(int i)
{
    /* // lenear scale
    byte level = max(spectrum[i]-160, 0) / 160;
    if (level > 10)
      level = 10;
    */
    // log scale
    byte level;
    for (level = 0; level < 10; level++) {
      if (spectrum[i]-80 < level_v[level]) break;
    }
    
    if (max[i] <= level) {
      max[i] = level;
      max_cnt[i] = 0;
      max_dwn[i] = 0;
    } else if (max[i] && ++max_cnt[i] >= max_keep) {
      if (max[i] >= 5)
        max_dwn[i] = 1;
      max[i]--;
      max_cnt[i] = max_keep - max_dwn_speed;
    }
    return level;
}


void loop() {
  static byte first = 1;
  if (++band == 14) band = 0;
  if (millis() < 7500) {
    animation();
    return;
  }
  if (first == 1) {
    first = 0;
    band = 0; // start from 0
  }

  if (skip_cnt == 0) {
    spectrum[band%7] = analogRead(spectrumOutL);
    spectrum[band%7+7] = analogRead(spectrumOutR);
    spectrum[band%7] += analogRead(spectrumOutL); // => 0-2046
    spectrum[band%7+7] += analogRead(spectrumOutR); // => 0-2046
    digitalWrite(spectrumStrobe,HIGH);
    delayMicroseconds(20);
    digitalWrite(spectrumStrobe,LOW);

    Serial.print(spectrum[band%7]);
    Serial.print(" ");
    Serial.print(spectrum[band%7+7]);
    Serial.print(band%7 == 6 ? '\n' : '\t');
  } else {
    delayMicroseconds(600);
  }
  
  int levelL = update_level(band%7);
  int levelR = update_level(band%7+7);
  
  unsigned bitmap = level_bitmap(band < 7 ? levelL : levelR);
  if (max[band] >= 5 || (max[band] && max_dwn[band]))
    bitmap |= 1 << max[band]-1;
  
  display(band < 7 ? 6-band : band, bitmap);
}