TensorFlow micro_speech を ESP32 でチューニングする

(更新

TensorFlow Lite micro_speech finally got out of experimental.  Running it on ESP32 and tuning the code.

※ 修正および説明が不足していたので、12月4日に内容を加筆修正しています。[加筆] あるいは [修正] と書いてあるところがそうです。御迷惑をおかけしました。

ひさびさの機械学習(ディープラーニング)ネタです。近年、多くのお客様や、私の同業の技術者の間で、センサやマイクロフォンなどを使った IoT アプリケーションを設計する際に、大きな検索テーブルによるパターンマッチングや、閾値(スレッシュホールド)の調整だけでは、現場での利用において期待する精度を得られないという話を多く伺います。

深層ニューラルネットワーク技術はかなり一般的となっており、組込設計の分野でも、AI 的アプローチの研究が必要となってきているように感じます。

以前(去年の 7月頃)、TensorFlow Lite の micro_speech(マイコン用の音声認識・識別)に挑戦しましたが、当時はまだその機能(コード)が experimental(実験段階)ということで、いろいろ苦労しました。しかし最近、いろんな雑誌やブログで TensorFlow Lite for Microcontrollers や TinyML というキーワードを頻繁に見るようになり、さらに調べてみたところでは、micro_speech もとうとう正式リリースとなったようです。(ちなみに、microfrontend 等の機能は、まだ experimental のようです。)

私もこうしてはいられない!  と思い、評価を再開することにしました。

micro_speech が experimental から外れる

調べてみると、TenforFlow v2.2.0 から micro_speech が正式リリースに含まれるようになったようです。また、micro_speech が Espressif ESP-EYE  ボードでサポートされていることが分かったので、手元の ESP-EYE で動かしてみることにしました。

最初そのままビルドしたところ、「yes」「no」のキーワードをちゃんと認識しないように見えました。ESP-EYE 搭載のマイクが壊れているのではないかと思い、コード中にあちこち診断コードを入れて試してみたのですが、マイクが壊れている訳ではなく、micro_speech を実際のマイコンボードで動かすには、いろいろとチューニングや試行錯誤が必要なことが分かったので、報告させて頂こうと思います。おそらく同じように、うまく動かなくて困っていらっしゃる方は多いのではないか、と思います。

マイクのゲインや DC オフセットはあまり関係ない

TL; DR;

最初疑ったのは(上述のように)、マイクが壊れていたり、マイクのゲインが小さすぎたりするのではないか、という点です。いろいろ実験して、マイクからは 16ビット符号付き整数のサンプル値が読み取れることが分かったのですが、受信レベルがフルスケールに対して数十 dB(評価にも依るが、30 〜 50dB 程度)くらい低いように思われます。実際、マイクのすぐそばで大きな声を出すくらいでは、信号は全くといっていいほどクリッピング(飽和)しません。

こんなに信号レベルが低くて大丈夫なのか、と思ったのですが、結論から言うと、音声の認識にはそれほど大きな影響はなさそうです。ただし、認識漏れが多いなあ、と思ったら、10dB 程度のゲインをかけてやるのは効果がありそうです。この記事の最後のほうで、Knowles 社の MEMS マイク SPH0645 を動かす修正を示しますが、そのコードにはゲインをかける機能を実装してあります。

もう一点、私の手元の Knowles SPH0645 は、かなり強い DC オフセット(フルスケールの 2〜3%程度)があり、それが悪さをしていることも考えたのですが、micro_speech のフロントエンドには、周波数 125 Hz 以下のスペクトルを除去する機能があるようで、これも杞憂に終わりました。

まずはコードの準備から

最初に、評価環境を用意しましょう。前述の通り、今回は Espressif 社の ESP-EYE を使います。もし、ESP-EYE はないけど ESP32 のボードがあり、手元に I2S インターフェイスのマイクがあれば、それを利用できるかも知れません。この記事の最後のほうで、Knowles SPH0645(Adafruit 社の I2S MEMS Microphone Breakout – SPH0645LM4H: PRODUCT ID 3421)を使う方法を示します。

今回は、以下のバージョンを利用します。

  • ESP-IDF: v4.1
  • TensorFlow: v2.2.1

実はもっと新しい TensorFlow も試したのですが、サンプルコードがうまくビルドできないので諦めました。まだ発展途上ということでしょう。この記事も、おそらく来年中には時代遅れになると予想しています。(つまり、これは季節モノの記事ということになります。)

こちらのサイトを参考にして、

$ make -f tensorflow/lite/micro/tools/make/Makefile TARGET=esp generate_micro_speech_esp_project

まで実行します。そこまで終わったら、(そのままでも良いのですが、自分の修正に対するバージョン管理が容易となるよう)ディレクトリ tensorflow/lite/micro/tools/make/gen/esp_xtensa-esp32/prj/micro_speech を、例えば $HOME/esp/ の下などにコピーすると良いでしょう。

修正 1点目: デバッグログを有効にする

ビルドしてそのまま動いた人は幸せです。私の場合は「yes」「no」の出力がほとんどまったく表示されませんでした。私はいきなり、マイクの入力信号を疑ったのですが、前述のように、そうではないようです。

まずは問題解析をしやすくするため、ESP-IDF のデバッグログを有効にしましょう。そのためには、idf.py menuconfig すれば良いのですが、sdkconfig.defaults をいじるほうが簡単かも知れません。パッチを示します。

diff --git a/esp-idf/sdkconfig.defaults b/esp-idf/sdkconfig.defaults
index 4c3f6b7..970dc51 100644
--- a/esp-idf/sdkconfig.defaults
+++ b/esp-idf/sdkconfig.defaults
@@ -18,3 +18,4 @@ CONFIG_ESPTOOLPY_FLASHFREQ_80M=y
 CONFIG_INT_WDT=
 CONFIG_TASK_WDT=
 CONFIG_COMPILER_OPTIMIZATION_PERF=y
+CONFIG_LOG_DEFAULT_LEVEL_VERBOSE=y

修正 2点目: 軟判定結果の数値を表示してみる

プログラムでは、「yes」「no」を検出して表示するようになっていますが、TensorFlow の推論コードの後段に、閾値(スレッシュホールド)を使った硬判定のコードがあります。main/recognize_commands.cc の、ここです。

129    if ((current_top_score > detection_threshold_) &&
130        ((current_top_label != previous_top_label_) ||
131         (time_since_last_top > suppression_ms_))) {
132      previous_top_label_ = current_top_label;
133      previous_top_label_time_ = current_time_ms;
134      *is_new_command = true;

current_top_score はどうやって計算しているのでしょうか。これを目視できるようにするため、以下のようなパッチを当ててみましょう。[加筆] なお、関数 RecognizeCommands::RecognizeCommands() が呼ばれるタイミングが分かるよう、そのためのログ出力も追加しています。

[修正]
diff --git a/esp-idf/main/recognize_commands.cc b/esp-idf/main/recognize_commands.cc
index e09ad1b..2a030d3 100644
--- a/esp-idf/main/recognize_commands.cc
+++ b/esp-idf/main/recognize_commands.cc
@@ -17,6 +17,10 @@ limitations under the License.
 
 #include 
 
+#include "esp_log.h"
+
+static const char *TAG = "TF_LITE_RECOGNIZE_COMMANDS";
+
 RecognizeCommands::RecognizeCommands(tflite::ErrorReporter* error_reporter,
                                      int32_t average_window_duration_ms,
                                      uint8_t detection_threshold,
@@ -35,6 +39,7 @@ RecognizeCommands::RecognizeCommands(tflite::ErrorReporter* error_reporter,
 TfLiteStatus RecognizeCommands::ProcessLatestResults(
     const TfLiteTensor* latest_results, const int32_t current_time_ms,
     const char** found_command, uint8_t* score, bool* is_new_command) {
+  ESP_LOGD(TAG, "In ProcessLatestResults()");
   if ((latest_results->dims->size != 2) ||
       (latest_results->dims->data[0] != 1) ||
       (latest_results->dims->data[1] != kCategoryCount)) {
@@ -104,6 +109,7 @@ TfLiteStatus RecognizeCommands::ProcessLatestResults(
   }
   for (int i = 0; i < kCategoryCount; ++i) {
     average_scores[i] /= how_many_results;
+    ESP_LOGD(TAG, "average_scores[%d] = %d", i, average_scores[i]);
   }
 
   // Find the current highest scoring category.
[加筆] 実行してみると、

D (2437) TF_LITE_RECOGNIZE_COMMANDS: In ProcessLatestResults()
D (3437) TF_LITE_RECOGNIZE_COMMANDS: In ProcessLatestResults()
D (4427) TF_LITE_RECOGNIZE_COMMANDS: In ProcessLatestResults()

というログメッセージばかり表示され、average_scores[] が出力されませんね。なぜでしょう?  ログ出力の内容を追加してみます。先ほどのパッチを適用後、さらに以下のパッチを当ててください。

--- a/esp-idf/main/recognize_commands.cc
+++ b/esp-idf/main/recognize_commands.cc
***************
*** 85,90 ****
--- 85,97 ----
    const int64_t how_many_results = previous_results_.size();
    const int64_t earliest_time = previous_results_.front().time_;
    const int64_t samples_duration = current_time_ms - earliest_time;
+ 
+   ESP_LOGD(TAG, "how_many_results = %d", (int) how_many_results);
+   ESP_LOGD(TAG, "minimum_count_ = %d", (int) minimum_count_);
+   ESP_LOGD(TAG, "samples_duration = %d", (int) samples_duration);
+   ESP_LOGD(TAG, "average_window_duration_ms_ / 4 = %d",
+       (int) (average_window_duration_ms_ / 4));
+ 
    if ((how_many_results < minimum_count_) ||
        (samples_duration < (average_window_duration_ms_ / 4))) {
      *found_command = previous_top_label_;

これを実行してみると、次のようになります。

D (4427) TF_LITE_RECOGNIZE_COMMANDS: In ProcessLatestResults()
D (4427) TF_LITE_RECOGNIZE_COMMANDS: how_many_results = 2
D (4427) TF_LITE_RECOGNIZE_COMMANDS: minimum_count_ = 3
D (4427) TF_LITE_RECOGNIZE_COMMANDS: samples_duration = 1000
D (4437) TF_LITE_RECOGNIZE_COMMANDS: average_window_duration_ms_ / 4 = 250

なぜか、how_many_results が少なすぎて、硬判定処理がスキップされているようです。困りましたね。この根本原因は後で調べることにして、とりあえずコンストラクタ RecognizeCommands() を呼び出すときのパラメタを修正してみます。(引数の末尾 minimum_count を 2 に。)

diff --git a/esp-idf/main/main_functions.cc b/esp-idf/main/main_functions.cc
index f2ee8e7..50adc02 100644
--- a/esp-idf/main/main_functions.cc
+++ b/esp-idf/main/main_functions.cc
@@ -114,7 +114,7 @@ void setup() {
                                                  feature_buffer);
   feature_provider = &static_feature_provider;
 
-  static RecognizeCommands static_recognizer(error_reporter);
+  static RecognizeCommands static_recognizer(error_reporter, 1000, 200, 1500, 2);
   recognizer = &static_recognizer;
 
   previous_time = 0;

これでようやく、軟判定処理が動くようになりました。

D (4427) TF_LITE_RECOGNIZE_COMMANDS: In ProcessLatestResults()
D (4427) TF_LITE_RECOGNIZE_COMMANDS: how_many_results = 2
D (4427) TF_LITE_RECOGNIZE_COMMANDS: minimum_count_ = 2
D (4427) TF_LITE_RECOGNIZE_COMMANDS: samples_duration = 1000
D (4437) TF_LITE_RECOGNIZE_COMMANDS: average_window_duration_ms_ / 4 = 250
D (4447) TF_LITE_RECOGNIZE_COMMANDS: averate_scores[0] = 7
D (4447) TF_LITE_RECOGNIZE_COMMANDS: averate_scores[1] = 84
D (4457) TF_LITE_RECOGNIZE_COMMANDS: averate_scores[2] = 55
D (4457) TF_LITE_RECOGNIZE_COMMANDS: averate_scores[3] = 109

一回の推論毎に、average_scores[] が出力されています。この配列は(デフォルトでは)4要素からなっていて、ファイル main/micro_features/micro_model_settings.cc にあるように、

const char* kCategoryLabels[kCategoryCount] = {
    "silence",
    "unknown",
    "yes",
    "no",
};

となっています。つまり、average_scores[0] が silence 、1 が unknown、2 が yes、3 が no に対する「確からしさ」のスコアということになります。値は uint8_t なので、0〜255 になります。値が高いほど「確からしい」ということになります。

コードを動かしながら、この変数値を見ていくと、yes と喋ると average_scores[2] が大きくなるはずです。「どうも私の英語の発音が悪いのではなかろうか?」と思う場合は、Google Translate などで喋らせることもできますが、それほど発音には厳しくないようなので、日本人の英語でも大丈夫だと思います。

ここまで動いているようであれば、一つの解決法は閾値の調整です。以下のような感じで閾値を下げてあげると改善するかと思います。(ここでは、デフォルトの閾値 (コンストラクタの 3つめの引数 detection_threshold)200 を 150 に変更しています。[修正] 1000は無視してください。)

diff --git a/esp-idf/main/main_functions.cc b/esp-idf/main/main_functions.cc
index f2ee8e7..bd4e9b9 100644
--- a/esp-idf/main/main_functions.cc
+++ b/esp-idf/main/main_functions.cc
@@ -114,7 +114,7 @@ void setup() {
                                                  feature_buffer);
   feature_provider = &static_feature_provider;
 
-  static RecognizeCommands static_recognizer(error_reporter);
+  static RecognizeCommands static_recognizer(error_reporter, 1000, 150, 1500, 2);
   recognizer = &static_recognizer;
 
   previous_time = 0;

修正 3点目: 推定頻度を増やしてみる

ところで、上記のように変数を表示しながら実行すると、首をかしげる方があるかも知れません。どうして、推論が約 1秒に 1回しか実行されないのでしょうか??  こちらの説明を見ると、音声信号を 20ミリ秒ごとにスライドしながら推論を繰り返すような説明があります。。。1秒間に 50回は推論してくれても良さそうですね。

私は最初、ESP32 の性能が思ったほと高くなく、1秒間に 1回程度しか推論できないのだと思っていたのですが、コードに診断機能を入れながら調べていくと、そうではなくて、実際に時間を要しているのは main/feature_provider.ccTfLiteStatus FeatureProvider::PopulateFeatureData() の処理だということが分かりました。同メソッドのコード中のコメントを見ると、一度受信して計算した feature データは、20ミリ秒単位にスライドしながら再利用し、新しいデータだけを 20ミリ秒(正確にはオーバーラップして 30ミリ秒。ストライド幅が 20ミリ秒)ずつ受信して新たな推論を繰り返せるように見えます。おそらく、作者の意図もそうではないかと思います。

しかし動かしてみると、このメソッドを呼び出すたびに毎度、kFeatureSliceCount(つまり49)個の slice(あるいはフレーム)を繰り返し受信しています。これでは、49個の slice を受信しないと、次の推論に移れません。これが、このプログラムでは 1秒毎(正確には 49 × 20 = 980ミリ秒)にしか推論が動かない原因です。

この根本原因は、メソッド中で新規に受信する slice 数(slices_needed)を、引数 last_time_in_mstime_in_ms を使って計算しているというものです。これですと、一度メソッドを呼び出して 980ミリ秒の時間がかかると、次からの呼び出しでも、毎回、毎回、slices_needed が 49 になってしまうのです。これでは面白くないですね。

私は以下のような修正を施してみました。これは、実時間で slice 数を決めるのではなく、リングバッファ中に準備できている受信データバイト数を元に slice 数を決める、というやり方です。これで、メソッド呼び出し毎に「5つ強」程度の slice だけを受信し、その度に推論アルゴリズムを動かせるようになりました。おおよそ、8倍程度に推論頻度が上がるはずです。(実測値)

diff --git a/esp-idf/main/esp/audio_provider.cc b/esp-idf/main/esp/audio_provider.cc
index 32b377d..7c73603 100644
--- a/esp-idf/main/esp/audio_provider.cc
+++ b/esp-idf/main/esp/audio_provider.cc
@@ -183,3 +183,11 @@ TfLiteStatus GetAudioSamples(tflite::ErrorReporter *error_reporter,
 }
 
 int32_t LatestAudioTimestamp() { return g_latest_audio_timestamp; }
+
+int32_t AvailAudioSamplesMs(void) {
+  if (!g_is_audio_initialized)
+    return 0;
+  else
+    return (rb_filled(g_audio_capture_buffer) / sizeof(int16_t)) /
+      (kAudioSampleFrequency / 1000);
+}
diff --git a/esp-idf/main/feature_provider.cc b/esp-idf/main/feature_provider.cc
index a1e3a9b..35feec7 100644
--- a/esp-idf/main/feature_provider.cc
+++ b/esp-idf/main/feature_provider.cc
@@ -34,6 +34,8 @@ FeatureProvider::~FeatureProvider() {}
 TfLiteStatus FeatureProvider::PopulateFeatureData(
     tflite::ErrorReporter* error_reporter, int32_t last_time_in_ms,
     int32_t time_in_ms, int* how_many_new_slices) {
+  extern int32_t AvailAudioSamplesMs(void);
+
   if (feature_size_ != kFeatureElementCount) {
     TF_LITE_REPORT_ERROR(error_reporter,
                          "Requested feature_data_ size %d doesn't match %d",
@@ -43,10 +45,9 @@ TfLiteStatus FeatureProvider::PopulateFeatureData(
 
   // Quantize the time into steps as long as each window stride, so we can
   // figure out which audio data we need to fetch.
-  const int last_step = (last_time_in_ms / kFeatureSliceStrideMs);
   const int current_step = (time_in_ms / kFeatureSliceStrideMs);
 
-  int slices_needed = current_step - last_step;
+  int slices_needed = AvailAudioSamplesMs() / kFeatureSliceStrideMs;
   // If this is the first call, make sure we don't use any cached information.
   if (is_first_run_) {
     TfLiteStatus init_status = InitializeMicroFeatures(error_reporter);
[加筆] なお、この修正を施したあとは、先ほどのコンストラクタ RecognizeCommands() を呼び出す際の引数を(閾値を除いて)以下のように戻しても大丈夫です。

static RecognizeCommands static_recognizer(error_reporter, 1000, 150, 1500, 3);

変な挙動と小さな警告を 1つずつ修正

上記までの修正でほぼ期待通りに動くようになりました。TensorFlow の設計は素晴らしいものだと思うのですが、マイクロプロセッサ用のデモコードの設計は、まだ若干荒削りな印象です。

さて。最後に 2つだけ小さな修正パッチを示しておきます。

コードを動かしていると、(ログ出力レベルの閾値にも依りますが)次のような警告が気になる方もあるでしょう。

D (2327) TF_LITE_AUDIO_PROVIDER: RB FILLED RIGHT NOW IS 0
D (2327) TF_LITE_AUDIO_PROVIDER:  Partial Read of Data by Model
V (2337) TF_LITE_AUDIO_PROVIDER:  Could only read 0 bytes when required 640 bytes

main/esp/ringbuf.c の設計があまり良くないように思われます。もし私が製品化をする際には大幅に変更することになると思いますが、来年辺りまでには GitHub 上のコードも書き直されてしまうかも知れませんので、簡単な修正(quick hack)にとどめます。

diff --git a/esp-idf/main/esp/audio_provider.cc b/esp-idf/main/esp/audio_provider.cc
index 32b377d..f752ab8 100644
--- a/esp-idf/main/esp/audio_provider.cc
+++ b/esp-idf/main/esp/audio_provider.cc
@@ -161,7 +161,7 @@ TfLiteStatus GetAudioSamples(tflite::ErrorReporter *error_reporter,
   int32_t bytes_read =
       rb_read(g_audio_capture_buffer,
               ((uint8_t *)(g_audio_output_buffer + history_samples_to_keep)),
-              new_samples_to_get * sizeof(int16_t), 10);
+              new_samples_to_get * sizeof(int16_t), 50);
   if (bytes_read < 0) {
     ESP_LOGE(TAG, " Model Could not read data from Ring Buffer");
   } else if (bytes_read < new_samples_to_get * sizeof(int16_t)) {
@@ -183,3 +183,11 @@ TfLiteStatus GetAudioSamples(tflite::ErrorReporter *error_reporter,
 }
 
 int32_t LatestAudioTimestamp() { return g_latest_audio_timestamp; }

もう一点。コンパイル時の警告が気になるので修正します。

diff --git a/esp-idf/main/esp/main.cc b/esp-idf/main/esp/main.cc
index ac47d68..66f44f6 100644
--- a/esp-idf/main/esp/main.cc
+++ b/esp-idf/main/esp/main.cc
@@ -23,7 +23,7 @@ limitations under the License.
 #include "freertos/task.h"
 #include "../main_functions.h"
 
-int tf_main(int argc, char* argv[]) {
+void tf_main(void) {
   setup();
   while (true) {
     loop();

Knowles SPH0645 で動かす修正

さて。上のほうで書きましたように、もし ESP-EYE ではなく、Adafruit 社の I2S MEMS Microphone Breakout – SPH0645LM4H(PRODUCT ID: 3421)などで動かしたい方もあるかも知れません。この MEMS マイクロフォンは、ESP-EYE のものとは異なり、量子化ビット数が 18ビット、I2S フォーマットが 32ビットとなっています。このマイクを ESP32 で動かすにはちょっと工夫が必要で、以下が参考になります。

パッチを示します。ポイントは、

  • I2S インターフェイスの設定変更
  • Knowles SPH0645 の I2S 信号タイミングに対するワークアラウンド
  • 32ビットの値を 16ビットに変換しながらリングバッファに書き込む

という点でしょうか。受信標本値に対してオフセットやゲインで修正するオマケ機能もありますので、御活用(?)ください。

補足: [加筆] Knowles SPH0645 では、標本化周波数 32kHz 〜 64kHz しかサポートされていません。16kHz 標本化は推奨外のようですので、御注意ください。 最新版のデータシートに依りますと、標本化周波数 16kHz 〜 64kHz がサポートされているようです。(2020/12/4 現在)

diff --git a/esp-idf/main/esp/audio_provider.cc b/esp-idf/main/esp/audio_provider.cc
index 32b377d..77ba81b 100644
--- a/esp-idf/main/esp/audio_provider.cc
+++ b/esp-idf/main/esp/audio_provider.cc
@@ -25,6 +25,7 @@ limitations under the License.
 // clang-format on
 
 #include "driver/i2s.h"
+#include "soc/i2s_reg.h"
 #include "esp_log.h"
 #include "esp_spi_flash.h"
 #include "esp_system.h"
@@ -57,14 +58,14 @@ int16_t g_history_buffer[history_samples_to_keep];
 }  // namespace
 
 const int32_t kAudioCaptureBufferSize = 80000;
-const int32_t i2s_bytes_to_read = 3200;
+const int32_t i2s_bytes_to_read = 3200 * (32 / 16);
 
 static void i2s_init(void) {
   // Start listening for audio: MONO @ 16KHz
   i2s_config_t i2s_config = {
       .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_TX),
       .sample_rate = 16000,
-      .bits_per_sample = (i2s_bits_per_sample_t)16,
+      .bits_per_sample = (i2s_bits_per_sample_t)32,
       .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
       .communication_format = I2S_COMM_FORMAT_I2S,
       .intr_alloc_flags = 0,
@@ -85,6 +86,19 @@ static void i2s_init(void) {
   if (ret != ESP_OK) {
     ESP_LOGE(TAG, "Error in i2s_driver_install");
   }
+
+  /*
+   * Alterations for SPH0645 to ensure we receive MSB correctly.
+   * Reference:
+   *   - Manuel's comment in
+   *     https://hackaday.io/project/162059-street-sense/log/160705-new-i2s-microphone
+   *   - https://gist.github.com/GrahamM/1d5ded26b23f808a80520e8c1510713a
+   */
+  // I2S_RX_SD_IN_DELAY
+  REG_SET_BIT(I2S_TIMING_REG(1), BIT(9));
+  // Phillips I2S - WS changes a cycle earlier
+  REG_SET_BIT(I2S_TIMING_REG(1), I2S_RX_MSB_SHIFT);
+
   ret = i2s_set_pin((i2s_port_t)1, &pin_config);
   if (ret != ESP_OK) {
     ESP_LOGE(TAG, "Error in i2s_set_pin");
@@ -111,15 +125,39 @@ static void CaptureSamples(void *arg) {
         ESP_LOGW(TAG, "Partial I2S read");
       }
       /* write bytes read by i2s into ring buffer */
-      int bytes_written = rb_write(g_audio_capture_buffer,
-                                   (uint8_t *)i2s_read_buffer, bytes_read, 10);
+      const int32_t OFFSET = 0;
+      const int32_t GAIN = 1;
+
+      int bytes_written = 0;
+      for (int ct = 0; ct < bytes_read / sizeof(int32_t); ct ++) {
+        // SPH0645 emits 32-bit/ch value but the LS 14 bits are always 0
+        int32_t sample32 =
+          (int32_t) i2s_read_buffer[ct * 4 + 3] << 24 |
+          (int32_t) i2s_read_buffer[ct * 4 + 2] << 16 |
+          (int32_t) i2s_read_buffer[ct * 4 + 1] << 8;
+
+        // Truncate to 16 bits
+        sample32 >>= 16;
+
+        // Value correction if required
+        sample32 -= OFFSET;
+        sample32 *= GAIN;
+
+        // Test clipping
+        if (sample32 > INT16_MAX || sample32 < INT16_MIN)
+          ESP_LOGW(TAG, "Signal clipping");
+
+        int16_t sample16 = (int16_t) sample32;
+        bytes_written += rb_write(
+            g_audio_capture_buffer, (uint8_t*) &sample16, 2, 10);
+      }
       /* update the timestamp (in ms) to let the model know that new data has
        * arrived */
       g_latest_audio_timestamp +=
           ((1000 * (bytes_written / 2)) / kAudioSampleFrequency);
       if (bytes_written <= 0) {
         ESP_LOGE(TAG, "Could Not Write in Ring Buffer: %d ", bytes_written);
-      } else if (bytes_written < bytes_read) {
+      } else if (bytes_written < bytes_read / (32 / 16)) {
         ESP_LOGW(TAG, "Partial Write");
       }
     }

お問い合わせはお気軽に!

今日はここまでにしたいと思います。次回は、異なる音声データ(日本語など)を用意して学習させる等、もう少し工夫してみたいと思います。

お問い合わせを頂いた後、継続して営業活動をしたり、ニュースレター等をお送りしたりすることはございません。
御返答は 24時間以内(営業時間中)とさせて頂いております。もし返答が届かない場合、何らかの事情でメールが不達となっている可能性がございます。大変お手数ですが、別のメールアドレス等で督促頂けますと幸いです。