#1. はじめに
M5StickC には マイクロフォンがついており
HAT として スピーカーに簡単接続できる
最近ESPNowっぽいものでセンサを繋ぎまくった経緯があり
どれくらいの大きいデータを送信できるのかの検証を兼ねて
作ってみる
- 近くのホームセンターで ESPNow で 33m の通信が出来感動したことは言うまでもありません
- 最初は なんちゃってBle通信だったんですけど 10m くらいしか出来なかった
#2. M5StickC I2Sの扱い方
色々とWEBをさまよったあげく
lang-shipさんの素敵なサイトに辿りつく
なるほど..
録音は I2S Mic -> soundBuffer -> soundStorage
再生は soundStorage -> soundBuffer -> I2S Speaker
って感じで食わせるという事なんですね
#3. HATを使ってI2Sで再生もできちゃうのね
2個の M5StickC は持っているのだが
HAT は 1個しかない... 100円ガチャで入手したスピーカーとか
繋いでも大丈夫なんだろうか...
という事で半田コテで GND/G26 に繋がるように製作する
#4. 仕様
二つの M5StickC に同じプログラムを転送して
録音が終わったら soundStorage 全体を ESPNow で転送して
ブロードキャストしてあげれば MacAddress に関係なく
受信した側は soundStorage の中身を再生すれば良い
これでトランシーバーっぽいのが出来上がるという事になる
転送の開始と転送の最後に1Byteのフラグ(STX/ETX)を送り
旨い具合に同期を取る感じで....
- ESPNOW_SEND_DELAY が3になってますが delay を入れないと大きいデータは上手い具合に同期が取れないようで resurt を見てちょっと愕然としました
//============================================> myESPNow.h
//
// M5Stick-mic-3.ino
// +-----+
// | |
// | | Rec:
// | | I2S Mic -> soundBuffer -> soundStorage
// | |
// | | Play:
// |M5 | soundStorage -> soundBuffer -> I2S Speaker
// |Stick|
// +-----+
//
// M5WalkyTalky.ino
// +-----+ +-----+
// | | | |
// | | Rec: | |
// | | I2S Mic -> soundBuffer -> soundStorage | |
// | | | |
// | | :Play | |
// |Talk | soundStorage -> soundBuffer -> I2S Speaker |Listn|
// | er| | er|
// +-----+ +-----+
//
#include <esp_now.h>
#include <WiFi.h>
esp_now_peer_info_t slave;
#define ESPNOW_MAXSEND (250) // ESPNow送信最大値
#define ESPNOW_SEND_DELAY (3) // 最低の待ち時間
#define STX (0x02) // 転送開始
#define ETX (0x03) // 転送終了
// MacAdrsを表示する
void dispAdrs(const uint8_t *mac_addr)
{
char macStr[18];
snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X",
mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
M5.Lcd.println(macStr);
}
void prePlay()
{
// 初期雑音を消す
memset(soundStorage, 0, sizeof(soundStorage));
recPos = 128;
i2sPlay();
}
// タイトル表示
void titleDisp()
{
M5.Lcd.print(" ");
M5.Lcd.setTextColor(WHITE, BLACK);
M5.Lcd.println(" M5WalkyTalky ");
M5.Lcd.setTextColor(BLACK, WHITE);
M5.Lcd.println(" BtnA Rec/Speak" );
M5.Lcd.println(" BtnB Play" );
}
// データ送信コールバック
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
// dispAdrs(mac_addr);
// M5.Lcd.println(status == ESP_NOW_SEND_SUCCESS ? "Success" : "Fail");
}
// データ受信コールバック
void OnDataRecv(const uint8_t *mac_addr, const uint8_t *recv, int recvcnt) {
static int row_ = 0;
// dispAdrs(mac_addr);
Serial.printf("[recv] recvcnt:%d\n", recvcnt);
if (recvcnt == 1) {
// 転送された長さが1Byte?
if (recv[0] == STX) {
// STX?
row_ = 0;
recPos = 0;
memset(soundStorage, 0x0, sizeof(soundStorage));
} else {
// ETX?
Serial.printf("[recv] row:%d recPos:%d\n", row_, recPos);
digitalWrite(10, !HIGH);
M5.Lcd.setCursor(0, 24);
M5.Lcd.println(" Play... ");
M5.Lcd.setCursor(0, 32);
M5.Lcd.printf(" Recv <= %5d Byte", recPos);
i2sPlay();
M5.Lcd.setCursor(0, 24);
M5.Lcd.println(" ");
digitalWrite(10, !LOW);
}
} else {
// recvcntが1以外
memcpy(&soundStorage[recPos], recv, recvcnt);
recPos += recvcnt;
row_ ++;
}
}
void resultCheck(int cnt, esp_err_t result)
{
switch (result) {
case ESP_OK:
break;
case ESP_ERR_ESPNOW_NOT_INIT:
Serial.printf("%d:ESPNOW not Init.\n", cnt);
break;
case ESP_ERR_ESPNOW_ARG:
Serial.printf("%d:Invalid Argument.\n", cnt);
break;
case ESP_ERR_ESPNOW_INTERNAL:
Serial.printf("%d:Internal Error.\n", cnt);
break;
case ESP_ERR_ESPNOW_NO_MEM:
Serial.printf("%d:ESP_ERR_ESPNOW_NO_MEM.\n", cnt);
break;
case ESP_ERR_ESPNOW_NOT_FOUND:
Serial.printf("%d:Peer not found.\n", cnt);
break;
default:
Serial.printf("%d:Not sure what happened.\n", cnt);
break;
}
}
void sendESPNow()
{
uint8_t dt[1] = {0};
size_t sendPos = 0;
int mod = recPos % ESPNOW_MAXSEND;
int row = recPos / ESPNOW_MAXSEND;
esp_err_t result;
Serial.printf("[send] row:%d mod:%d\n", row, mod);
M5.Lcd.setCursor(0, 32);
M5.Lcd.printf(" Send => %5d Byte", recPos);
dt[0] = STX;
result = esp_now_send(slave.peer_addr, dt, 1);
delay(ESPNOW_SEND_DELAY);
resultCheck(-1, result);
int i;
for (i = 0; i < row; i++) {
result = esp_now_send(slave.peer_addr, &soundStorage[sendPos], ESPNOW_MAXSEND);
delay(ESPNOW_SEND_DELAY);
resultCheck(i, result);
sendPos += ESPNOW_MAXSEND;
}
if (mod) {
result = esp_now_send(slave.peer_addr, &soundStorage[sendPos], mod);
delay(ESPNOW_SEND_DELAY);
resultCheck(i, result);
sendPos += mod;
}
dt[0] = ETX;
result = esp_now_send(slave.peer_addr, dt, 1);
delay(ESPNOW_SEND_DELAY);
resultCheck(-1, result);
Serial.printf("[send] sendPos %d\n", sendPos);
}
void setupESPNow()
{
WiFi.mode(WIFI_STA);
WiFi.disconnect();
if (esp_now_init() == ESP_OK) {
Serial.print("ESPNow Init Success\n");
} else {
Serial.print("ESPNow Init Failed\n");
ESP.restart();
}
pinMode(10, OUTPUT);
digitalWrite(10, !LOW);
memset(&slave, 0x0, sizeof(slave));
for (int i = 0; i < sizeof(slave.peer_addr); ++i) {
slave.peer_addr[i] = (uint8_t)0xff;
}
esp_err_t addStatus = esp_now_add_peer(&slave);
esp_now_register_send_cb(OnDataSent);
esp_now_register_recv_cb(OnDataRecv);
}
//<============================================ myESPNow.h
#5. lang-shipさんの ino に組み込む
- M5WalkyTalky.ino myESPNow.h は同じフォルダに入れて下さい
- 立ち上がりに スピーカーから雑音が出ちゃうので prePlay(recPos = 128 で短い再生)を実行
-
100KByte
を確保しようとすると謎のエラー しかたなく80KByte
に調整 - サンプリングレートは 44100 で 3秒くらいしか音が出ない、それだと話にならないので 16384 で調整、これで5秒は話せる
- 録音は
BtnA
、 離すと自動的にブロードキャストします - 話したあとに
BtnB
で再生出来ます - 受信した側も
BtnB
で再度再生が出来ます - 修正部分には
//edit
がついてます
//
// M5StickCのマイクを使ってみる その3 録音再生
// https://lang-ship.com/blog/work/m5stickc-mic-3/
//
//
#include <M5StickC.h>
#include <driver/i2s.h>
#define htonl(x) ( ((x)<<24 & 0xFF000000UL) | \
((x)<< 8 & 0x00FF0000UL) | \
((x)>> 8 & 0x0000FF00UL) | \
((x)>>24 & 0x000000FFUL) )
#define PIN_CLK (0) // I2S Clock PIN
#define PIN_DATA (34) // I2S Data PIN
#define SAMPLING_RATE (16384) // サンプリングレート(44100, 22050, 16384, 8192, more...) // edit
#define BUFFER_LEN (1024 * 1) // バッファサイズ // edit
#define STORAGE_LEN (1024 * 80) // 本体保存容量(MAX 100K前後) // edit
#define WAVE_EXPORT (0) // WAVEファイルに出力するか
uint8_t soundBuffer[BUFFER_LEN]; // DMA転送バッファ
uint8_t soundStorage[STORAGE_LEN]; // サウンドデータ保存領域
bool recFlag = false; // 録音状態
int recPos = 0; // 録音の長さ
// 録音をする
void i2sRecord() {
// 録音用設定
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM),
.sample_rate = SAMPLING_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ALL_RIGHT,
.communication_format = I2S_COMM_FORMAT_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 2,
.dma_buf_len = BUFFER_LEN,
.use_apll = false,
.tx_desc_auto_clear = true,
.fixed_mclk = 0,
};
// PIN設定
i2s_pin_config_t pin_config;
pin_config.bck_io_num = I2S_PIN_NO_CHANGE;
pin_config.ws_io_num = PIN_CLK;
pin_config.data_out_num = I2S_PIN_NO_CHANGE;
pin_config.data_in_num = PIN_DATA;
// 録音設定実施
i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
i2s_set_pin(I2S_NUM_0, &pin_config);
i2s_set_clk(I2S_NUM_0, SAMPLING_RATE, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);
// 録音開始
recFlag = true;
xTaskCreatePinnedToCore(i2sRecordTask, "i2sRecordTask", 2048, NULL, 1, NULL, 1);
}
// 再生をする
void i2sPlay() {
// 再生設定
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN),
.sample_rate = SAMPLING_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format = I2S_COMM_FORMAT_I2S_MSB,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 2,
.dma_buf_len = BUFFER_LEN,
.use_apll = false,
.tx_desc_auto_clear = true,
.fixed_mclk = 0,
};
// 再生設定実施
i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
i2s_set_pin(I2S_NUM_0, NULL);
i2s_zero_dma_buffer(I2S_NUM_0);
// 再生
size_t transBytes;
size_t playPos = 0;
while ( playPos < recPos ) {
for ( int i = 0 ; i < BUFFER_LEN ; i += 2 ) {
soundBuffer[i] = 0; // 下位8ビットは無視される
soundBuffer[i + 1] = soundStorage[playPos]; // 上位8ビットにuint8_tのデータを入れる
playPos++;
}
// データ転送
i2s_write(I2S_NUM_0, (char*)soundBuffer, BUFFER_LEN, &transBytes, (100 / portTICK_RATE_MS));
// Serial.println(playPos); // edit
}
// 後始末
i2s_zero_dma_buffer(I2S_NUM_0);
i2s_driver_uninstall(I2S_NUM_0);
}
// 録音用タスク
void i2sRecordTask(void* arg)
{
// 初期化
recPos = 0;
memset(soundStorage, 0, sizeof(soundStorage));
// 録音処理
while (recFlag) {
size_t transBytes;
// I2Sからデータ取得
i2s_read(I2S_NUM_0, (char*)soundBuffer, BUFFER_LEN, &transBytes, (100 / portTICK_RATE_MS));
// int16_t(12bit精度)をuint8_tに変換
for (int i = 0 ; i < transBytes ; i += 2 ) {
if ( recPos < STORAGE_LEN ) {
int16_t* val = (int16_t*)&soundBuffer[i];
soundStorage[recPos] = ( *val + 32768 ) / 256;
recPos++;
} else {
digitalWrite(10, !LOW); // edit
}
}
Serial.printf("transBytes = %d, STORAGE_LEN=%d, recPos=%d\n", transBytes, STORAGE_LEN, recPos);
vTaskDelay(1 / portTICK_RATE_MS);
}
i2s_driver_uninstall(I2S_NUM_0);
// タスク削除
vTaskDelete(NULL);
}
#include "myESPNow.h" // edit
void setup() {
M5.begin();
M5.Lcd.setRotation(3);
M5.Lcd.fillScreen(WHITE);
M5.Lcd.setTextColor(BLACK, WHITE);
/* // edit
M5.Lcd.println("Sound Recorder");
M5.Lcd.println("BtnA Record");
M5.Lcd.println("BtnB Play");
*/
setupESPNow(); // edit
prePlay(); // edit
titleDisp(); // edit
}
void loop() {
M5.update();
if ( M5.BtnA.wasPressed() ) {
digitalWrite(10, !HIGH); // edit
// 録音スタート
M5.Lcd.setCursor(0, 24);
M5.Lcd.println(" Rec... ");
Serial.println("Record Start");
i2sRecord();
} else if ( M5.BtnA.wasReleased() ) {
digitalWrite(10, !LOW); // edit
// 録音ストップ
M5.Lcd.setCursor(0, 24);
M5.Lcd.println(" ");
recFlag = false;
delay(100); // 録音終了まで待つ
sendESPNow(); // edit
Serial.println("Record Stop");
// WAVEファイルをシリアルに出力
if ( WAVE_EXPORT ) {
Serial.printf("52494646"); // RIFFヘッダ
Serial.printf("%08lx", htonl(recPos + 44 - 8)); // 総データサイズ+44(チャンクサイズ)-8(ヘッダサイズ)
Serial.printf("57415645"); // WAVEヘッダ
Serial.printf("666D7420"); // フォーマットチャンク
Serial.printf("10000000"); // フォーマットサイズ
Serial.printf("0100"); // フォーマットコード
Serial.printf("0100"); // チャンネル数
Serial.printf("%08lx", htonl(SAMPLING_RATE)); // サンプリングレート
Serial.printf("%08lx", htonl(SAMPLING_RATE)); // バイト/秒
Serial.printf("0200"); // ブロック境界
Serial.printf("0800"); // ビット/サンプル
Serial.printf("64617461"); // dataチャンク
Serial.printf("%08lx", htonl(recPos)); // 総データサイズ
for (int n = 0; n <= recPos; n++) {
Serial.printf("%02x", soundStorage[n]);
}
Serial.printf("\n");
}
} else if ( M5.BtnB.wasReleased() ) {
// 再生スタート
M5.Lcd.setCursor(0, 24);
M5.Lcd.println(" Play...");
Serial.println("Play Start");
i2sPlay();
M5.Lcd.setCursor(0, 24);
M5.Lcd.println(" ");
Serial.println("Play Stop");
}
delay(10);
}
#6. 最後に
ESPNow の転送はセンサーでは随分とやっていたのですが
さすがに 80KByte はきつかったです
でも これで写真でも映像でも送れる事がわかって一安心です
いつも素晴らしい src を公開してくださっている lang-shipさんに感謝です
今後ともよろしくお願い申し上げます
https://lang-ship.com/blog/