気の向くままに辿るIT/ICT/IoT
IoT・電子工作

ESP32とI2S対応マイクINMP441入力音源の保存と再生

ホーム前へ次へ
ESP8266って?

ESP32とI2S対応マイクINMP441入力音源の保存と再生

ESP32とI2S対応マイクINMP441入力音源の保存と再生

2023/03/21

 ESP32と比較的安価なI2S対応MEMSマイクロフォンモジュールINMP441でwavファイルに保存したオーディオ入力をVLCでネットワーク再生したり、サーバを建ててwavファイルにブラウザでアクセス、ダウンロードメニューからアプリを選んで各種メディアプレイヤーで再生したり、ダウンロード・保存後、aplay等で再生したりしてみた話。

経緯

 ポピュラーと言う割には、言語圏関係なく情報が少な過ぎじゃないかい?って感じのINMP441。

 そんな中、INMP441をESP32に接続、Arduino IDEで巷で良さげなArduino IDEのシリアルプロッタで入力した音源の波形を取得できるスケッチをアップロードしたら確かに波形は確認できました。

 さて、次は、音源の再生...。

 はて、どうやったら入力した音声などの音を確認できるんだっけ?音声をヘッダファイルとして再生はやってみたことはあれど、リアルタイム、もしくは保存済み音源ファイルの再生を、そのままESP32内蔵DACでしてみたい...が、あまり情報が見当たらない...。

 そこでリアルタイムや保存しつつの内蔵DAC再生は棚上げ、改めて探してESP32_INMP441_RECORDING.inoに行き着くもwav保存とSPIFFS上のファイルリストアップのみで再生ロジックがない...というわけでサーバにアクセスする恰好とし、後述のスケッチになった次第。

 INMP441を接続したESP32ボード1つでESP32内蔵DAC使ってそのまま再生できたら検証するには楽だけどと思ったんですけどね。

 I2S対応マイクx2、I2S対応DACx2でインターフォンのみならず、ESP32 WROVER+OV2640を加えてビデオドアフォンでもという道の途中、いろいろあってI2S DACがまだ未着な今、検証はともかく、ESPボード1つだと拡声器以外実用性もなさそうだし、少なくとも今は、そこは追求しなくていっかということで。

要注意

ESP32とピンヘッダ未ハンダ・ハンダ済みINMP441の配線

 どうやらINMP441は、接触不良に超敏感な模様。

 というのもINMP441にピンヘッダをハンダ付けしてあるか否かに関わらず、特にジャンパワイヤを使っている場合、それまで正常に機能していても少し回路を動かしただけでも、何も入力されないか、ノイズのみということになりかねないので注意。

 自身は、2個買って1つはピンヘッダをハンダ付け、もう1つは未ハンダな状態で前者にはジャンパワイヤのソケット(メス)を挿す恰好で、後者はブレッドボード上のINMP441のピンホールに直接、また、ピンヘッダを立てINMP441を挿して同列にジャンパワイヤ(オス)を挿したりしてみましたが、どのパターンも結果、接触不良でハマり、時間を浪費することになりました。

 シリアルプロッタなら波形でおおよそ確認可能も音源録音のみのスケッチで確認方法が再生のみだと再生時にのみ、音声入力できたか否か程度しかわかりません。

 よって入力できていなかった(ファイルを再生しても音が思うように録音できていなかった)場合、接触不良を疑って対処療法でジャンパワイヤやピンの部分をごにょごにょ抑えたり、離したり、束ねたりしてみたところで正常に機能しているのか否かを視覚的にも聴覚的にも確認できず、祈りながら録音し直し、改めて再生してみる程度しか、やれることがありません。

 これがあったのでPCで[arecord | aplay](ループバック?)するかのように同じESP32で内蔵DACからリアルタイムに音源出力を確認できたら良いのにと冒頭述べた通り、試みた次第。

 というわけで、とは言え、まだ試していないので、たぶんですが、少なくともESP32とINMP441は、PCBやユニバーサルボード等にハンダ付けしたソケットヘッダに挿しつつ、ジャンパワイヤを使うにしても配線もハンダ付けしてから確認する方が無難かと思われます。

