XIAO ESP32S3 Sense キーワードスポッティング
この文書は AI によって翻訳されています。内容に不正確な点や改善すべき点がございましたら、文書下部のコメント欄または以下の Issue ページにてご報告ください。
https://github.com/Seeed-Studio/wiki-documents/issues
XIAO ESP32S3 Sense & Edge Impulse キーワードスポッティング

このチュートリアルでは、TinyML を使用して XIAO ESP32S3 Sense マイクロコントローラーボード上でキーワードスポッティング (KWS) システムを実装する方法を説明します。データ収集とモデルトレーニングには Edge Impulse を使用します。KWS は音声認識システムにとって重要であり、TinyML の力を借りれば、小型で低消費電力のデバイスでも実現可能です。Edge Impulse と XIAO ESP32S3 Sense を使用して、独自の KWS システムを構築しましょう!
はじめに
このプロジェクトを開始する前に、以下の準備手順に従って、このプロジェクトに必要なソフトウェアとハードウェアを準備してください。
ハードウェア
このプロジェクトを成功させるために、以下のハードウェアを準備する必要があります。
- XIAO ESP32S3 Sense
- microSD カード(32GB 以下)
- microSD カードリーダー
- USB-C データケーブル
XIAO ESP32S3 Sense に拡張ボードを取り付けることで、拡張ボード上のマイクを使用できます。
拡張ボードの取り付けは非常に簡単です。拡張ボードのコネクタを XIAO ESP32S3 の B2B コネクタに合わせて押し込み、「カチッ」という音が聞こえたら取り付け完了です。

XIAO ESP32S3 Sense は最大 32GB の microSD カードをサポートしています。そのため、XIAO 用に microSD カードを購入する場合は、この仕様を参照してください。また、microSD カードを使用する前に FAT32 フォーマットにフォーマットしてください。

フォーマット後、microSD カードを microSD カードスロットに挿入できます。挿入方向に注意してください。金色の端子が内側を向くようにしてください。

ソフトウェア
XIAO ESP32S3 Sense を初めて使用する場合は、開始する前に以下の 2 つの Wiki を読んで使用方法を学ぶことをお勧めします。
音声データのキャプチャ(オフライン)
ステップ 1. 録音した音声サンプルを .wav オーディオファイルとして microSD カードに保存します。
オンボードの SD カードリーダーを使用して .wav オーディオファイルを保存します。まず、XIAO の PSRAM を有効にする必要があります。

