マイク比較 M5StickC、M5Stack Fire、ESP-EYE その2 録音の準備 ESP-EYE

概要

前回はなんとなくの波形だけ表示してみました。今回は実際に録音をして波形を確認してみたいと思いますが、まずはどうすれば比較できるかを準備します。

※実験途中でかなり後半が怪しく、いま動かすとちゃんと動きません。。。以下の記事に確認しなおした情報をまとめていますので、この記事に書いてあることは実験方法以外参考にしないでください

実験方法

前回なんとなく波形がでたパラメーターをベースに音声データを保存して、シリアル出力に16進数で吐き出す。

その後にHEX変換ツールを使って、バイナリデータに変換後に音声編集アプリで波形を確認してみました。

利用アプリ

無料で使えるオープンソースの音声編集ソフトを使いました。

スケッチ

#include <driver/i2s.h>

#define I2S_NUM             I2S_NUM_0           // 0 or 1

#define I2S_SAMPLE_RATE     16000
#define I2S_SAMPLE_SIZE     512
#define I2S_BUFFER_SIZE     (10*1024)

#define I2S_PIN_CLK         26
#define I2S_PIN_WS          32
#define I2S_PIN_DOUT        I2S_PIN_NO_CHANGE
#define I2S_PIN_DIN         33

byte i2s_samples[I2S_BUFFER_SIZE];
int i2s_samplesCount;

void i2sMicInit() {
  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_16BIT,
    .channel_format       = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_I2S,
    .intr_alloc_flags     = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count        = 2,
    .dma_buf_len          = 512,
    .use_apll             = false,
    .tx_desc_auto_clear   = false,
    .fixed_mclk           = 0
  };
  i2s_pin_config_t pin_config = {
    .bck_io_num           = I2S_PIN_CLK,
    .ws_io_num            = I2S_PIN_WS,
    .data_out_num         = I2S_PIN_DOUT,
    .data_in_num          = I2S_PIN_DIN,
  };

  i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
  i2s_set_pin(I2S_NUM, &pin_config);
  //i2s_set_clk(I2S_NUM, I2S_SAMPLE_RATE, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);

  i2s_samplesCount = 0;
}

void setup() {
  Serial.begin(115200);
  delay(100);

  i2sMicInit();
}

void loop() {
  size_t bytes_read;
  i2s_read(I2S_NUM, (void *)&i2s_samples[i2s_samplesCount], 512, &bytes_read, portMAX_DELAY);
  i2s_samplesCount += bytes_read;

  if (I2S_BUFFER_SIZE <= i2s_samplesCount) {
    for (int i = 0; i < I2S_BUFFER_SIZE; i++) {
      Serial.printf("%02X", i2s_samples[i]);
    }
    Serial.println();
    i2s_samplesCount = 0;
  }
}

操作手順

Arduino側

後ほどコードを出しますが、録音したデータをSerial.printf(“%02X”,i2s_samples[i])で表示します。

テスト音声再生

Audacityのジェネレーターからトーンを選択して、440Hzの音声データを作成して、この音を再生しながら録音してみます。

シリアルモニタからコピー

Arduinoから吐き出した16進数の文字列をコピーしてきます。テストの音声を再生させながら、1行分コピーするとちょうどいいです。

バイナリファイルへ変換

上記のWebツールを使わせていただきました。

左上に16進数文字列をペーストして、入力形式をHEX(16進数)にセットして、出力形式をファイルダウンロードにしてから変換実行をすると、ファイルがダウンロードされます。

Audacityでインポート

メニューから「取り込み」→「ロー(Raw)データの取り込み」を選択します。

取り込み設定は16ビットのリトルエンディアンで、モノラルにして16000Hzを選択します。

※これ以降はかなり書いてあることがおかしいです。。。

無音に見えます。。。念の為再生してみまと、ちゃんと440Hzぐらいの音がします。録音はされていますが、音が小さいのですね。とりあえず入力側は変更せずにAudacityで拡大してみます。

音声増幅

Audacityで音声を全選択してから、エフェクトの増幅を選びます。

デフォルトで劣化しない最大音量に増幅する設定になっているのでそのままOKで増幅させます。

波形が見えました。思ったより波形が波打っていますね、、、

Arduinoにクロック設定を追加

ちょっと気になっていた設定がありまして、クロック設定を追加してみました。

  i2s_set_clk(I2S_NUM, I2S_SAMPLE_RATE, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);

この状態で録音して、取り込んでみます。

あれ、増幅しなくても波形が見える大きさです。そして波形ががびがびしていますね。ここまで変わるのがちょっと意外です。。。

APIリファレンスを読む