2023/04/29

 ようやく重い腰を上げて先に進もうかと、もう1つの方のINMP441や2つのDACもハンダ付けしたところでテスト。

 後者は何の問題もないものの、前者が音声入力できない、雑音すら入らない、何度ピン上のハンダ付けをやり直しても改善されない...と思ったら、INMP441の周囲の金色の縁取り部分とピンの間が少しでもハンダでくっついてつながるとアウトであることが判明。

 そもそも金縁にハンダを付けないよう要注意。

 それとジャンパワイヤの接触不良による雑音、ピン挿入部を抑えるのもさることながら、それ以上にジャンパワイヤを限りなく真っ直ぐに伸ばすと雑音|ノイズが消えることが判明。

 特にGND、VDDとL/Rのジャンパワイヤ。

 っていうか、ハンダ付け、うまくなるどころか、どんどん下手になっていくのは、なぜ...。

ESP32とINMP441の配線

ESP32INMP441
3.3VVDD
GPIO 32SCK
GPIO 33SD
GPIO 25WS
GNDL/R
GND

 INMP441の電源電圧は、1.62V-3.63VなのでVDDは3.3Vです。

 INMP441のSCK/SD/WSについては、ESP32のGPIOは、これに限らず、順に14/GPIO 32/GPIO 15などでも良いようです。

 むしろ、I2S対応DACを併用する場合、ESP32ではGPIO 25/GPIO 26の2ピンがDAC出力として固定されており、両方とも使用するのでGPIOとして25や26は避ける必要があります。

 ちょっと知るたび混乱中...。

スケッチ

#include <driver/i2s.h>
#include <SPIFFS.h>
#include <WiFi.h>
#include <ESP32WebServer.h>
#include <ESPmDNS.h>
#include <FS.h>
 
ESP32WebServer webserver(80);
const char *ssid   = "ANY_SSID";
const char *password = "ANY_PASSPHRASE";
 
IPAddress local_IP(192, 168, 0, 250);
IPAddress gateway(192, 168, 0, 1);
IPAddress subnet(255, 255, 255, 0);
 
const char common_name[40] = "inmp441test";
const char* mdnsName = common_name;
#define BUFFER_SIZE 16384
uint8_t buf[BUFFER_SIZE];
 
#define I2S_WS 25
#define I2S_SD 33
#define I2S_SCK 32
#define I2S_PORT I2S_NUM_0
#define I2S_SAMPLE_RATE  (16000)
#define I2S_SAMPLE_BITS  (16)
#define I2S_READ_LEN   (16 * 1024)
#define RECORD_TIME    (30) // 秒
#define I2S_CHANNEL_NUM  (1)
#define FLASH_RECORD_SIZE (I2S_CHANNEL_NUM * I2S_SAMPLE_RATE * I2S_SAMPLE_BITS / 8 * RECORD_TIME)
 
File spiffs_file;
const char fname[] = "/recording.wav";
const int headerSize = 44;
 
void setup() {
 Serial.begin(115200);
 SPIFFSInit();
 i2sInit();
 xTaskCreate(i2s_adc, "i2s_adc", 1024 * 3, NULL, 1, NULL);
 
 startWiFi();
 startMDNS();
 startServer();
}
 
void loop() {
 webserver.handleClient();
}
 
void startWiFi() {
 WiFi.disconnect();
 
// if (!WiFi.config(local_IP, gateway, subnet, primaryDNS, secondaryDNS)) {
 if (!WiFi.config(local_IP, gateway, subnet)) {
  Serial.println("STA Failed to configure");
 }
 // WiFi.softAP(ssid, password);
 WiFi.mode(WIFI_STA);
 WiFi.begin(ssid, password);
 while ( WiFi.status() != WL_CONNECTED) {
  delay(500);
  Serial.print(".");
 }
 Serial.println("");
 Serial.print("SSID \"");
 Serial.print(ssid);
 Serial.println("\" started\r\n");
 
 Serial.print("Connected to ");
 Serial.println(ssid);
 
 Serial.print("IP address: ");
 Serial.println(WiFi.localIP());
 
 // Serial.print("hostname : ");
 // Serial.println(WiFi.hostname());
 Serial.println("");
}
 
void startMDNS() {
 MDNS.begin(mdnsName);
 Serial.print("mDNS responder started: http://");
 Serial.print(mdnsName);
 Serial.println(".local");
}
 