次に、以下のプログラムをコンパイルして XIAO ESP32S3 にアップロードします。
このコードは、Seeed XIAO ESP32S3 Sense ボードの I2S インターフェースを使用して音声を録音し、録音を SD カード上の .wav ファイルとして保存します。また、シリアルモニターから送信されるコマンドを通じて録音プロセスを制御できます。オーディオファイルの名前はカスタマイズ可能で(トレーニングで使用するクラスラベルである必要があります)、複数の録音を行い、それぞれを新しいファイルに保存できます。また、録音の音量を増加させる機能も含まれています。
ESP32 バージョンが 2.0.x の場合、完全なプログラムをプレビューするにはここをクリック
/*
* Seeed XIAO ESP32S3 Sense 用 WAV レコーダー
*
* 注意: このコードを実行するには、ESP-32 チップの PSRAM 機能を使用する必要があります。
* アップロード前に有効にしてください。
* ツール > PSRAM: "OPI PSRAM"
*
* M.Rovai による改変 @2023年5月、Seeed のオリジナルコードから
*/
#include <I2S.h>
#include "FS.h"
#include "SD.h"
#include "SPI.h"
// 必要に応じて変更
#define RECORD_TIME 10 // 秒、最大値は 240
#define WAV_FILE_NAME "data"
// 最適な設定のため変更しない
#define SAMPLE_RATE 16000U
#define SAMPLE_BITS 16
#define WAV_HEADER_SIZE 44
#define VOLUME_GAIN 2
int fileNumber = 1;
String baseFileName;
bool isRecording = false;
void setup() {
Serial.begin(115200);
while (!Serial) ;
I2S.setAllPins(-1, 42, 41, -1, -1);
if (!I2S.begin(PDM_MONO_MODE, SAMPLE_RATE, SAMPLE_BITS)) {
Serial.println("I2S の初期化に失敗しました!");
while (1) ;
}
if(!SD.begin(21)){
Serial.println("SD カードのマウントに失敗しました!");
while (1) ;
}
Serial.printf("ラベル名を入力してください\n");
//record_wav();
}
void loop() {
if (Serial.available() > 0) {
String command = Serial.readStringUntil('\n');
command.trim();
if (command == "rec") {
isRecording = true;
} else {
baseFileName = command;
fileNumber = 1; // 新しいベースファイル名が設定されるたびにファイル番号をリセット
Serial.printf("録音を開始するには rec を送信してください \n");
}
}
if (isRecording && baseFileName != "") {
String fileName = "/" + baseFileName + "." + String(fileNumber) + ".wav";
fileNumber++;
record_wav(fileName);
delay(1000); // 一度に複数のファイルを録音しないように遅延
isRecording = false;
}
}
void record_wav(String fileName)
{
uint32_t sample_size = 0;
uint32_t record_size = (SAMPLE_RATE * SAMPLE_BITS / 8) * RECORD_TIME;
uint8_t *rec_buffer = NULL;
Serial.printf("録音を開始します...\n");
File file = SD.open(fileName.c_str(), FILE_WRITE);
// WAV ファイルにヘッダーを書き込む
uint8_t wav_header[WAV_HEADER_SIZE];
generate_wav_header(wav_header, record_size, SAMPLE_RATE);
file.write(wav_header, WAV_HEADER_SIZE);
// 録音用に PSRAM を確保
rec_buffer = (uint8_t *)ps_malloc(record_size);
if (rec_buffer == NULL) {
Serial.printf("メモリ確保に失敗しました!\n");
while(1) ;
}
Serial.printf("バッファ: %d バイト\n", ESP.getPsramSize() - ESP.getFreePsram());
// 録音開始
esp_i2s::i2s_read(esp_i2s::I2S_NUM_0, rec_buffer, record_size, &sample_size, portMAX_DELAY);
if (sample_size == 0) {
Serial.printf("録音に失敗しました!\n");
} else {
Serial.printf("録音 %d バイト\n", sample_size);
}
// 音量を増加
for (uint32_t i = 0; i < sample_size; i += SAMPLE_BITS/8) {
(*(uint16_t *)(rec_buffer+i)) <<= VOLUME_GAIN;
}
// WAV ファイルにデータを書き込む
Serial.printf("ファイルに書き込み中...\n");
if (file.write(rec_buffer, record_size) != record_size)
Serial.printf("ファイル書き込みに失敗しました!\n");
free(rec_buffer);
file.close();
Serial.printf("録音完了: \n");
Serial.printf("新しいサンプルを録音するには rec を送信するか、新しいラベルを入力してください\n\n");
}
void generate_wav_header(uint8_t *wav_header, uint32_t wav_size, uint32_t sample_rate)
{
// 参考: http://soundfile.sapp.org/doc/WaveFormat/
uint32_t file_size = wav_size + WAV_HEADER_SIZE - 8;
uint32_t byte_rate = SAMPLE_RATE * SAMPLE_BITS / 8;
const uint8_t set_wav_header[] = {
'R', 'I', 'F', 'F', // ChunkID
file_size, file_size >> 8, file_size >> 16, file_size >> 24, // ChunkSize
'W', 'A', 'V', 'E', // Format
'f', 'm', 't', ' ', // Subchunk1ID
0x10, 0x00, 0x00, 0x00, // Subchunk1Size (16 for PCM)
0x01, 0x00, // AudioFormat (1 for PCM)
0x01, 0x00, // NumChannels (1 channel)
sample_rate, sample_rate >> 8, sample_rate >> 16, sample_rate >> 24, // SampleRate
byte_rate, byte_rate >> 8, byte_rate >> 16, byte_rate >> 24, // ByteRate
0x02, 0x00, // BlockAlign
0x10, 0x00, // BitsPerSample (16 bits)
'd', 'a', 't', 'a', // Subchunk2ID
wav_size, wav_size >> 8, wav_size >> 16, wav_size >> 24, // Subchunk2Size
};
memcpy(wav_header, set_wav_header, sizeof(set_wav_header));
}
ESP32のバージョンが3.0.xの場合は、ここをクリックして完全なプログラムをプレビューしてください
/*
* Seeed XIAO ESP32S3 Sense 用 WAV レコーダー
*
* 注意: このコードを実行するには、ESP-32チップのPSRAM機能を使用する必要があります。
* アップロード前に有効にしてください。
* ツール > PSRAM: "OPI PSRAM"
*
* M.Rovaiによって2023年5月にSeeedのオリジナルコードを基に改変
*/
#include <ESP_I2S.h>
#include "FS.h"
#include "SD.h"
#include "SPI.h"
// 必要に応じて変更
#define RECORD_TIME 10 // 秒, 最大値は240
#define WAV_FILE_NAME "data"
// 最適な設定のため変更しない
#define SAMPLE_RATE 16000U
#define SAMPLE_BITS 16
#define WAV_HEADER_SIZE 44
#define VOLUME_GAIN 2
I2SClass I2S;
String baseFileName;
int fileNumber = 1;
bool isRecording = false;
void setup() {
Serial.begin(115200);
while (!Serial) ;
// 42番ピンをPDMクロック、41番ピンをPDMデータピンとして設定
I2S.setPinsPdmRx(42, 41);
if (!I2S.begin(I2S_MODE_PDM_RX, 16000, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO)) {
Serial.println("I2Sの初期化に失敗しました!");
while (1) ;
}
if(!SD.begin(21)){
Serial.println("SDカードのマウントに失敗しました!");
while (1) ;
}
Serial.printf("ラベル名を入力してください\n");
//record_wav();
}
void loop() {
if (Serial.available() > 0) {
String command = Serial.readStringUntil('\n');
command.trim();
if (command == "rec") {
isRecording = true;
} else {
baseFileName = command;
fileNumber = 1; // 新しいベースファイル名が設定されるたびにファイル番号をリセット
Serial.printf("録音を開始するには「rec」と送信してください\n");
}
}
if (isRecording && baseFileName != "") {
String fileName = "/" + baseFileName + "." + String(fileNumber) + ".wav";
fileNumber++;
record_wav(fileName);
delay(1000); // 複数のファイルを一度に録音しないように遅延
isRecording = false;
}
}
void record_wav(String fileName)
{
uint32_t sample_size = 0;
uint32_t record_size = (SAMPLE_RATE * SAMPLE_BITS / 8) * RECORD_TIME;
uint8_t *rec_buffer = NULL;
Serial.printf("録音を開始します...\n");
File file = SD.open(fileName.c_str(), FILE_WRITE);
// WAVファイルにヘッダーを書き込む
uint8_t wav_header[WAV_HEADER_SIZE];
generate_wav_header(wav_header, record_size, SAMPLE_RATE);
file.write(wav_header, WAV_HEADER_SIZE);
// PSRAMを使用して録音用メモリを確保
rec_buffer = (uint8_t *)ps_malloc(record_size);
if (rec_buffer == NULL) {
Serial.printf("メモリ確保に失敗しました!\n");
while(1) ;
}
Serial.printf("バッファ: %d バイト\n", ESP.getPsramSize() - ESP.getFreePsram());
// 録音を開始
esp_i2s::i2s_read(esp_i2s::I2S_NUM_0, rec_buffer, record_size, &sample_size, portMAX_DELAY);
if (sample_size == 0) {
Serial.printf("録音に失敗しました!\n");
} else {
Serial.printf("%d バイトを録音しました\n", sample_size);
}
// 音量を増幅
for (uint32_t i = 0; i < sample_size; i += SAMPLE_BITS/8) {
(*(uint16_t *)(rec_buffer+i)) <<= VOLUME_GAIN;
}
// WAVファイルにデータを書き込む
Serial.printf("ファイルに書き込み中...\n");
if (file.write(rec_buffer, record_size) != record_size)
Serial.printf("ファイル書き込みに失敗しました!\n");
free(rec_buffer);
file.close();
Serial.printf("録音が完了しました:\n");
Serial.printf("新しいサンプルを録音するには「rec」を送信するか、新しいラベルを入力してください\n\n");
}
void generate_wav_header(uint8_t *wav_header, uint32_t wav_size, uint32_t sample_rate)
{
// 参考: http://soundfile.sapp.org/doc/WaveFormat/
uint32_t file_size = wav_size + WAV_HEADER_SIZE - 8;
uint32_t byte_rate = SAMPLE_RATE * SAMPLE_BITS / 8;
const uint8_t set_wav_header[] = {
'R', 'I', 'F', 'F', // ChunkID
file_size, file_size >> 8, file_size >> 16, file_size >> 24, // ChunkSize
'W', 'A', 'V', 'E', // Format
'f', 'm', 't', ' ', // Subchunk1ID
0x10, 0x00, 0x00, 0x00, // Subchunk1Size (PCMの場合は16)
0x01, 0x00, // AudioFormat (PCMの場合は1)
0x01, 0x00, // NumChannels (1チャンネル)
sample_rate, sample_rate >> 8, sample_rate >> 16, sample_rate >> 24, // SampleRate
byte_rate, byte_rate >> 8, byte_rate >> 16, byte_rate >> 24, // ByteRate
0x02, 0x00, // BlockAlign
0x10, 0x00, // BitsPerSample (16ビット)
'd', 'a', 't', 'a', // Subchunk2ID
wav_size, wav_size >> 8, wav_size >> 16, wav_size >> 24, // Subchunk2Size
};
memcpy(wav_header, set_wav_header, sizeof(set_wav_header));
}
次に、このコードをXIAOにアップロードし、「hello」や「stop」といったキーワードのサンプルを取得します。また、ノイズやその他の単語も録音できます。シリアルモニターが録音するラベルを入力するように促します。
ラベル(例: hello)を送信します。プログラムは次のコマンドを待機します: rec。