上記のI2Sのリファレンスを読み直します。

#include "driver/i2s.h"
#include "freertos/queue.h"

static const int i2s_num = 0; // i2s port number

static const i2s_config_t i2s_config = {
    .mode = I2S_MODE_MASTER | I2S_MODE_TX,
    .sample_rate = 44100,
    .bits_per_sample = 16,
    .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
    .communication_format = I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB,
    .intr_alloc_flags = 0, // default interrupt priority
    .dma_buf_count = 8,
    .dma_buf_len = 64,
    .use_apll = false
};

static const i2s_pin_config_t pin_config = {
    .bck_io_num = 26,
    .ws_io_num = 25,
    .data_out_num = 22,
    .data_in_num = I2S_PIN_NO_CHANGE
};

...

    i2s_driver_install(i2s_num, &i2s_config, 0, NULL);   //install and start i2s driver
    i2s_set_pin(i2s_num, &pin_config);
    i2s_set_sample_rates(i2s_num, 22050); //set sample rates
    i2s_driver_uninstall(i2s_num); //stop & destroy i2s driver

送信用ですが、設定例があります。i2s_configはサンプリングレートが44100ですが、i2s_set_sample_rates()は22050です。なぜ、、、

スケッチ例を探す

void i2sInit()
{
   i2s_config_t i2s_config = {
    .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN),
    .sample_rate =  I2S_SAMPLE_RATE,              // The format of the signal using ADC_BUILT_IN
    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, // is fixed at 12bit, stereo, MSB
    .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
    .communication_format = I2S_COMM_FORMAT_I2S_MSB,
    .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
    .dma_buf_count = 4,
    .dma_buf_len = 8,
    .use_apll = false,
    .tx_desc_auto_clear = false,
    .fixed_mclk = 0
   };
   i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
   i2s_set_adc_mode(ADC_UNIT_1, ADC_INPUT);
   i2s_adc_enable(I2S_NUM_0);
}

i2s_driver_install()で検索するとHiFreq_ADC.inoのスケッチ例がありました。こちらではi2s_set_sample_rates()は呼び出していませんね、、、

ESP-IDFでも探す

  • \bluetooth\a2dp_gatts_coex\main\main.c
  • \bluetooth\a2dp_sink\main\main.c
  • \peripherals\i2s\main\i2s_example_main.c
  • \peripherals\i2s_adc_dac\main\app_main.c

上記4つサンプルがありましたが、全部i2s_set_sample_rates()系は呼び出していませんでした。そして送信ばかりで受信のサンプルが少ないですね。

バッファサイズと数も調査する

dma_buf_countとdma_buf_lenにいくつを設定すればいいのかわからないので、サンプルの値を確認してみます。

出典サンプルdma buf countdma buf lencount * len
Arduino\I2S\HiFreq_ADC\HiFreq_ADC.ino4832
ESP-IDF\bluetooth\a2dp_gatts_coex\main\main.c660360
ESP-IDF\bluetooth\a2dp_sink\main\main.c660360
ESP-IDF\peripherals\i2s\main\i2s_example_main.c660360
ESP-IDF\peripherals\i2s_adc_dac\main\app_main.c210242048
API リファレンス/esp32/api-reference/peripherals/i2s.html864512

んー、一貫性がありません。ESP-IDFのlenが60ってのが8の倍数じゃないんですよね。Arduinoだと動かなかったような?

フォーラムなどでもたまに質問がありましたが、明確な答えがでていませんでした。送信などでは一度に送る量などによって細かいDMAをたくさんの方が良さそうですが、受信だとリアルタイム性を求めなければバッファが溢れない量あればよさそうな感じです。

GitHubを探す

espressifのリポジトリでi2s_driver_installを含むコードを見ていきます。

送信

非常にシンプルな送信例です。データを作成して送信前にi2s_set_clk()を呼び出しています。

受信

シンプルな受信例を発見しました。DMAバッファは300を3個と中途半端な数ですね。。。そして、intr_alloc_flagsにESP_INTR_FLAG_LEVEL2を使っています。割り込みレベルを少し上げているのかな?

初期化はi2s_driver_install()のあとにi2s_set_pin()とi2s_zero_dma_buffer(1)とシンプルです。受信時にはi2s_zero_dma_buffer()で受信バッファをすべてクリアしないと確かに雑音が入っている気がします。。。

クロック系の関数はありませんでした。

ADC受信

ADCのみはESP-IDFではなくArduinoのスケッチ例しかありませんでした。i2s_set_pin()のかわりにADCの設定が追加されていますね。こちらも本当はi2s_zero_dma_buffer()を呼び出したほうがよさそうなサンプルですね。

