見出し画像

オーディオ信号処理のプロトタイピングを M5Stack Core2 で~スマホの中の好きな曲を、オリジナル信号処理してイヤホンで聴こう!

オーディオ信号処理プロトタイピング

なかなか手頃なのが見つからなかったのですが、M5Stack Core2 なら手軽にできそうだったので試してみました。

スマホの中の好きな曲やYouTube等 -- (ブルートゥース) --> M5Stack Core2 -- (I2S) --> 有線イヤホン、という流れです。信号処理は M5Stack Core2 に入れます。

スマホとの接続は一般的なBTイヤホンで使われるA2DPプロファイル(コーデックは多分SBC?)で、イヤホンアンプへは 44.1kHz×16ビットステレオで送られます。

まだよく分からないところが多いのですが、とりあえず備忘録的に書いておきます。
何か分かったら随時更新しようかと。

環境構築

色々なところに情報は載っているので簡単に。
1.Arduino Software (IDE) インストール

2.COMポートの確認
 M5Stack Core2をPCに接続し、「スタートボタン」の上で右クリック→「デバイスマネージャー」を開いて、ポート名を確認
 ポート(COMとLPT)
  Silicon Lab ~ の「COM*」

3.Arduinoライブラリのインストール
(1)ボードマネージャーでURL追加
 https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json
 ツール>ボードマネージャで「M5stack」と打ち、M5stackのマネージャをインストール
 M5stack by M5Stack

(2)M5Stack Core2のライブラリをインストール
「スケッチ」→「ライブラリをインクルード」→「ライブラリを管理…」
「M5core2」と検索
 M5Core2 by M5Stack

4.ボード設定
(1)ボード設定
「ツール」→「ボード」→「M5Stack Arduino」→「M5Stack-Core2」の順番で選択

(2)COMポートの設定
 ツール → 「シリアルポート」を選択
 最初に確認した「COM*」のシリアルポートを選択

5.Hello Worldを実行する
 「スケッチ」→「マイコンボードに書き込む」
 または、➡ アイコンクリック

ハードウェア

電源
バッテリーは裏蓋部分に内蔵されています。
そのまま拡張できると思っていたのですが、裏蓋は外さないとダメでした。
電池モジュールを買えば良いのでしょうが、今回はプロトモジュールを余分に買っていたので、バッテリーを切り離してこれに入れてみました。
コネクタ部分のBATとGNDに繋ぐだけです。

画像1


イヤホンアンプ
プロトモジュールに、適当なI2Sヘッドホンアンプボードを繋ぎます。
今回は UDA1334 を使いました。PCM5102 とかでも良いですね。
国内では千円前後するので、2週間~1ヶ月とか待てるなら海外サイトで買った方が良いでしょう。

配線は、以下のように5本繋げばOKです。(裏で配線してます)
UDA1334  VIN GND WSEL DIN BCLK
M-BUS    5V     GND  G25   G22  G26

画像2

後述する setup() 内で設定すれば、ポートマッピングは変更も可能です。デフォルトで上記の設定になっています。

i2s_pin_config_t tx_pin_config;
tx_pin_config.bck_io_num = 26;
tx_pin_config.ws_io_num = 25;
tx_pin_config.data_out_num = 22;
tx_pin_config.data_in_num = I2S_PIN_NO_CHANGE;
a2dp_sink.set_pin_config(tx_pin_config);

しかし、買ったプロトモジュールは、内蔵基板が単なるユニバーサルでした!Webで見たヤツはコネクタからパターンが引き出されていたのですが、売っているところが見当たりませんでした。これはかなり使いにくいです。(゚~゚)

それと、高さがあまりないのでイヤホンアンプ基板が収まりません。底蓋とかないのかな?


ソフトウェア

ブルートゥースライブラリ A2DP を使わせて頂きました。BTでの送受信がとても簡単に行えます!
Apacheライセンスなので、実際に使用する場合は多分ライセンス条項等の表示が必要ですね。

今回はBTの受信のみですので、必要なファイルは以下の3つになります。
 BluetoothA2DPCommon.h
 BluetoothA2DPSink.h
 BluetoothA2DPSink.cpp