そして、コマンドrecが送信されるたびにプログラムは新しいサンプルの録音を開始します。ファイルはhello.1.wav、hello.2.wav、hello.3.wavなどとして保存されます。新しいラベル(例: stop)が送信された場合、各新しいサンプルに対してコマンドrecを送信する必要があり、stop.1.wav、stop.2.wav、stop.3.wavなどとして保存されます。
最終的に、SDカードに保存されたファイルを取得します。
各ラベルサンプルに十分な音声を用意することをお勧めします。10秒間の録音セッション中にキーワードを何度も繰り返すことができますが、次のステップでサンプルを分割するために、キーワード間に少し間隔を空ける必要があります。
トレーニングデータの取得
ステップ 2. 収集した音声データのアップロード
生データセットが定義され収集されたら、Edge Impulseで新しいプロジェクトを開始する必要があります。プロジェクトが作成されたら、データ取得セクションで既存データのアップロードツールを選択します。アップロードするファイルを選択してください。

そして、それらをスタジオにアップロードします(データを自動的にトレーニング/テストに分割することができます)。すべてのクラスとすべての生データに対してこの手順を繰り返してください。
データセット内のすべてのデータは1秒の長さですが、前のセクションで記録されたサンプルは10秒であり、互換性を持たせるために1秒のサンプルに分割する必要があります。サンプル名の後にある三点アイコンをクリックし、サンプルを分割を選択してください。