こちらもクロック系の関数はなかったです。

DAC送信

ADC受信と使い方は似ていますね。送信だけれどクロック系関数は呼び出していません。

ADC受信&DAC送信

いろいろやっていますが、特に上のサンプルと変わっていませんでした。

Bluetooth送信

両方A2DPでのオーディオ送信のサンプルかな?

テクニカルマニュアルを読む

迷子になったので、テクニカルマニュアルを読みます。しかし、直接的な記述なし、、、

受信サンプルの設定にしてみる

    //.use_apll             = false,
    //.tx_desc_auto_clear   = false,
    //.fixed_mclk           = 0

上記の謎設定をコメントアウトしてみました。

ちなみにdma_buf_countとdma_buf_len、intr_alloc_flagsをESP_INTR_FLAG_LEVEL2にしても波形は変わりませんでした、、、

サンプリングレートを疑う

16000Hzサンプリングレートで440Hzのトーンを作成したのが一番上の波形です。真ん中が謎のパラメーターを消したもので、きれいな波形に見えていましたが、サンプリングレートがおかしいですね。

下が最初に取得した波形です。こっちは波形は汚いですがサンプリングレートはただしそうです。

そして、マイクで録音した波形は2ポイント同じデータが続いていますね。実質的なサンプリングレートが半分に見えます。

謎のパラメータを調べる

use_apll

高精度クロックを使うかのフラグですね。詳細不明。

tx_desc_auto_clear

TXなので送信用パラメーターです。送信用バッファをクリアするかどうかの設定なので受信には関係なさそうですね。どうやら送信するものがなくなった場合にデフォルトだと前回のデータを送信しちゃうみたいなので、この設定ができたみたいです。

fixed_mclk

MCLKの指定をするかのフラグですね。

まとめ

よくよく考えると、構造体の初期化をしないと変な値がはいっていますので絶対に指定しないとだめですね、、、

再度検証

#include <driver/i2s.h>

#define I2S_NUM             I2S_NUM_0           // 0 or 1

#define I2S_SAMPLE_RATE     16000
#define I2S_SAMPLE_SIZE     512
#define I2S_BUFFER_SIZE     (5*1024)

#define I2S_PIN_CLK         26
#define I2S_PIN_WS          32
#define I2S_PIN_DOUT        I2S_PIN_NO_CHANGE
#define I2S_PIN_DIN         33

byte i2s_samples[I2S_BUFFER_SIZE];
int i2s_samplesCount;

void i2sMicInit() {
  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_16BIT,
    .channel_format       = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = I2S_COMM_FORMAT_I2S,
    .intr_alloc_flags     = ESP_INTR_FLAG_LEVEL2,
    .dma_buf_count        = 2,
    .dma_buf_len          = 512,
    .use_apll             = false,
    .tx_desc_auto_clear   = false,
    .fixed_mclk           = 0
  };
  i2s_pin_config_t pin_config = {
    .bck_io_num           = I2S_PIN_CLK,
    .ws_io_num            = I2S_PIN_WS,
    .data_out_num         = I2S_PIN_DOUT,
    .data_in_num          = I2S_PIN_DIN,
  };

  i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
  i2s_set_pin(I2S_NUM, &pin_config);
  //  i2s_set_clk(I2S_NUM, I2S_SAMPLE_RATE, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);
  i2s_zero_dma_buffer(I2S_NUM);

  i2s_samplesCount = 0;
}

void setup() {
  Serial.begin(115200);
  delay(100);

  i2sMicInit();
}

void loop() {
  size_t bytes_read;
  i2s_read(I2S_NUM, (void *)&i2s_samples[i2s_samplesCount], 512, &bytes_read, portMAX_DELAY);
  i2s_samplesCount += bytes_read;

  if (I2S_BUFFER_SIZE <= i2s_samplesCount) {
    for (int i = 0; i < I2S_BUFFER_SIZE; i++) {
      Serial.printf("%02X", i2s_samples[i]);
    }
    Serial.println();
    i2s_samplesCount = 0;
  }
}

上記をベースに検証し直します、、、

バッファが長いと取得が面倒なので、すこし短い時間に変更しました。

パターン1 ベースのまま

上が録音データで、下が再生データです。位相はそろえてあります。サンプリングレートはあっていますが、上の波形がずれていますね。2ポイント単位でデータを取得しています。

パターン2 クロック指定

  i2s_set_clk(I2S_NUM, I2S_SAMPLE_RATE, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);

上記でクロック指定を追加。

周波数はあっていますが、データがバラけています。