void startServer() {
 webserver.on("/", handleRoot);
 webserver.on("/edit.html", HTTP_POST, []() {
  webserver.send(200, "text/plain", "");
 }, handleFileUpload);
 
 webserver.onNotFound(handleNotFound);
 
 webserver.begin();
 Serial.println("HTTP webserver started.");
}
 
void handleNotFound() {
 if (!handleFileRead(webserver.uri())) {
  webserver.send(404, "text/plain", "404: File Not Found");
 }
}
 
bool handleFileRead(String path) {
 Serial.println("handleFileRead: " + path);
 if (path.endsWith("/")) path += "index.html";
 String contentType = getContentType(path);
 String pathWithGz = path + ".gz";
 if (SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)) {
  if (SPIFFS.exists(pathWithGz))
   path += ".gz";
  File spiffs_file = SPIFFS.open(path, "r");
  size_t sent = webserver.streamFile(spiffs_file, contentType);
  spiffs_file.close();
  Serial.println(String("\tSent spiffs_file: ") + path);
  return true;
 }
 Serial.println(String("\tFile Not Found: ") + path);
 return false;
}
 
void handleRoot() {
 Serial.println("Access");
 char message[20];
 String(webserver.arg(0)).toCharArray(message, 20);
 webserver.send(200, "text/html", (char *)buf);
}
 
void handleFileUpload() {
 HTTPUpload upload = webserver.upload();
 String path;
 if (upload.status == UPLOAD_FILE_START) {
  path = upload.file;
  if (!path.startsWith("/")) path = "/" + path;
  if (!path.endsWith(".gz")) {
   String pathWithGz = path + ".gz";
   if (SPIFFS.exists(pathWithGz))
    SPIFFS.remove(pathWithGz);
  }
  Serial.print("handleFileUpload Name: ");
  Serial.println(path);
  spiffs_file = SPIFFS.open(path, "w");
  path = String();
 } else if (upload.status == UPLOAD_FILE_WRITE) {
  if (spiffs_file)
   spiffs_file.write(upload.buf, upload.currentSize);
 } else if (upload.status == UPLOAD_FILE_END) {
  if (spiffs_file) {
   spiffs_file.close();
   Serial.print("handleFileUpload Size: ");
   Serial.println(upload.totalSize);
   webserver.sendHeader("Location", "/success.html");
   webserver.send(303);
  } else {
   webserver.send(500, "text/plain", "500: couldn't create spiffs_file");
  }
 }
}
 
String formatBytes(size_t bytes) {
 if (bytes < 1024) {
  return String(bytes) + "B";
 } else if (bytes < (1024 * 1024)) {
  return String(bytes / 1024.0) + "KB";
 } else if (bytes < (1024 * 1024 * 1024)) {
  return String(bytes / 1024.0 / 1024.0) + "MB";
 }
}
 
String getContentType(String fname) {
 if (fname.endsWith(".html")) return "text/html";
 else if (fname.endsWith(".css")) return "text/css";
 else if (fname.endsWith(".json")) return "text/css";
 else if (fname.endsWith(".js")) return "application/javascript";
 else if (fname.endsWith(".ico")) return "image/x-icon";
 else if (fname.endsWith(".png")) return "image/x-icon";
 else if (fname.endsWith(".gz")) return "application/x-gzip";
 return "text/plain";
}
void SPIFFSInit(){
 if(!SPIFFS.begin(true)){
  Serial.println("SPIFFS initialisation failed!");
  while(1) yield();
 }
 SPIFFS.remove("/test.wav");
 SPIFFS.remove(fname);
 spiffs_file = SPIFFS.open(fname, FILE_WRITE);
 if(!spiffs_file){
  Serial.println("File is not available!");
 }
 
 byte header[headerSize];
 wavHeader(header, FLASH_RECORD_SIZE);
 
 spiffs_file.write(header, headerSize);
 listSPIFFS();
}
 