ツール内に入ったら、データを1秒の記録に分割します。必要に応じてセグメントを追加または削除してください。

この手順はすべてのサンプルに対して繰り返す必要があります。
ステップ 3. インパルスの作成(前処理 / モデル定義)
インパルスは生データを取り込み、信号処理を使用して特徴を抽出し、その後学習ブロックを使用して新しいデータを分類します。

まず、1秒のウィンドウでデータポイントを取得し、データを拡張し、そのウィンドウを500msずつスライドさせます。ゼロパッドデータオプションが設定されていることに注意してください。これは、1秒未満のサンプルをゼロで埋めるために重要です(場合によっては、ノイズやスパイクを避けるために分割ツールで1000msウィンドウを減少させました)。
各1秒の音声サンプルは前処理され、画像(例えば、13 x 49 x 1)に変換される必要があります。ここでは、MFCCを使用します。これは、Mel Frequency Cepstral Coefficientsを使用して音声信号から特徴を抽出し、人間の声に非常に適しています。
次に、分類にはKERASを選択します。これにより、畳み込みニューラルネットワークを使用して画像分類を行い、モデルをゼロから構築します。
ステップ 4. 前処理(MFCC)
次のステップは、次のフェーズでトレーニングされる画像を作成することです。デフォルトのパラメータ値を保持するか、DSPのAutotuneparametersオプションを利用することができます。ここでは後者を選択します。

機械学習モデルの構築
ステップ 5. モデル設計とトレーニング
畳み込みニューラルネットワーク(CNN)モデルを使用します。基本的なアーキテクチャは、Conv1D + MaxPoolingの2つのブロック(それぞれ8および16ニューロン)と0.25のドロップアウトで定義されます。そして最後の層では、フラット化後に4つのニューロンを持ち、各クラスに1つずつ割り当てます。
ハイパーパラメータとしては、学習率を0.005に設定し、モデルを100エポックでトレーニングします。また、ノイズなどのデータ拡張も含めます。結果は良好に見えます。

XIAO ESP32S3 Senseへのデプロイ
ステップ6. XIAO ESP32S3 Senseへのデプロイ
Edge Impulseは必要なライブラリ、前処理関数、トレーニング済みモデルをすべてパッケージ化し、それをコンピュータにダウンロードします。Arduino Libraryオプションを選択し、下部でQuantized (Int8)を選択して、Buildボタンを押してください。

Edge ImpulseはESP NNアクセラレータを使用したESP32S3用のSDKをまだリリースしていませんが、Dmitry Maslovのおかげで、ESP32-S3用にアセンブリ最適化が復元され修正されています。このソリューションはまだ公式ではなく、EIが他のボードとの競合を解決した後にEI SDKに含まれる予定です。
現在のところ、これは非EONバージョンでのみ動作します。そのため、Enable EON Compilerオプションを選択しないようにしてください。
Buildボタンを選択すると、Zipファイルが作成され、コンピュータにダウンロードされます。
ダウンロードしたライブラリを使用する前に、ESP NNアクセラレータを有効にする必要があります。そのためには、プロジェクトのGitHubから予備バージョンをダウンロードし、解凍して、Arduinoライブラリフォルダ内のsrc/edge-impulse-sdk/porting/espressif/ESP-NN
にあるESP NNフォルダと置き換えてください。

