オーディオ信号処理のプロトタイピングを 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に繋ぐだけです。
イヤホンアンプ
プロトモジュールに、適当なI2Sヘッドホンアンプボードを繋ぎます。
今回は UDA1334 を使いました。PCM5102 とかでも良いですね。
国内では千円前後するので、2週間~1ヶ月とか待てるなら海外サイトで買った方が良いでしょう。
配線は、以下のように5本繋げばOKです。(裏で配線してます)
UDA1334 VIN GND WSEL DIN BCLK
M-BUS 5V GND G25 G22 G26
後述する 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 のオーバーフローは考えないことにします!
#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)
半導体不足で入手困難になる前に、とりあえず手に入れてみてはいかがでしょうか?( ̄ー ̄)