パターン3 周波数を倍にする

    .sample_rate          = I2S_SAMPLE_RATE*2,

変になるのわかっていますが検証。

32000Hzで取り込み。周波数が倍になっていますが、山がきれいですね。。。ただよく考えるとポイントが増えるはずで、周波数は変わらないはずです。

16000Hzで取り込んで見ました、、、あれ?

きれいじゃない?

2ポイント単位で処理されていますがちゃんと山ができています。

パターン4 周波数を倍+クロック指定

    .sample_rate          = I2S_SAMPLE_RATE*2,
  i2s_set_clk(I2S_NUM, I2S_SAMPLE_RATE, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);

これはだめと。

パターン5 周波数を倍+クロック倍指定

    .sample_rate          = I2S_SAMPLE_RATE*2,
  i2s_set_clk(I2S_NUM, I2S_SAMPLE_RATE*2, I2S_BITS_PER_SAMPLE_16BIT, I2S_CHANNEL_MONO);

アナログの測定誤差っぽくみえますが、交互にずれているのでおそらく位相の違う波形がずれている感じですね、、、

パターン6 周波数を倍+use_apll

    .sample_rate          = I2S_SAMPLE_RATE*2,
    .use_apll             = true,

パターン3とあまりかわらないですが、若干山がなまっているきがします。

パターン7 周波数を倍+use_apll+fixed_mclkを倍

    .sample_rate          = I2S_SAMPLE_RATE*2,
    .use_apll             = true,
    .fixed_mclk           = I2S_SAMPLE_RATE*2

違いがわからず。

パターン8 周波数を倍+use_apll+fixed_mclkを4倍

    .sample_rate          = I2S_SAMPLE_RATE*2,
    .use_apll             = true,
    .fixed_mclk           = I2S_SAMPLE_RATE*2

無茶な設定をいれてみる。

波形が崩れたので、このパラメーターは影響をあたえているのがわかりました。

パターン9 周波数を倍+use_apll+fixed_mclk

    .sample_rate          = I2S_SAMPLE_RATE*2,
    .use_apll             = true,
    .fixed_mclk           = I2S_SAMPLE_RATE

んー、とくにかわらない。

考える

パターン3が一番キレイかな?

ここまでESP-EYEに搭載しているI2Sマイクのデータシートをみていないのが敗因なきがします。。。

データシートを探す

何故かESP-EYEの商品ページには搭載しているマイクの情報がありません。

上記に回路図があるので展開して確認します。

おそらくこのマイクです。検索するとM5StickVに採用されたマイクですね(微妙な表現)。

えーっとデータシートを読むんですがタイミング系のことばかりで、よくわからないですね、、、

あっ、12ビット精度のマイクだと思っていたら24ビット精度(32ビット保存)だった、、、なので1サンプリング2バイトじゃなくて、4バイトみたいです。ということでデータが変だったのは受信単位がおかしかったからでした。

再々検証

パターン10 周波数を倍+32ビット受信

    .sample_rate          = I2S_SAMPLE_RATE*2,
    .bits_per_sample      = I2S_BITS_PER_SAMPLE_32BIT,

おー、ちゃんと1ポイント単位でデータが取れています。周波数倍のままだったので16000に戻してみます。

パターン11 32ビット受信

    .bits_per_sample      = I2S_BITS_PER_SAMPLE_32BIT,

あれ、周波数は倍で良かったみたいです。

パターン12 32ビット受信+MSB

データシートにMSBとあったので指定してみました。

    .bits_per_sample      = I2S_BITS_PER_SAMPLE_32BIT,
    .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB),

おー、これこれまでで一番きれいですね。32ビット精度だと使いにくいので16ビット精度に落としたいと思います。

パターン13 32ビット受信+MSB+16ビット保存

#include <driver/i2s.h>

#define I2S_NUM             I2S_NUM_0           // 0 or 1

#define I2S_SAMPLE_RATE     16000
#define I2S_SAMPLE_SIZE     512
#define I2S_BUFFER_SIZE     (5*1024)

#define I2S_PIN_CLK         26
#define I2S_PIN_WS          32
#define I2S_PIN_DOUT        I2S_PIN_NO_CHANGE
#define I2S_PIN_DIN         33

byte i2s_samples[I2S_BUFFER_SIZE];
int i2s_samplesCount;

