ラズピコを使ってステレオのスペクトラムアナライザを作る
◆まえがき
Raspberry Pi Pico (以下ラズピコ)を使って0.96インチOLED(SSD1306インターフェイス)に表示する、オーディオスペクトラムアナライザーを作ってみました。
◆概要
FFTをやってみたくなってArduino UNOを使ってキャラクタ液晶に表示するタイプの物を作ったりしていたのですが、もう少し本格的な物を作りたくなりました。最初はUNOでやっていたのですが、メモリーの制限が厳しいので断念しラズピコで作ることにしました。
◆回路図
ACカップルで入力し電位が1/2Vccになるように抵抗でバイアスしてADCに入力しています。普通ならR1-R4には1桁高い値を使うのですが、RP2040のADCの入力抵抗が80kΩ min.と言う仕様になっているので低めの抵抗値を使いました。
0.96インチのOLEDはI2Cインターフェイスで接続しています。
◆外観
◆プログラム
0.96インチのOLEDの表示のライブラリはu8g2.hを使用。FFTのライブラリにはArduinoFFT.hを使いました。
プログラムはちょっと長いですが全体が見える形で公開します。
/* オーディオ信号を128x64のOLEDにスペクトル表示 20220603_PiPicoFftAnalyzer.ino ボードはRP picoを使用。ライブラリは ArduinoFFT.h 2022/06/03 ラジオペンチ http://radiopench.blog96.fc2.com/ */ #include <Arduino.h> #include <U8g2lib.h> #include <Wire.h> #include "arduinoFFT.h" #define R_IN 26 // R-入力ピン #define L_IN 27 // L-入力ピン #define PX1 62 // 左画面(L)原点 #define PX2 65 // 右画面(R)原点 #define PY1 16 // 波形画面の下端 #define PY2 55 // スペクトル画面の下端(-50db) arduinoFFT FFT = arduinoFFT(); // Create FFT object U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); const uint16_t samples = 128; // サンプル数 double vReal_R[samples]; // FFTの計算領域(実際にはたぶん32ビット浮動小数点?) double vImag_R[samples]; double vReal_L[samples]; double vImag_L[samples]; int16_t wave_R[samples]; // 波形の生データー int16_t wave_L[samples]; void setup() { pinMode(16, OUTPUT); // 実行時間測定用 pinMode(25, OUTPUT); // pico内蔵LED // Wire.setClock(400000); // これは効果無し Serial.begin(115200); analogReadResolution(12); // ADCのフルスケールを12ビットに設定 u8g2.begin(); u8g2.setFont(u8g2_font_6x10_tf); u8g2.setDrawColor(1); u8g2.setFontPosTop(); // 左上を文字位置とする u8g2.clearBuffer(); u8g2.drawStr( 0, 0, "Start FFT v0.4"); u8g2.sendBuffer(); delay(1000); } void loop() { // 波形の読み取り(5ms) // digitalWrite(16, HIGH); digitalWrite(25, HIGH); // サンプリング中は内蔵LED点灯 for (int i = 0; i < samples; i++) { wave_R[i] = analogRead(R_IN); // 波形データー取得(fs:4096) wave_L[i] = analogRead(L_IN); delayMicroseconds(21); // サンプリング周期調整(1サイクル39us) } // (5ms) digitalWrite(25, LOW); // digitalWrite(16, LOW); // FFT計算データ準備(1.7ms) for (int i = 0; i < samples; i++) { vReal_R[i] = (wave_R[i] - 2048) * 3.3 / 4096.0; // 電圧に換算 vReal_L[i] = (wave_L[i] - 2048) * 3.3 / 4096.0; vImag_R[i] = 0; vImag_L[i] = 0; } // FFTの計算 (33ms) FFT.Windowing(vReal_R, samples, FFT_WIN_TYP_HAMMING, FFT_FORWARD); // 窓関数(ハミング)適用 FFT.Windowing(vReal_L, samples, FFT_WIN_TYP_HAMMING, FFT_FORWARD); FFT.Compute(vReal_R, vImag_R, samples, FFT_FORWARD); // FFT FFT.Compute(vReal_L, vImag_L, samples, FFT_FORWARD); FFT.ComplexToMagnitude(vReal_R, vImag_R, samples); // 絶対値の算出 FFT.ComplexToMagnitude(vReal_L, vImag_L, samples); u8g2.clearBuffer(); // 画面バッファクリア(22us) showWaveform(); // 波形表示(6ms) showSpectrum(); // スペクトラム表示(9.4ms) showOthers(); // 目盛線他の表示(1.6ms) u8g2.sendBuffer(); // (30ms) delay(1); // 書き込み不良対策のおまじない } //(ループ実行時間:82ms) void showWaveform() { // 入力波形表示 for (int i = 0; i < 52; i++) { u8g2.drawLine(PX2 + i, PY1 - (wave_R[i * 2]) / 256, PX2 + i + 1, 16 - (wave_R[i * 2 + 1] / 256)); // R-ch波形プロット u8g2.drawLine(PX1 - i, PY1 - (wave_L[i * 2]) / 256, PX1 - i - 1, 16 - (wave_L[i * 2 + 1] / 256)); // L-ch波形プロット } } void showSpectrum() { // スペクトラム表示 int d; static int peak_R[64]; // 過去ピーク値 static int peak_L[64]; for (int xi = 1; xi < 60; xi++) { // スペクトラム表示(0は飛ばす) d = barLength(vReal_R[xi]); u8g2.drawVLine(xi + PX2, PY2 - d, d); // 右側(R-ch)スペクトラム u8g2.drawVLine(xi + PX2, PY2 - peak_R[xi], 1); // 右側ピーク if (peak_R[xi] < d) { // 最新値がピーク値以上だったら peak_R[xi] = d; // ピーク値の更新 } if (peak_R[xi] > 0) { peak_R[xi] --; // ピーク値をディケイ } d = barLength(vReal_L[xi]); // L側スペクトラム表示 u8g2.drawVLine(PX1 - xi, PY2 - d, d); // 左側(L-ch)スペクトラム u8g2.drawVLine(PX1 - xi, PY2 - peak_L[xi], 1); // 右側ピーク if (peak_L[xi] < d) { // 最新値がピーク値以上だったら peak_L[xi] = d; // ピーク値更新 } if (peak_L[xi] > 0) { peak_L[xi] --; // ピーク値をディケイ } } } int barLength(double d) { // スペクトルグラフの長さを計算 float fy; int y; fy = 14.0 * (log10(d) + 1.5); // 10倍(20dB)で14画素 y = fy; y = constrain(y, 0, 56); return y; } void showOthers() { // グラフの修飾(目盛他の作画) // 領域区分線 u8g2.drawVLine(PX1, 0, 64); // L画面原点線 u8g2.drawVLine(PX2, 0, 64); // R画面原点線 u8g2.drawHLine(0, PY2, 128); // スペクトル下端線 // 周波数目盛(横軸) for (int xp = PX1; xp > 0; xp -= 5) { // L側 1kHz間隔目盛 u8g2.drawVLine(xp, PY2 + 1, 1); } u8g2.drawVLine(PX1 - 25, PY2 + 1, 2); // L 5k目盛 u8g2.drawVLine(PX1 - 50, PY2 + 1, 2); // L 10k目盛 for (int xp = PX2; xp < 127; xp += 5) { // R側 1kHz間隔目盛 u8g2.drawVLine(xp, PY2 + 1, 1); } u8g2.drawVLine(PX2 + 25, PY2 + 1, 2); // R 5k目盛 u8g2.drawVLine(PX2 + 50, PY2 + 1, 2); // R 10k目盛 u8g2.setFont(u8g2_font_micro_tr); // 小さなフォント(3x5)で、 u8g2.setCursor( 7, 58); u8g2.print("10k"); // L側周波数表示 u8g2.setCursor( 34, 58); u8g2.print("5k"); u8g2.setCursor( 58, 58); u8g2.print("0"); u8g2.setCursor( 67, 58); u8g2.print("0"); // R側周波数表示 u8g2.setCursor( 87, 58); u8g2.print("5k"); u8g2.setCursor(110, 58); u8g2.print("10k"); // スペクトルレベル目盛(縦軸) for (int y = PY2 - 7; y > 16; y -= 14) { // dB目盛線(横の点線) for (int x = 13; x < 128; x += 5) { u8g2.drawHLine(x, y, 2); } } u8g2.setCursor(0, 17); u8g2.print("0dB"); // スペクトル感度 u8g2.setCursor(0, 30); u8g2.print("-20"); // u8g2.setCursor(0, 45); u8g2.print("-40"); // // LR表示 // u8g2.setFont(u8g2_font_6x10_tf); // 少し大きなフォントで // u8g2.setCursor(0, 4); u8g2.print("L"); // チャンネル表示 // u8g2.setCursor(121, 4); u8g2.print("R"); // u8g2.setFont(u8g2_font_crox1cb_tf); // 少し豪華なフォントで u8g2.setCursor(0, 3); u8g2.print("L"); // チャンネル表示 u8g2.setCursor(119, 3); u8g2.print("R"); // }FFTの条件としてはサンプル数128、サンプリング周期39μsのステレオ入力になっていて、この条件で上限周波数12.8kHz、バンド幅200Hz/64チャンネルのFFTが可能になります。
サンプリングから表示までの1サイクルを82msで回るようになっています。(個別の処理時間はコメント内に記載)
◆画面
画面の左右にステレオのL/Rチャンネルの波形とスペクトラムを表示します。
この画面はファンクションジェネレーターから5kHzの正弦波を入力した状態です。正確な位置にスペクトルが表示されていることが判ります。
ちょっと気になるのはメインの5kHzのピークの周りにスプリアスのようなものがまとわりついていることです。ひょっとしたら、RP2040のADCのINLの悪さ(エラッタ E11)が原因なのかも判りません。
あと、こうやって単一周波数の信号を入力すると、エイリアシング現象で12.8kHzより上の信号が折り返されて表示されるのが良く判ります。用途によっては簡単なローパスフィルタを入れておいた方が良さそうです。
◆動画
◆まとめ
簡単な物ですが動かすと面白いです。こうやって実際にライブラリのレベルから動かしてみると、FFTに対する理解が深まります。
今回はステレオで作りましたが、1チャンネルならもっと高性能なスペアナを作ることも可能です。時間がある時にやって見たいと思います。
私はラズピコを使った経験があまり無く、Arduino で使う場合の言語リファレンスの所在も判らないので先人の方のコードを真似しながら書きました。そんなことで変なコードになっているところがあるかも知れませんが、お気付きの点などあればコメントなどで教えて頂くと幸いです。
- 関連記事
-
- FFT処理の入出力の値の関係を調べてみた
- ラズピコとArduinoでFFTアナライザーを作ってみた
- Arduinoとラズピコで作るオーディオ用スペクトラムアナライザが完成
- スペクトラムアナライザの上限周波数を20kHzに拡大、ESP32にも対応
- ラズピコを使ってステレオのスペクトラムアナライザを作る