M5StickCのマイクを使ってみる その3 録音再生

現時点の情報ですので、最新情報はM5StickC非公式日本語リファレンスを確認してください。

概要

かなり苦労しましたが、なんとか録音と再生ができました!

M5StickCにスピーカーHATを装着した状態で、ホームボタンを押している間録音して、右ボタンを押すと再生します。

録音データは符号付き16ビット(12ビット精度)ですが、再生データは符号なし8ビットとフォーマットが違うので、ちょっと苦労しましたが、どうせ8ビットのデータしか使わないだろうってことで、録音時に8ビットにまるめて保存しています。

録音データ概要

M5StickC内蔵I2Sマイクの録音データは、符号付き16ビットですので、2バイト単位で送信されてきます。精度は12ビットのようですが、実際のデータは16ビットなので、精度を意識しなくてもよいと思います。

データの範囲は-32767から32768までで、0が無音のはずです。

しかしながら、録音したデータをみると、データが少し下にオフセットされています。無音状態のものを確認しても、私の端末はマイナスにオフセットされて、録音されています。

補正しようと思いましたが、なるべくシンプルなサンプルコードにしようと思い、まずは無補正のままにしたいと思います。

また、音も小さいので、ゲインコントロールもしたほうがいいと思います。

再生データ概要

ESP32のDACは8ビットでの出力になります。0から255までで127か128が無音になるはずです。

録音が12ビット精度で、再生が8ビット精度なので、内部保存データも8ビットに合わせました。外部に出力する場合には16ビットで保存してもよいとおもいますが、そもそもそんなにマイクの性能が高くないので8ビットでも、音質はほとんど変わらないと思います。

サンプルコード

#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, more...)
#define BUFFER_LEN    (1024)        // バッファサイズ
#define STORAGE_LEN   (102400)      // 本体保存容量(MAX 100K前後)

#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);
  }

  // 後始末
  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++;
      }
    }
    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);
}

void setup() {
  M5.begin();
  M5.Lcd.setRotation(3);
  M5.Lcd.fillScreen(WHITE);
  M5.Lcd.setTextColor(BLACK, WHITE);
  M5.Lcd.println("Sound Recorder");
  M5.Lcd.println("BtnA Record");
  M5.Lcd.println("BtnB Play");
}

void loop() {
  M5.update();

  if ( M5.BtnA.wasPressed() ) {
    // 録音スタート
    M5.Lcd.setCursor(0, 24);
    M5.Lcd.println("REC...");
    Serial.println("Record Start");
    i2sRecord();
  } else if ( M5.BtnA.wasReleased() ) {
    // 録音ストップ
    M5.Lcd.setCursor(0, 24);
    M5.Lcd.println("      ");
    recFlag = false;
    delay(100); // 録音終了まで待つ

    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.println("       ");
    Serial.println("Play Stop");
  }

  delay(10);
}

なるべくプレーンなコードにしています。オフセット値とゲインなどの調整が必要なので、もうちょっと手をいれる予定です。実験コードなのでGitHubにもあげない予定です。

またタスク周りとか、録音と再生でのI2Sドライバの共有方法などがちょっと危ういコードとなっています。

まとめ

コード自体はそれほど大変そうにみえないですが、情報が少ないので結構苦労しました。

あとスピーカーHATをつけていると、プログラム転送中とかに爆音ノイズがなるので注意してください。

プログラム転送直後もノイジーなので、一度ホームボタンを押して短時間録音してから接続するとノイズがならないと思います。本当はsetup()とかでi2s_zero_dma_buffer()を呼べば音が鳴らなくなると思うのですが、ドライバーをセットする前に呼ぶとリセットかかるので、今後調整したいと思います。

コメント

  1. fkd より:

    お世話になります。
    こちらを見てソースコードをコンパイルしたところ、

    ‘amp’ was not declared in this scope

    とのエラーが出て書き込むことが出来ませんでした。

    なにかお気づきの点がありましたらアドバイスを頂けませんでしょうか。
    何卒宜しくお願いします。

    • たなかまさゆき より:

      ブログの文字コピーすると&の文字が& amp;になってしまっています
      & a m p ; を&に置換して試してもらえますか?

    • たなかまさゆき より:

      設定変えて&が変換されなくしたので、再度コピーして試していただければと思います

  2. fkd より:

    何度もすみません、
    ご返答頂けていたのに気が付きませんでした。

    ご対応、ありがとうございます!
    今後とも宜しくお願いします。

  3. chromLinux より:

    いつも大変お世話になっております
    こちらの記事を元に ESPNow でトランシーバーっぽいのを作成しQiiaに上げました
    よろしくお願い申し上げます

    https://qiita.com/chrmlinux/items/aab5f439ee279bc05fbd

    • たなかまさゆき より:

      トランシーバー!
      できそうだと思っていましたが、実際に作るのはすごいです

      • chrmlinux より:

        ありがとうございます
        それでは 拙作のESPNowトランシーバーが たなかさんの次回著書に掲載されるのを楽しみにしておきます

        今後ともよろしくお願い申し上げます