void i2sMicInit() {
  i2s_config_t i2s_config = {
    .mode                 = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
    .sample_rate          = I2S_SAMPLE_RATE * 2,
    .bits_per_sample      = I2S_BITS_PER_SAMPLE_32BIT,
    .channel_format       = I2S_CHANNEL_FMT_ONLY_LEFT,
    .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB),
    .intr_alloc_flags     = ESP_INTR_FLAG_LEVEL2,
    .dma_buf_count        = 2,
    .dma_buf_len          = 512,
    .use_apll             = false,
    .tx_desc_auto_clear   = false,
    .fixed_mclk           = 0
  };
  i2s_pin_config_t pin_config = {
    .bck_io_num           = I2S_PIN_CLK,
    .ws_io_num            = I2S_PIN_WS,
    .data_out_num         = I2S_PIN_DOUT,
    .data_in_num          = I2S_PIN_DIN,
  };

  i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
  i2s_set_pin(I2S_NUM, &pin_config);
  i2s_zero_dma_buffer(I2S_NUM);

  i2s_samplesCount = 0;
}

void setup() {
  Serial.begin(115200);
  delay(100);

  i2sMicInit();
}

void loop() {
  size_t bytes_read;
  int32_t readBuff[512 / 4];
  i2s_read(I2S_NUM, (void *)readBuff, 512, &bytes_read, portMAX_DELAY);

  for (int i = 0; i < bytes_read / 4; i++) {
    int16_t *p = (int16_t*)&i2s_samples[i2s_samplesCount];
    *p = (readBuff[i] / 256);
    i2s_samplesCount += 2;
  }

  if (I2S_BUFFER_SIZE <= i2s_samplesCount) {
    for (int i = 0; i < I2S_BUFFER_SIZE; i++) {
      Serial.printf("%02X", i2s_samples[i]);
    }
    Serial.println();
    i2s_samplesCount = 0;
  }
}

32ビットデータがありますが、実際には24ビット精度なので、さらに8ビット分をすてて保存しています。

なんと、増幅をしなくてもグラフが見えるようになりました!

まとめ

まだノイジーな音ですが、スタート時点と比べるとかなりの改善ができました。ちゃんとデータシートを読みましょう、、、

逆にここまで調査しないと、それっぽく聞こえるけれどちゃんと録音できていないってことになります。また、今回はノートパソコンのスピーカーから鳴らしているので、その影響でノイジーになっている可能性があります。

あとインターネットのESP32の情報はかなり昔のバージョンに関するものが多いですので気をつけましょう!

そしてこのブログもかなり嘘が書いてあることがあるので、気をつけて使ってください。

8件のコメント

  1. パターン13のソースに、MicroSDへwaveファイルとして出力する機能を追加しようとしたのですが
    67行あたりに、
    file.write(i2s_samples, I2S_BUFFER_SIZE);
    を追加したら、ノイズしか保存されませんでした。
    waveファイル出力するには、何か工夫が必要なのでしょうか?
    宜しくお願いいたします。

  2. ご回答ありがとうございます。

    こちらの環境は、ESP-WROOM-32にSDカードモジュールを接続してSPI通信で出力
    しようとしています。
    ソースの流れはこんな感じです。
    File file;
    file = SD.open(“/sound.wav”, FILE_WRITE);
    file.write(waveのヘッダー情報, ヘッダー情報のサイズ);
    file.write(i2s_samples, I2S_BUFFER_SIZE);

    1. 標準環境ではマイクが搭載されていないので、何らかのマイクを接続する必要があります。
      外付けマイクも複数購入したのですが、検証ができていません、、、

  3. すみません、肝心なものを書き忘れておりました。

    マイクはADMP441でI2S接続しています。
    なお、パターン13の
    Serial.printf(“%02X”, i2s_samples[i]);
    で取得した文字列をデータ変換ツール hogehoge.tkで出力し
    Audacityでインポートしたら正常に聞こえました。
    音声受信は成功しているようですので、これをファイル出力できれば助かります。

    1. そこまでできているのであれば、あとは保存だけなのですね!

      waveのヘッダー情報はまずは保存せずに、RAWフォーマットで保存してからバイナリエディタで確認をしてみるのをおすすめします。
      Serial.printf(“%02X”, i2s_samples[i]);
      との差分がなければwaveのヘッダーがおかしいのがわかります。その場合にはAudacityでエクスポートしてみて、ヘッダーを確認してみるのがかんたんだと思います

  4. 上記のやり方で検証したら、ヘッダーに問題がある事が分かりました。
    そして、Audacityでエクスポートしたwaveファイルのヘッダー部分をコピーしたら
    正常に再生できました。

    ありがとうございました。

コメントする

メールアドレスが公開されることはありません。

管理者承認後にページに追加されます。公開されたくない相談はその旨本文に記載するかTwitterなどでDM投げてください。またスパム対策として、日本語が含まれない投稿は無視されますのでご注意ください。