void i2sInit(){
 esp_err_t err;
 i2s_config_t i2s_config = {
  .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
  .sample_rate = I2S_SAMPLE_RATE,
  .bits_per_sample = i2s_bits_per_sample_t(I2S_SAMPLE_BITS),
  .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
  .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_STAND_I2S | I2S_COMM_FORMAT_STAND_MSB),
  .intr_alloc_flags = 0,
  .dma_buf_count = 8,
  .dma_buf_len = 1024,
  .use_apll = 1
 };
 
 err = i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL);
 if (err!=ESP_OK) {
  Serial.println("Failed to install driver");
 } else {
  Serial.println("Installed driver");
 }
 
 const i2s_pin_config_t pin_config = {
  .bck_io_num = I2S_SCK,
  .ws_io_num = I2S_WS,
  .data_out_num = -1,
  .data_in_num = I2S_SD
 };
 
 err = i2s_set_pin(I2S_PORT, &pin_config);
 if (err!=ESP_OK) {
  Serial.println("Failed to install pin");
 } else {
  Serial.println("Installed pin");
 }
}
 
void i2s_adc_data_scale(uint8_t * d_buff, uint8_t* s_buff, uint32_t len)
{
 uint32_t j = 0;
 uint32_t dac_value = 0;
 for (int i = 0; i < len; i += 2) {
  dac_value = ((((uint16_t) (s_buff[i + 1] & 0xf) << 8) | ((s_buff[i + 0]))));
  d_buff[j++] = 0;
  d_buff[j++] = dac_value * 256 / 2048;
 }
}
 
void i2s_adc(void *arg)
{
// i2s_start(I2S_PORT);
 int i2s_read_len = I2S_READ_LEN;
 int flash_wr_size = 0;
 size_t bytes_read;
 
 char* i2s_read_buff = (char*) calloc(i2s_read_len, sizeof(char));
 uint8_t* flash_write_buff = (uint8_t*) calloc(i2s_read_len, sizeof(char));
 
 i2s_read(I2S_PORT, (void*) i2s_read_buff, i2s_read_len, &bytes_read, portMAX_DELAY);
 i2s_read(I2S_PORT, (void*) i2s_read_buff, i2s_read_len, &bytes_read, portMAX_DELAY);
  
 Serial.println(" *** Recording Start *** ");
 while (flash_wr_size < FLASH_RECORD_SIZE) {
  //read data from I2S bus, in this case, from ADC.
  i2s_read(I2S_PORT, (void*) i2s_read_buff, i2s_read_len, &bytes_read, portMAX_DELAY);
  //example_disp_buf((uint8_t*) i2s_read_buff, 64);
  //save original data from I2S(ADC) into flash.
  i2s_adc_data_scale(flash_write_buff, (uint8_t*)i2s_read_buff, i2s_read_len);
  spiffs_file.write((const byte*) flash_write_buff, i2s_read_len);
  flash_wr_size += i2s_read_len;
  ets_printf("Sound recording %u%%\n", flash_wr_size * 100 / FLASH_RECORD_SIZE);
  ets_printf("Never Used Stack Size: %u\n", uxTaskGetStackHighWaterMark(NULL));
 }
 spiffs_file.close();
 
 free(i2s_read_buff);
 i2s_read_buff = NULL;
 free(flash_write_buff);
 flash_write_buff = NULL;
 listSPIFFS();
// i2s_stop(I2S_PORT);
 vTaskDelete(NULL);
}
 
void example_disp_buf(uint8_t* buf, int length)
{
 printf("======\n");
 for (int i = 0; i < length; i++) {
  printf("%02x ", buf[i]);
  if ((i + 1) % 8 == 0) {
   printf("\n");
  }
 }
 printf("======\n");
}
 
