M5StickCのマイクを使ってみる その2 録音してみた

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

概要

マイクからの入力データをオンメモリに保存して、シリアルモニタ経由で保存することができました。

SPIFFSとかに保存して、Wi-fi AP経由でダウンロードなども考えましたが、まずはかんたんに利用できるもので検証しました。

MICのスペックについて

型番SPM1423
データシートhttps://github.com/m5stack/M5-Schematic/blob/master/Core/SPM1423HM4H-B.pdf
製造元Knowles Electronics, LLC.
方向無指向性
周波数範囲100Hz ~ 10kHz
感度-22dB ±3dB @ 94dB SPL
S/N比61.5dB
電圧範囲1.6V ~ 3.6V(2.8V供給)
電流 – 供給600µA

マイクのスペックです。データシートはまだ読めていません。

マイクのデータ量について

サンプリングレート(Hz)転送量(Byte/s)転送単位(byte)転送回数(回/s)対象時間(ms/回)
44,10088,200512172.35.8
44,10088,2001,02486.111.6
44,10088,2002,04843.123.2
44,10088,2004,09621.546.4
44,10088,2008,19210.892.9
44,10088,20016,3845.4185.8
44,10088,20032,7682.7371.5
44,10088,20065,5361.3743.0
44,10088,200102,4000.91161.0
44,10088,200109,7140.81243.9
22,05044,10051286.111.6
22,05044,1001,02443.123.2
22,05044,1002,04821.546.4
22,05044,1004,09610.892.9
22,05044,1008,1925.4185.8
22,05044,10016,3842.7371.5
22,05044,10032,7681.3743.0
22,05044,10065,5360.71486.1
22,05044,100102,4000.42322.0
22,05044,100109,7140.42487.8
16,38432,76851264.015.6
16,38432,7681,02432.031.3
16,38432,7682,04816.062.5
16,38432,7684,0968.0125.0
16,38432,7688,1924.0250.0
16,38432,76816,3842.0500.0
16,38432,76832,7681.01000.0
16,38432,76865,5360.52000.0
16,38432,768102,4000.33125.0
16,38432,768109,7140.33348.2
11,02522,05051243.123.2
11,02522,0501,02421.546.4
11,02522,0502,04810.892.9
11,02522,0504,0965.4185.8
11,02522,0508,1922.7371.5
11,02522,05016,3841.3743.0
11,02522,05032,7680.71486.1
11,02522,05065,5360.32972.2
11,02522,050102,4000.24644.0
11,02522,050109,7140.24975.7

マイクは1サンプリングあたり2バイトです。実際には12ビットなので4ビット分は利用していないはずです。

サンプリングレートが44,100Hzの場合、1秒あたり88,200バイトのデータが必要になります。

DMA転送単位を512バイトにすると、1秒あたり172.3回転送しなければならず、1回あたり5.8ms以下で処理しないといけないことが、上の表からわかります。

サンプリングレートの目安ですが、CDが44,100Hzで、音声認識では16,384Hzがよく使われているようです。実際11,025Hzですと、がびがびで何を言っているか、わかりにくいです。

サンプルスケッチ

#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
#define PIN_DATA 34
#define SAMPLING_RATE (44100/2)
#define READ_LEN (2 * 512)
#define SAVE_LEN (900*100)
uint8_t READ_BUFFER[READ_LEN] = {0};
uint8_t SAVE_BUFFER[SAVE_LEN] = {0};
uint32_t savePos = 0;
bool recFlag = false;
uint16_t oldy[160];
int16_t *adcBuffer = NULL;
TaskHandle_t taskHandle;
size_t bytesread;
void i2sInit()
{
  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, // is fixed at 12bit, stereo, MSB
    .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 = READ_LEN,
  };
  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(mic_record_task, "mic_record_task", 2048, NULL, 1, &taskHandle, 1);
}
void mic_record_task (void* arg)
{
  savePos = 0;
  memset(SAVE_BUFFER, 0, SAVE_LEN);
  while (recFlag) {
    i2s_read(I2S_NUM_0, (char*) READ_BUFFER, READ_LEN, &bytesread, (100 / portTICK_RATE_MS));
    adcBuffer = (int16_t *)READ_BUFFER;
    showSignal();
    Serial.printf("bytesread = %d, SAVE_LEN=%d, savePos=%d\n", bytesread, SAVE_LEN, savePos);
    if ( (savePos + bytesread) < SAVE_LEN ) {
      memcpy( &SAVE_BUFFER[savePos], READ_BUFFER, bytesread );
      savePos += bytesread;
    }
    vTaskDelay(1 / portTICK_RATE_MS);
  }
  // タスク削除
  vTaskDelete(NULL);
}
void setup() {
  M5.begin();
  M5.Lcd.setRotation(3);
  M5.Lcd.fillScreen(WHITE);
  M5.Lcd.setTextColor(BLACK, WHITE);
  M5.Lcd.println("mic");
}
void showSignal() {
  // Offset
  int32_t offset_sum = 0;
  for (int n = 0; n < 160; n++) {
    offset_sum += (int16_t)adcBuffer[n];
  }
  int offset_val = -( offset_sum / 160 );
  // Auto Gain
  int max_val = 200;
  for (int n = 0; n < 160; n++) {
    int16_t val = (int16_t)adcBuffer[n] + offset_val;
    if ( max_val < abs(val) ) {
      max_val = abs(val);
    }
  }
  int y;
  for (int n = 0; n < 160; n++) {
    y = adcBuffer[n] + offset_val;
    y = map(y, -max_val, max_val, 10, 70);
    M5.Lcd.drawPixel(n, oldy[n], WHITE);
    M5.Lcd.drawPixel(n, y, BLACK);
    oldy[n] = y;
  }
}
void loop() {
  M5.update();
  if ( M5.BtnA.wasPressed() ) {
    // ボタン押した直後の雑音は録音しない
    delay(200);
    // REC
    i2sInit();
    M5.Lcd.setCursor(0, 0);
    M5.Lcd.println("mic rec...");
    Serial.println("REC ON");
  } else if ( M5.BtnA.wasReleased() ) {
    M5.Lcd.setCursor(0, 0);
    M5.Lcd.println("mic       ");
    recFlag = false;
    delay(100); // 録音終了まで待つ
    Serial.println("REC OFF");
    Serial.printf("size=%d, sec=%1.1f\n", savePos, (double)savePos / (SAMPLING_RATE * 2));
    Serial.printf("52494646");                        // RIFFヘッダ
    Serial.printf("%08lx", htonl(savePos + 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 * 2)); // バイト/秒
    Serial.printf("0200");                            // ブロック境界
    Serial.printf("1000");                            // ビット/サンプル
    Serial.printf("64617461");                        // dataチャンク
    Serial.printf("%08lx", htonl(savePos));           // 総データサイズ
    for (int n = 0; n <= savePos; n++) {
      Serial.printf("%02x", SAVE_BUFFER[n]);
    }
    Serial.printf("\n");
  }
  delay(10);
}