メインファイル
拡張子を ino にして、"ファイル名"と同じフォルダを作ってその中に置きます。上の3ファイルも同じディレクトリに入れます。画面をタッチする度に、信号処理部のON/OFFを切り替えるbypassフラグを反転させています。

ファイル名.ino

#include <M5Core2.h>
#include "BluetoothA2DPSink.h"
BluetoothA2DPSink a2dp_sink;
bool bypass = false;

void setup() {
 M5.begin(true, true, true, true);
 a2dp_sink.start("AudiiSion");
 //a2dp_sink.set_stream_reader(read_data_stream, true);
 M5.Lcd.setTextSize(2);
 M5.Lcd.print("\nAudiiSion Sound Lab.\n");
 char strtmp[100];
 sprintf(strtmp, "AudiiSion EP Ver.%s", "1.00");
 M5.Lcd.print(strtmp);
 M5.Lcd.setTextSize(3);
 M5.Lcd.setCursor(0, 100);
 M5.Lcd.print("ON ");
 delay(1000); 
}

void loop() {
 static int intCnt = 100;
 if (intCnt-- <= 1) {
   M5.update();
   Event& e = M5.Buttons.event;
   if (e & (E_TOUCH)) {
     // E_TOUCH, E_RELEASE, E_TAP, E_DBLTAP, E_PRESSING, E_PRESSED, E_LONGPRESSIONG, E_LONGPRESSED
     bypass = !bypass;
     M5.Lcd.setCursor(0, 100);
     M5.Lcd.setTextSize(3);
     if (!bypass) {
       M5.Lcd.print("ON ");
     } else {
       M5.Lcd.print("OFF");
     }
     a2dp_sink.start("AudiiSion");
     delay(200);
   }
   intCnt = 100;
 }
}

信号処理部
BluetoothA2DPSink.cpp を書き換えます。
DMAバッファサイズの変更と、audio_data_callback() に実際の処理を入れます。

サンプルのread_data_stream() に
// Do something with the data packet
と書いてあるのでここに信号処理が書けるのかと思ったのですが、そのままでは何もできませんでした。
setup() で
a2dp_sink.set_stream_reader(read_data_stream, true);
とすれば、参照だけはできましたが、データを書き戻しても反映されませんでした。

結局、BluetoothA2DPSink.cpp の audio_data_callback() に直接書く以外の方法が分からなかったのでそうします。

void BluetoothA2DPSink::audio_data_callback(const uint8_t *data, uint32_t len) {
 ESP_LOGD(BT_AV_TAG, "%s", __func__);
 if (is_i2s_output) {
   int16_t* wdata = (int16_t*)data;
   for (int k = 0; k < len / 2; k += 2) {
     int16_t L_in = wdata[k];  int16_t R_in = wdata[k + 1];
     // 任意の信号処理
     wdata[k] = L_in;  wdata[k + 1] = R_in;
   }
   size_t i2s_bytes_written;
   if (i2s_write(i2s_port, (void*) data, len, &i2s_bytes_written, portMAX_DELAY) != ESP_OK) {
     ESP_LOGE(BT_AV_TAG, "i2s_write has failed");
   }
   if (i2s_bytes_written < len) {
     ESP_LOGE(BT_AV_TAG, "Timeout: not all bytes were written to I2S");
   }
 }

 if (stream_reader != NULL) {
   stream_reader(data, len);
 }
 if (data_received != NULL) {
   data_received();
 }
}

で行けるのですが、ちょっとした処理をするとタイムオーバーで勝手にリセットが掛かってしまいますので、DMAバッファサイズを大きくします。