void wavHeader(byte* header, int wavSize){
 header[0] = 'R';
 header[1] = 'I';
 header[2] = 'F';
 header[3] = 'F';
 unsigned int fileSize = wavSize + headerSize - 8;
 header[4] = (byte)(fileSize & 0xFF);
 header[5] = (byte)((fileSize >> 8) & 0xFF);
 header[6] = (byte)((fileSize >> 16) & 0xFF);
 header[7] = (byte)((fileSize >> 24) & 0xFF);
 header[8] = 'W';
 header[9] = 'A';
 header[10] = 'V';
 header[11] = 'E';
 header[12] = 'f';
 header[13] = 'm';
 header[14] = 't';
 header[15] = ' ';
 header[16] = 0x10;
 header[17] = 0x00;
 header[18] = 0x00;
 header[19] = 0x00;
 header[20] = 0x01;
 header[21] = 0x00;
 header[22] = 0x01;
 header[23] = 0x00;
 header[24] = 0x80;
 header[25] = 0x3E;
 header[26] = 0x00;
 header[27] = 0x00;
 header[28] = 0x00;
 header[29] = 0x7D;
 header[30] = 0x00;
 header[31] = 0x00;
 header[32] = 0x02;
 header[33] = 0x00;
 header[34] = 0x10;
 header[35] = 0x00;
 header[36] = 'd';
 header[37] = 'a';
 header[38] = 't';
 header[39] = 'a';
 header[40] = (byte)(wavSize & 0xFF);
 header[41] = (byte)((wavSize >> 8) & 0xFF);
 header[42] = (byte)((wavSize >> 16) & 0xFF);
 header[43] = (byte)((wavSize >> 24) & 0xFF);
}
 
 
void listSPIFFS(void) {
 Serial.println(F("\r\nListing SPIFFS files:"));
 static const char line[] PROGMEM = "=================================================";
 
 Serial.println(FPSTR(line));
 Serial.println(F(" File name               Size"));
 Serial.println(FPSTR(line));
 
 fs::File root = SPIFFS.open("/");
 if (!root) {
  Serial.println(F("Failed to open directory"));
  return;
 }
 if (!root.isDirectory()) {
  Serial.println(F("Not a directory"));
  return;
 }
 
 fs::File file = root.openNextFile();
 while (file) {
 
  if (file.isDirectory()) {
   Serial.print("DIR : ");
   String fileName = file.name();
   Serial.print(fileName);
  } else {
   String fileName = file.name();
   Serial.print(" " + fileName);
   // File path can be 31 characters maximum in SPIFFS
   int spaces = 33 - fileName.length(); // Tabulate nicely
   if (spaces < 1) spaces = 1;
   while (spaces--) Serial.print(" ");
   String fileSize = (String) file.size();
   spaces = 10 - fileSize.length(); // Tabulate nicely
   if (spaces < 1) spaces = 1;
   while (spaces--) Serial.print(" ");
   Serial.println(fileSize + " bytes");
  }
 
  file = root.openNextFile();
 }
 
 Serial.println(FPSTR(line));
 Serial.println();
 delay(1000);
}

 ESP32+INMP441からの入力音源をもとにSPIFFS上に.wavファイルを生成、リストアップしてくれるESP32_INMP441_RECORDING.inoに、これまで当サイトで主に自作スマート家電系で使ってきたソースを組み合わせてmDNSでもアクセス可能なサーバを建ててみたスケッチがこれです。

 このスケッチをアップして実行後、すぐにシリアルモニタを開いて録音、.wavファイルがリストアップされるのを確認した後、VLCなどストリーミング再生できるメディアプレイヤーか、ブラウザでIPアドレス、またはmDNSアドレス(.local)込みで当該ファイルにアクセスする恰好になります。

 前者だとそのまま再生、後者だと多くの場合、ダイヤログがポップアップされ、指定メディアプレイヤーでの再生かダウンロードを選択する流れになります。

ストリーミング再生

 よって、こうした機能を持つ任意の対応メディアプレイヤーを使って先のサーバ上にある録音済みの.wav等のファイルを指定すれば、ストリーミング再生できます。

 例えば、VLC Media Playerには、[Media (M)]メニューに[ネットワークストリームを開く(N)]があり、HTTPプロトコルも使えるのでIPアドレスや.localとファイル名でアクセスすることでストリーミング再生することができます。

一時保存の音源ファイルを自動再生

 ほとんどのブラウザでは、音源ファイルのURLにアクセスすると指定するメディアプレイヤーで再生するかファイルとして保存するかを選択できるようになっています。

 ここで前者を選べば、指定した音源ファイルを一時ファイルとしてダウンロード、自動再生することができます。

 ログイン中は、一時ファイルのパスさえ確認して指定できれば、再度、再生することもできますが、普通は確認のために1度だけ聴くというケースが多いでしょう。

ローカル保存の音源ファイルを再生

 ほとんどのブラウザでは、音源ファイルのURLにアクセスすると指定するメディアプレイヤーで再生するかファイルとして保存するかを選択できるようになっています。

 ここで後者を選ぶと、ダウンロード後、aplayなどCLIやGUIの任意のメディアプレイヤーで当該ファイルを指定して再生することができます。

ホーム前へ次へ