実験用のちょっと不安定なコードなので、ハングアップをするかもしれません。

出力結果

M5StickC initializing...OK
REC ON
bytesread = 1024, SAVE_LEN=102400, savePos=0
bytesread = 1024, SAVE_LEN=102400, savePos=1024
bytesread = 1024, SAVE_LEN=102400, savePos=2048
bytesread = 1024, SAVE_LEN=102400, savePos=3072
bytesread = 1024, SAVE_LEN=102400, savePos=4096
bytesread = 1024, SAVE_LEN=102400, savePos=5120
bytesread = 1024, SAVE_LEN=102400, savePos=6144
bytesread = 1024, SAVE_LEN=102400, savePos=7168
bytesread = 1024, SAVE_LEN=102400, savePos=8192
bytesread = 1024, SAVE_LEN=102400, savePos=9216
bytesread = 1024, SAVE_LEN=102400, savePos=10240
bytesread = 1024, SAVE_LEN=102400, savePos=11264
bytesread = 1024, SAVE_LEN=102400, savePos=12288
bytesread = 1024, SAVE_LEN=102400, savePos=13312
bytesread = 1024, SAVE_LEN=102400, savePos=14336
bytesread = 1024, SAVE_LEN=102400, savePos=15360
bytesread = 1024, SAVE_LEN=102400, savePos=16384
bytesread = 1024, SAVE_LEN=102400, savePos=17408
bytesread = 1024, SAVE_LEN=102400, savePos=18432
bytesread = 1024, SAVE_LEN=102400, savePos=19456
bytesread = 1024, SAVE_LEN=102400, savePos=20480
bytesread = 1024, SAVE_LEN=102400, savePos=21504
bytesread = 1024, SAVE_LEN=102400, savePos=22528
bytesread = 1024, SAVE_LEN=102400, savePos=23552
bytesread = 1024, SAVE_LEN=102400, savePos=24576
bytesread = 1024, SAVE_LEN=102400, savePos=25600
bytesread = 1024, SAVE_LEN=102400, savePos=26624
bytesread = 1024, SAVE_LEN=102400, savePos=27648
bytesread = 1024, SAVE_LEN=102400, savePos=28672
bytesread = 1024, SAVE_LEN=102400, savePos=29696
bytesread = 1024, SAVE_LEN=102400, savePos=30720
bytesread = 1024, SAVE_LEN=102400, savePos=31744
bytesread = 1024, SAVE_LEN=102400, savePos=32768
bytesread = 1024, SAVE_LEN=102400, savePos=33792
bytesread = 1024, SAVE_LEN=102400, savePos=34816
bytesread = 1024, SAVE_LEN=102400, savePos=35840
bytesread = 1024, SAVE_LEN=102400, savePos=36864
bytesread = 1024, SAVE_LEN=102400, savePos=37888
bytesread = 1024, SAVE_LEN=102400, savePos=38912
bytesread = 1024, SAVE_LEN=102400, savePos=39936
REC OFF
size=40960, sec=0.9
(省略)

最後の行が音声データで、この長い行をコピーして、バイナリファイルに保存するとWAVファイルになります。

検証では、上記のツールを利用させていただき、入力をHEXにして、出力をファイルダウンロードにして、拡張子を.wavなどに変換することで再生することが可能です。