BluetoothA2DPSink::BluetoothA2DPSink() {
         ~
     .dma_buf_count = 8,
     .dma_buf_len = 1024,
         ~

dma_buf_len の最大値は1024のようです。
dma_buf_count の意味がよく分からないのですが、ある程度大きくした方が安定します。(^-^;

経過時間表示
ついでに、処理に掛かった時間表示も入れます。
 BTから送られてくるオーディオサンプルのフレームサイズ
 フレーム時間
 フレーム毎の処理に掛かった時間、その平均
 最小-最大、平均時間/フレーム時間
をそれぞれ表示しています。
bypassではないときだけmin/max/aveの更新をし、bypassの時は毎回の経過時間のみ更新することにします。表示の更新は20フレーム毎としています。ETCnt のオーバーフローは考えないことにします!

画像3

#include "BluetoothA2DPSink.h" の後に以下を挿入します。

#include <M5Core2.h>
char strTemp[100];
static int updateCnt = 0;
static float maxET = 0, minET = 0, aveET = 0, ETCnt = 0;

audio_data_callback() に、実際の処理と、処理時間表示を入れます。

void BluetoothA2DPSink::audio_data_callback(const uint8_t *data, uint32_t len) {
 ESP_LOGD(BT_AV_TAG, "%s", __func__);
extern bool bypass;
float fs = 44.1;
if (is_i2s_output) {
    int stTime = micros();
	int16_t* wdata = (int16_t*)data;
    if (!bypass) {
		for (int k = 0; k < len / 2; k += 2) {
			int16_t L_in = wdata[k];  int16_t R_in = wdata[k + 1];
     		// 任意の信号処理
			wdata[k] = L_in;  wdata[k + 1] = R_in;
		}
    }
   int nowTime = micros();
   float esTime = (float)(nowTime - stTime) / 1000;
   if (!bypass) {
     maxET = max(maxET, esTime);  minET = min(minET, esTime);
     aveET = (aveET * (ETCnt++) + esTime) / ETCnt;
   }
   if (updateCnt-- == 0) {
     updateCnt = 20;
     M5.Lcd.setTextSize(2);
     M5.Lcd.setCursor(0, 150);
     float frTime = (float)len / 4 / fs;
     sprintf(strTemp, "Frame Size %d(Samples)\n %4.2f(ms)\n", len / 4, frTime);
     M5.Lcd.print(strTemp);
     sprintf(strTemp, "Time(ms) %4.2f %4.2f(ave)\n%4.2f - %4.2f  ", esTime, aveET, minET, maxET);
     M5.Lcd.print(strTemp);
     sprintf(strTemp, "%3.1f%%(ave)  ", aveET * 100 / frTime);
     M5.Lcd.print(strTemp);
   }
   size_t i2s_bytes_written;
   if (i2s_write(i2s_port, (void*) data, len, &i2s_bytes_written, portMAX_DELAY) != ESP_OK) {
     ESP_LOGE(BT_AV_TAG, "i2s_write has failed");
   }
   if (i2s_bytes_written < len) {
     ESP_LOGE(BT_AV_TAG, "Timeout: not all bytes were written to I2S");
   }
}

問題点

・タッチ判定安定度
 軽くさわるとうまく切り替わりません。
 しっかり目にタッチするとそこそこ安定して切り替わりますが、もっと安定させたいところです。

・タッチ時ノイズ
 タッチすると、イヤホンに「ビビッ!」というノイズが出ます。
 多分、I2Sに何か出てるのではないかと思いますが、止める方法が分かりません。

・開発途中、たまにLCDが何も表示されなかったり、一度電源も入らないことがありました。簡単なサンプルを書き込むと復旧しましたが。(¬_¬) 

まとめ

ESP32はfloat型演算用のコプロも搭載されています。
自分の信号処理もfloat版とint版で試してみました。やはりint版の方が少し速いようですが、floatでもかなりの処理がリアルタイムで動きそうです。
doubleで書いてしまうと遅くなりますので、floatに。

しかし、細かいところで色々苦労はしますが、BTでのA2DP伝送がこんなに簡単にできるとは驚きです! 特に「高音質」というわけではありませんが、ノイズもなくそこそこの音で鳴ってくれます。

基板だけなら千円くらいですが、M5Stack Core2 はケースに入れたりする手間がなく、電池&タッチ液晶付きなのでとても気軽に使えます。

PCならもちろん何でもすぐできるのですが、今一つガジェット感に欠けて面白みがありません。これなら、スマホと M5Stack Core2 単体で動作するので、MATLABに飽きたらMATLABで気分転換しているような方にもお勧めです。(u_u)

半導体不足で入手困難になる前に、とりあえず手に入れてみてはいかがでしょうか?( ̄ー ̄)

この記事が気に入ったらサポートをしてみませんか?