Arduino IDEで、Sketchタブに移動し、Add .ZIP Libraryオプションを選択して、Edge Impulseによってダウンロードされた.zipファイルを選択します。
完全なコードはプロジェクトのGitHubで確認できます。スケッチをボードにアップロードし、実際の推論をテストしてください。
コード内でインポートされるライブラリは、使用するライブラリの名前に更新する必要があります。また、点灯のロジックは、実際にトレーニングしたラベルの順序に基づいて修正する必要があります。
ESP32のバージョンが2.0.xの場合、完全なプログラムをプレビューするにはここをクリックしてください
/* Edge Impulse Arduino examples
* Copyright (c) 2022 EdgeImpulse Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// メモリが制限されているターゲットの場合、このマクロを削除して10K RAMを節約
#define EIDSP_QUANTIZE_FILTERBANK 0
/*
** 注意: TFLiteアリーナ割り当ての問題が発生した場合。
**
** これは動的メモリの断片化が原因である可能性があります。
** boards.local.txtに"-DEI_CLASSIFIER_ALLOCATION_STATIC"を定義してみてください(存在しない場合は作成)。
** このファイルを`<ARDUINO_CORE_INSTALL_PATH>/arduino/hardware/<mbed_core>/<core_version>/`にコピーしてください。
**
** Arduinoがコアをインストールする場所を見つけるには、以下を参照してください:
** (https://support.arduino.cc/hc/en-us/articles/360012076960-Where-are-the-installed-cores-located-)
**
** 問題が解決しない場合、このモデルとアプリケーションに十分なメモリがありません。
*/
/* インクルード ---------------------------------------------------------------- */
#include <XIAO-ESP32S3-KWS_inferencing.h>
#include <I2S.h>
#define SAMPLE_RATE 16000U
#define SAMPLE_BITS 16
#define LED_BUILT_IN 21
/** オーディオバッファ、ポインタ、セレクタ */
typedef struct {
int16_t *buffer;
uint8_t buf_ready;
uint32_t buf_count;
uint32_t n_samples;
} inference_t;
static inference_t inference;
static const uint32_t sample_buffer_size = 2048;
static signed short sampleBuffer[sample_buffer_size];
static bool debug_nn = false; // 生の信号から生成された特徴などを表示するにはtrueに設定
static bool record_status = true;
/**
* @brief Arduinoセットアップ関数
*/
void setup()
{
// 初回実行時のセットアップコード
Serial.begin(115200);
// USB接続待機をキャンセルするには以下の行をコメントアウト
while (!Serial);
Serial.println("Edge Impulse Inferencing Demo");
pinMode(LED_BUILT_IN, OUTPUT); // ピンを出力として設定
digitalWrite(LED_BUILT_IN, HIGH); // 消灯
I2S.setAllPins(-1, 42, 41, -1, -1);
if (!I2S.begin(PDM_MONO_MODE, SAMPLE_RATE, SAMPLE_BITS)) {
Serial.println("I2Sの初期化に失敗しました!");
while (1) ;
}
// 推論設定の概要(model_metadata.hから)
ei_printf("推論設定:\n");
ei_printf("\t間隔: ");
ei_printf_float((float)EI_CLASSIFIER_INTERVAL_MS);
ei_printf(" ms.\n");
ei_printf("\tフレームサイズ: %d\n", EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE);
ei_printf("\tサンプル長: %d ms.\n", EI_CLASSIFIER_RAW_SAMPLE_COUNT / 16);
ei_printf("\tクラス数: %d\n", sizeof(ei_classifier_inferencing_categories) / sizeof(ei_classifier_inferencing_categories[0]));
ei_printf("\n2秒後に連続推論を開始します...\n");
ei_sleep(2000);
if (microphone_inference_start(EI_CLASSIFIER_RAW_SAMPLE_COUNT) == false) {
ei_printf("エラー: オーディオバッファ(サイズ %d)を割り当てられませんでした。この問題はモデルのウィンドウ長が原因である可能性があります\r\n", EI_CLASSIFIER_RAW_SAMPLE_COUNT);
return;
}
ei_printf("録音中...\n");
}
/**
* @brief Arduinoメイン関数。推論ループを実行します。
*/
void loop()
{
bool m = microphone_inference_record();
if (!m) {
ei_printf("エラー: オーディオの録音に失敗しました...\n");
return;
}
signal_t signal;
signal.total_length = EI_CLASSIFIER_RAW_SAMPLE_COUNT;
signal.get_data = µphone_audio_signal_get_data;
ei_impulse_result_t result = { 0 };
EI_IMPULSE_ERROR r = run_classifier(&signal, &result, debug_nn);
if (r != EI_IMPULSE_OK) {
ei_printf("エラー: クラス分類の実行に失敗しました (%d)\n", r);
return;
}
int pred_index = 0; // pred_indexを初期化
float pred_value = 0; // pred_valueを初期化
// 推論結果を出力
ei_printf("予測 ");
ei_printf("(DSP: %d ms., 分類: %d ms., 異常: %d ms.)",
result.timing.dsp, result.timing.classification, result.timing.anomaly);
ei_printf(": \n");
for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
ei_printf(" %s: ", result.classification[ix].label);
ei_printf_float(result.classification[ix].value);
ei_printf("\n");
if (result.classification[ix].value > pred_value){
pred_index = ix;
pred_value = result.classification[ix].value;
}
}
// 推論結果を表示
if (pred_index == 3){
digitalWrite(LED_BUILT_IN, LOW); // 点灯
}
else{
digitalWrite(LED_BUILT_IN, HIGH); // 消灯
}
#if EI_CLASSIFIER_HAS_ANOMALY == 1
ei_printf(" 異常スコア: ");
ei_printf_float(result.anomaly);
ei_printf("\n");
#endif
}
static void audio_inference_callback(uint32_t n_bytes)
{
for(int i = 0; i < n_bytes>>1; i++) {
inference.buffer[inference.buf_count++] = sampleBuffer[i];
if(inference.buf_count >= inference.n_samples) {
inference.buf_count = 0;
inference.buf_ready = 1;
}
}
}
static void capture_samples(void* arg) {
const int32_t i2s_bytes_to_read = (uint32_t)arg;
size_t bytes_read = i2s_bytes_to_read;
while (record_status) {
/* i2sから一度にデータを読み取る - XIAO ESP2S3 SenseおよびI2S.hライブラリ用に修正 */
// i2s_read((i2s_port_t)1, (void*)sampleBuffer, i2s_bytes_to_read, &bytes_read, 100);
esp_i2s::i2s_read(esp_i2s::I2S_NUM_0, (void*)sampleBuffer, i2s_bytes_to_read, &bytes_read, 100);
if (bytes_read <= 0) {
ei_printf("I2S読み取りエラー : %d", bytes_read);
}
else {
if (bytes_read < i2s_bytes_to_read) {
ei_printf("部分的なI2S読み取り");
}
// データをスケール(そうしないと音が小さすぎる)
for (int x = 0; x < i2s_bytes_to_read/2; x++) {
sampleBuffer[x] = (int16_t)(sampleBuffer[x]) * 8;
}
if (record_status) {
audio_inference_callback(i2s_bytes_to_read);
}
else {
break;
}
}
}
vTaskDelete(NULL);
}
/**
* @brief 推論構造体を初期化し、PDMをセットアップ/開始
*
* @param[in] n_samples サンプル数
*
* @return { 戻り値の説明 }
*/
static bool microphone_inference_start(uint32_t n_samples)
{
inference.buffer = (int16_t *)malloc(n_samples * sizeof(int16_t));
if(inference.buffer == NULL) {
return false;
}
inference.buf_count = 0;
inference.n_samples = n_samples;
inference.buf_ready = 0;
// if (i2s_init(EI_CLASSIFIER_FREQUENCY)) {
// ei_printf("I2Sの開始に失敗しました!");
// }
ei_sleep(100);
record_status = true;
xTaskCreate(capture_samples, "CaptureSamples", 1024 * 32, (void*)sample_buffer_size, 10, NULL);
return true;
}
/**
* @brief 新しいデータを待機
*
* @return 完了時にtrue
*/
static bool microphone_inference_record(void)
{
bool ret = true;
while (inference.buf_ready == 0) {
delay(10);
}
inference.buf_ready = 0;
return ret;
}
/**
* 生のオーディオ信号データを取得
*/
static int microphone_audio_signal_get_data(size_t offset, size_t length, float *out_ptr)
{
numpy::int16_to_float(&inference.buffer[offset], out_ptr, length);
return 0;
}
/**
* @brief PDMを停止し、バッファを解放
*/
static void microphone_inference_end(void)
{
free(sampleBuffer);
ei_free(inference.buffer);
}
#if !defined(EI_CLASSIFIER_SENSOR) || EI_CLASSIFIER_SENSOR != EI_CLASSIFIER_SENSOR_MICROPHONE
#error "現在のセンサーに対して無効なモデルです。"
#endif
ESP32のバージョンが3.0.xの場合。完全なプログラムをプレビューするにはここをクリックしてください。
/* Edge Impulse Arduino examples
* Copyright (c) 2022 EdgeImpulse Inc.
*
* 本ソフトウェアおよび関連するドキュメントファイル(以下「ソフトウェア」)を取得するすべての人に対し、
* ソフトウェアを制限なく使用、コピー、変更、結合、公開、配布、サブライセンス、または販売する権利を
* 無償で許可します。
*
* 上記の著作権表示および本許可表示は、ソフトウェアのすべてのコピーまたは重要な部分に含まれるものとします。
*
* 本ソフトウェアは「現状のまま」提供され、明示的または黙示的な保証はありません。
* 商品性、特定目的への適合性、および非侵害性を含むがこれに限定されない保証も含まれません。
* 著者または著作権者は、本ソフトウェアまたはその使用またはその他の取引に関連して発生する
* いかなる請求、損害、またはその他の責任についても責任を負いません。
*/
// メモリが制限されているターゲットの場合、このマクロを削除して10K RAMを節約します
#define EIDSP_QUANTIZE_FILTERBANK 0
/*
** 注意: TFLiteアリーナ割り当ての問題が発生した場合。
**
** これは動的メモリの断片化が原因である可能性があります。
** boards.local.txtに"-DEI_CLASSIFIER_ALLOCATION_STATIC"を定義してみてください(存在しない場合は作成)。
** このファイルを`<ARDUINO_CORE_INSTALL_PATH>/arduino/hardware/<mbed_core>/<core_version>/`にコピーしてください。
**
** Arduinoがコアをインストールする場所を見つけるには、
** (https://support.arduino.cc/hc/en-us/articles/360012076960-Where-are-the-installed-cores-located-)を参照してください。
**
** 問題が解決しない場合、このモデルとアプリケーションに十分なメモリがありません。
*/
/* インクルード ---------------------------------------------------------------- */
#include <XIAO-ESP32S3-KWS_inferencing.h>
#include <ESP_I2S.h>
I2SClass I2S;
#define SAMPLE_RATE 16000U
#define SAMPLE_BITS 16
#define LED_BUILT_IN 21
/** オーディオバッファ、ポインタ、およびセレクタ */
typedef struct {
int16_t *buffer;
uint8_t buf_ready;
uint32_t buf_count;
uint32_t n_samples;
} inference_t;
static inference_t inference;
static const uint32_t sample_buffer_size = 2048;
static signed short sampleBuffer[sample_buffer_size];
static bool debug_nn = false; // 生信号から生成された特徴などを表示するにはtrueに設定
static bool record_status = true;
/**
* @brief Arduinoのセットアップ関数
*/
void setup()
{
// 初回実行時にセットアップコードをここに記述
Serial.begin(115200);
// USB接続を待機するには以下の行をコメントアウト
while (!Serial);
Serial.println("Edge Impulse Inferencing Demo");
pinMode(LED_BUILT_IN, OUTPUT); // ピンを出力として設定
digitalWrite(LED_BUILT_IN, HIGH); // オフにする
// 42 PDMクロックと41 PDMデータピンを設定
I2S.setPinsPdmRx(42, 41);
if (!I2S.begin(I2S_MODE_PDM_RX, 16000, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO)) {
Serial.println("I2Sの初期化に失敗しました!");
while (1) ;
}
// 推論設定の概要(model_metadata.hから)
ei_printf("推論設定:\n");
ei_printf("\t間隔: ");
ei_printf_float((float)EI_CLASSIFIER_INTERVAL_MS);
ei_printf(" ms.\n");
ei_printf("\tフレームサイズ: %d\n", EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE);
ei_printf("\tサンプル長: %d ms.\n", EI_CLASSIFIER_RAW_SAMPLE_COUNT / 16);
ei_printf("\tクラス数: %d\n", sizeof(ei_classifier_inferencing_categories) / sizeof(ei_classifier_inferencing_categories[0]));
ei_printf("\n2秒後に連続推論を開始します...\n");
ei_sleep(2000);
if (microphone_inference_start(EI_CLASSIFIER_RAW_SAMPLE_COUNT) == false) {
ei_printf("エラー: オーディオバッファ(サイズ %d)を割り当てられませんでした。これはモデルのウィンドウ長が原因である可能性があります\r\n", EI_CLASSIFIER_RAW_SAMPLE_COUNT);
return;
}
ei_printf("録音中...\n");
}
/**
* @brief Arduinoのメイン関数。推論ループを実行します。
*/
void loop()
{
bool m = microphone_inference_record();
if (!m) {
ei_printf("エラー: オーディオの録音に失敗しました...\n");
return;
}
signal_t signal;
signal.total_length = EI_CLASSIFIER_RAW_SAMPLE_COUNT;
signal.get_data = µphone_audio_signal_get_data;
ei_impulse_result_t result = { 0 };
EI_IMPULSE_ERROR r = run_classifier(&signal, &result, debug_nn);
if (r != EI_IMPULSE_OK) {
ei_printf("エラー: クラス分類の実行に失敗しました (%d)\n", r);
return;
}
int pred_index = 0; // pred_indexを初期化
float pred_value = 0; // pred_valueを初期化
// 予測結果を表示
ei_printf("予測結果 ");
ei_printf("(DSP: %d ms., 分類: %d ms., 異常: %d ms.)",
result.timing.dsp, result.timing.classification, result.timing.anomaly);
ei_printf(": \n");
for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
ei_printf(" %s: ", result.classification[ix].label);
ei_printf_float(result.classification[ix].value);
ei_printf("\n");
if (result.classification[ix].value > pred_value){
pred_index = ix;
pred_value = result.classification[ix].value;
}
}
// 推論結果を表示
if (pred_index == 3){
digitalWrite(LED_BUILT_IN, LOW); //オンにする
}
else{
digitalWrite(LED_BUILT_IN, HIGH); //オフにする
}
#if EI_CLASSIFIER_HAS_ANOMALY == 1
ei_printf(" 異常スコア: ");
ei_printf_float(result.anomaly);
ei_printf("\n");
#endif
}
static void audio_inference_callback(uint32_t n_bytes)
{
for(int i = 0; i < n_bytes>>1; i++) {
inference.buffer[inference.buf_count++] = sampleBuffer[i];
if(inference.buf_count >= inference.n_samples) {
inference.buf_count = 0;
inference.buf_ready = 1;
}
}
}
static void capture_samples(void* arg) {
const int32_t i2s_bytes_to_read = (uint32_t)arg;
size_t bytes_read = i2s_bytes_to_read;
while (record_status) {
/* i2sから一度にデータを読み取る - XIAO ESP2S3 SenseおよびI2S.hライブラリ用に変更 */
// i2s_read((i2s_port_t)1, (void*)sampleBuffer, i2s_bytes_to_read, &bytes_read, 100);
esp_i2s::i2s_read(esp_i2s::I2S_NUM_0, (void*)sampleBuffer, i2s_bytes_to_read, &bytes_read, 100);
if (bytes_read <= 0) {
ei_printf("I2S読み取りエラー : %d", bytes_read);
}
else {
if (bytes_read < i2s_bytes_to_read) {
ei_printf("部分的なI2S読み取り");
}
// データをスケール(そうしないと音が小さすぎる)
for (int x = 0; x < i2s_bytes_to_read/2; x++) {
sampleBuffer[x] = (int16_t)(sampleBuffer[x]) * 8;
}
if (record_status) {
audio_inference_callback(i2s_bytes_to_read);
}
else {
break;
}
}
}
vTaskDelete(NULL);
}
/**
* @brief 推論構造体を初期化し、PDMをセットアップ/開始
*
* @param[in] n_samples サンプル数
*
* @return { 戻り値の説明 }
*/
static bool microphone_inference_start(uint32_t n_samples)
{
inference.buffer = (int16_t *)malloc(n_samples * sizeof(int16_t));
if(inference.buffer == NULL) {
return false;
}
inference.buf_count = 0;
inference.n_samples = n_samples;
inference.buf_ready = 0;
// if (i2s_init(EI_CLASSIFIER_FREQUENCY)) {
// ei_printf("I2Sの開始に失敗しました!");
// }
ei_sleep(100);
record_status = true;
xTaskCreate(capture_samples, "CaptureSamples", 1024 * 32, (void*)sample_buffer_size, 10, NULL);
return true;
}
/**
* @brief 新しいデータを待機
*
* @return 完了時にtrue
*/
static bool microphone_inference_record(void)
{
bool ret = true;
while (inference.buf_ready == 0) {
delay(10);
}
inference.buf_ready = 0;
return ret;
}
/**
* 生のオーディオ信号データを取得
*/
static int microphone_audio_signal_get_data(size_t offset, size_t length, float *out_ptr)
{
numpy::int16_to_float(&inference.buffer[offset], out_ptr, length);
return 0;
}
/**
* @brief PDMを停止し、バッファを解放
*/
static void microphone_inference_end(void)
{
free(sampleBuffer);
ei_free(inference.buffer);
}
#if !defined(EI_CLASSIFIER_SENSOR) || EI_CLASSIFIER_SENSOR != EI_CLASSIFIER_SENSOR_MICROPHONE
#error "現在のセンサーに対して無効なモデルです。"
#endif
キーワード HELLO が検出されると、LED が点灯する仕組みです。同様に、LED を点灯させる代わりに、導入部分で説明したように、外部デバイスの「トリガー」として使用することも可能です。

特別な感謝
MJRoBot (Marcelo Rovai) に感謝します。彼の XIAO ESP32S3 Sense を使用した Edge Impulse アクセスに関するチュートリアルコンテンツは非常に詳細で、機械学習に関する多くの知識が含まれています。
この記事の元の内容を読みたい場合は、下記のリンクから直接元の記事にアクセスできます。
MJRoBot は、XIAO ESP32S3 に関する非常に興味深いプロジェクトも多数公開しています。
リソース
- [GITHUB] プロジェクトの Github
- [EDGE-IMPULSE] Edge Impulse デモ
技術サポート & 製品ディスカッション
弊社製品をお選びいただきありがとうございます!製品をスムーズにご利用いただけるよう、さまざまなサポートを提供しています。お客様の好みやニーズに応じた複数のコミュニケーションチャネルをご用意しています。