まとめ

とりあえず、録音の基礎部分のみ検証できました。このままだと音が小さすぎるので4バイト分シフトしたほうが良さそうですが、音量はあとで操作できるのでもう少し便利に使えないか検証してみます。

オンメモリの制約で100Kまでしか録音できないのと、ボタンを押したときのノイズがまだ残っているので、そのへんを今後調整したいと思います。

続編

コメント

  1. unyonyonyo より:

    お世話になります。こちらのソースを見て勉強させていただいています。
    録音した信号がパルス信号のようなぷつぷつという音が一定間隔で出るようなものになってしまいます。これはwav時の変換の問題でしょうか?すみませんが、ご教授をお願い致します。

    • たなかまさゆき より:

      んー、それだけだとちょっとわからないですね
      ぷつぷつの間隔が一定かを確認して、周期を調べるともう少し調べやすいかもしれません
      あとは、環境からの音の可能性がありますので別の場所で確認するか、USB接続などをはずしてバッテリー動作などでも確認してみると問題の切り分けができると思います
      本体がおかしい可能性もありますしね。。。

      • unyonyonyo より:

        ご回答いただきありがとうございます。当方、業務の合間にやっているものでなかなか時間が取れないのですが、試してみたいと思います。ありがとうございます。

  2. L より:

    このコードを使い勉強させていただいています。
    現在音声を取得して音声データとして残したいと思ったところこのサイトのサンプルコードを見かけました。
    ですが、コードを実行したところ『Compilation error: ‘amp’ was not declared in this scope』このようなエラーが起き実行することができません。
    ですが、サイトからコピーさせていただいたはずなのでampはすでに定義されていると思いますがなぜこのようなエラーが出てしまうのでしょうか、お時間があればお答えいただけると大変助かります。

    • たなかまさゆき より:

      すみません、ブログのバージョンを変えたときに

      & “&” アンパサンド
      < “<" 小なり > ">” 大なり

      上記などの表記が化けてしまったようです。
      修正してみました。

  3. L より:

    お早い回答ありがとうございます。
    コード実行後またエラーが出てしまい困っています、こちら側のエラーであれば申し訳ないのですが
    section `.dram0.bss’ will not fit in region `dram0_0_seg’
    上記のエラーはどう言ったものなのでしょうか。
    メモリの領域エラーか何かなのでしょうか。
    よろしければご回答いただけると嬉しいです。

    • たなかまさゆき より:

      i2s_pin_config_t pin_config = {};

      手元だと違うエラーだったのですが、おそらく上記に修正すれば動くかな?
      ボードライブラリのバージョンがアップして、パラメーターが追加されています。
      元のコートですと追加されたパラメータが初期化されていないので、メモリ上のゴミデータがセットされます。
      上記は{}で0クリアしているのでこれで動くといいのですが、、、

  4. L より:

    21~43行目
    {
    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, // is fixed at 12bit, stereo, MSB
    .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 = READ_LEN,
    };
    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(mic_record_task, “mic_record_task”, 2048, NULL, 1, &taskHandle, 1);
    }

    上記の//変更場所のように書き換えをしました。この部分で大丈夫でしょうか?
    また、上記のように変更し実行してもやはり同じように
    ーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
    xtensa-esp32-elf/bin/ld: DRAM segment data does not fit.
    xtensa-esp32-elf/bin/ld: DRAM segment data does not fit.
    xtensa-esp32-elf/bin/ld: region `dram0_0_seg’ overflowed by 2232 bytes
    ーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
    のようなエラーが出てきてしまいます、ボードの更新なども最新になっていました。
    もしよろしければ確認のためボードとボードのバージョンも教えていただけると嬉しいです。

    何度も何度も質問してしまいすいません。ご丁寧な対応ありがとうございます。

    • たなかまさゆき より:

      すみません、コード更新の際に手元の別ファイルのコードをいれてしまっていました。

      (変更点1)
      #define SAVE_LEN (900*100)

      ボードライブラリのバージョンアップにより、自由に使えるメモリ量が減りました。
      そのため1024*100=102.4kのメモリ確保ができなくなってコンパイルエラーが出ていました
      とりあえず(900*100)など少し小さい値にすることで動くと思います。

      (変更点2)
      i2s_pin_config_t pin_config = {};//変更場所
      前回のですが、こちらは実行したときにメモリ上の数値によって動いたり、エラーになったりするので0でクリアしたほうが安全です

      • L より:

        無事実行することができました、これでまた少し学習を進めてみたいと思います。
        数回に渡りご丁寧にお答えいただきありがとうございました。
        また何かあれば質問してもよろしいでしょうか?

        • たなかまさゆき より:

          よかったです。お気軽の質問してください
          ちなみにTwitterとかの方が反応が早いと思いますが、ブロクのコメントでも構いません

  5. たなかまさゆき より:

    すみません、返信漏れておりました
    https://twitter.com/tnkmasayuki
    私はこのアカウントになります

    あとはFacebookとかも

    https://www.facebook.com/groups/927623023964478

    上記の「ESP8266/ESP32環境向上委員会」とかが活発です