In diesem Tutorial lernst du, wie du eine Sprachsteuerungsanwendung mit dem XIAO-ESP32-S3-Sense Board und der Edge Impulse Plattform erstellst. Zuerst sammeln wir Audiodaten mit dem Mikrofon des XIAO-ESP32-S3-Sense. Dann laden wir diese Audiodaten auf die Edge Impulse Plattform hoch und trainieren ein Keyword-Spotting-Modell. Schließlich setzen wir dieses TinyML-Modell auf dem XIAO-ESP32-S3-Sense ein, wodurch wir LEDs per Sprachbefehl steuern können.
Das TinyML-Modell wird darauf trainiert, die Wörter „rot“, „gelb“ und „grün“ zu erkennen. Wir verbinden drei LEDs (rot, gelb, grün) mit dem XIAO-ESP32-S3-Sense Board, die du dann per Sprachbefehl ein- und ausschalten kannst. Sieh dir das System unten in Aktion an:
Falls du den XIAO-ESP32-S3 Sense noch nicht benutzt hast, schau dir zuerst das Getting started with XIAO-ESP32-S3-Sense Tutorial an, ansonsten legen wir los …
Benötigte Teile
Du benötigst ein XIAO ESP32 S3 Sense Board, um die Codebeispiele auszuprobieren. Beachte, dass das Board heiß werden kann und du eventuell einen kleinen Heatsink am Rücken anbringen möchtest (siehe unten aufgeführtes Teil).
Außerdem brauchst du eine SD-Karte, um die gesammelten Audiodaten zu speichern. Ich habe eine 32 GB Karte aufgeführt, aber eine kleinere (8 GB) reicht auch aus. Und falls dein Computer keinen eingebauten SD-Kartenleser hat, benötigst du auch einen externen Kartenleser.

Seeed Studio XIAO ESP32 S3 Sense

USB-C-Kabel

Kleiner Kühlkörper 9×9 mm

SD-Karte 32GB

SD-Kartenleser
Makerguides is a participant in affiliate advertising programs designed to provide a means for sites to earn advertising fees by linking to Amazon, AliExpress, Elecrow, and other sites. As an Affiliate we may earn from qualifying purchases.
Das XIAO-ESP32-S3-Sense Board
Das XIAO-ESP32-S3-SenseBoard basiert auf dem ESP32-S3, einem Chip von Espressif, der für KI- und Edge-Computing-Aufgaben optimiert ist. Das Board besteht aus vier Teilen: dem Hauptboard, dem Sense Hat, der Kamera und einer externen Wi-Fi-Antenne:

Der Sense Hat kann auf das Hauptboard gesteckt werden und verfügt über einen SD-Karten-Sockel, eine Kamerabuchse und ein Mikrofon. Das Bild unten zeigt das komplett montierte XIAO-ESP32-S3-Sense Board mit eingelegter SD-Karte:

Für dieses Projekt benötigen wir jedoch weder die WiFi-Antenne noch die Kamera. Die unten gezeigte Konfiguration reicht aus und du brauchst die SD-Karte nicht mehr, sobald du die Audiodaten gesammelt hast.

Beachte, dass das Board entweder über den USB Type-C Anschluss oder über die Batterie-Ladeschnittstelle mit Strom versorgt werden kann, die an eine 3,7V LiPo-Batterie angeschlossen wird.
Mikrofon des XIAO-ESP32-S3-Sense
Das XIAO-ESP32-S3-Sense verfügt über ein eingebautes digitales MEMS Microphone vom Typ MSM261D3526H1CPM, das sich auf dem Sense Hat befindet. Siehe das Bild unten:

Die Kommunikation mit dem Mikrofon erfolgt über zwei Signalleitungen (PDM_CLK, PDM_DATA) via I2S protocol, die mit IO42 und IO41 verbunden sind, wie im Schaltplan unten gezeigt:

Für detailliertere Informationen siehe unser Record Audio with XIAO-ESP32-S3-Sense Tutorial.
Pinbelegung des XIAO-ESP32-S3-Sense Boards
Zum Schluss werfen wir einen Blick auf die Pinbelegung des XIAO-ESP32-S3-Sense Boards:

Der 5V-Pin liefert 5V vom USB-Anschluss. Der 3V3-Pin gibt die Ausgangsspannung des onboard-Reglers aus und kann bis zu 700mA liefern. Der GND-Pin ist die Masse.
Was die GPIOs betrifft: Das Board bietet 11 digitale/analoge GPIOs, aber GPIO0, GPIO3, GPIO43 und GPIO44 sind Strapping-Pins, die beim Start in einem bestimmten Zustand sein müssen – also sei vorsichtig mit diesen. Sobald der Mikrocontroller läuft, funktionieren die Strapping-Pins wie normale IO-Pins.
Wenn du mehr Hilfe brauchst, siehe das Getting started with XIAO-ESP32-S3-Sense Tutorial.
SD-Karte für Audioaufnahme formatieren
Bevor wir mit dem Sammeln von Audiodaten für das Training unseres TinyML-Modells beginnen, müssen wir sicherstellen, dass die SD-Karte korrekt formatiert ist.
Stecke die SD-Karte in den SD-Kartenleser, der entweder in deinem Computer eingebaut ist oder benutze den externen SD-Kartenleser (unter Benötigte Teile aufgeführt).
Öffne dann den Explorer (unter Windows), suche das neue USB-Laufwerk und mache einen Rechtsklick, um das Menü für das Laufwerk zu öffnen. Wähle dort „Weitere Optionen anzeigen“ und dann „Formatieren…“, um den Formatierungsdialog zu öffnen:

Überprüfe, ob das Dateisystem auf „FAT32“ eingestellt ist, und drücke „Start“.Achte darauf, dass du das richtige USB-Laufwerk ausgewählt hast, da das Formatieren alle vorhandenen Daten auf diesem Laufwerk löscht!
Ich gebe dem Laufwerk meist zuerst einen neuen Namen, z.B. „SAMPLES“, um sicherzugehen, dass ich das richtige Laufwerk formatiere und nicht versehentlich ein anderes.
Audiodaten mit XIAO-ESP32-S3-Sense sammeln
Als Nächstes schreiben wir den Code, um die Audiodaten zu sammeln, die wir für das Training unseres TinyML-Modells zur Sprachsteuerung benötigen. Stelle sicher, dass du die SD-Karte korrekt formatiert und in das XIAO-ESP32-S3-Sense eingesetzt hast, wie unten gezeigt:

Wir wollen unsere Stimme nutzen, um eine rote, gelbe oder grüne LED einzuschalten. Die Steuerwörter sind daher „rot“, „gelb“ und „grün“, aber du kannst auch andere Wörter in jeder Sprache wählen.

Du könntest die Audiodaten mit deinem Handy oder dem Mikrofon deines Computers aufnehmen, aber die Eigenschaften (Klang) dieser Mikrofone unterscheiden sich vom Mikrofon des XIAO-ESP32-S3-Sense. Es ist daher am besten, das Mikrofon des XIAO-ESP32-S3-Sense zu verwenden, was aber etwas Code erfordert, um Audiodaten aufzunehmen und auf der SD-Karte zu speichern.
Der folgende Code sammelt Audiodaten. Du musst das Label („rot“, „gelb“ oder „grün“) im Serial Monitor eingeben und Enter drücken.

Das startet eine einsekündige Aufnahme, in der du das Steuerwort (z.B. „rot“) sprichst. Die Aufnahme wird dann als Audiodatei auf der SD-Karte gespeichert. Schau dir zuerst den kompletten Code an, danach besprechen wir die Details:
#include "ESP_I2S.h"
#include "FS.h"
#include "SD.h"
I2SClass i2s;
void scaleVolume(int16_t* audioData, size_t sampleCount) {
const float gain = 16.0;
for (size_t i = 0; i < sampleCount; i++) {
audioData[i] = (int16_t)constrain(audioData[i] * gain, INT16_MIN, INT16_MAX);
}
}
void setup() {
Serial.begin(115200);
while (!Serial) {
delay(10);
}
i2s.setPinsPdmRx(42, 41);
if (!i2s.begin(I2S_MODE_PDM_RX, 16000, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO)) {
Serial.println("Can't find microphone!");
}
if (!SD.begin(21)) {
Serial.println("Failed to mount SD Card!");
}
Serial.println("Enter a label and press Enter to record 1 second of audio:");
}
void loop() {
static int cnt = 1;
static char filename[64];
static String label = "audio";
if (Serial.available() > 0) {
String entered = Serial.readStringUntil('\n');
entered.trim();
if (entered.length() > 0) {
label = entered;
cnt = 1;
String folder = "/" + label;
if (!SD.exists(folder)) {
SD.mkdir(folder);
}
}
sprintf(filename, "/%s/%s_%d.wav", label.c_str(), label.c_str(), cnt++);
Serial.printf("Recording to: %s\n", filename);
uint8_t* wav_buffer;
size_t wav_size;
wav_buffer = i2s.recordWAV(1, &wav_size);
if (wav_size > 44) {
size_t sampleCount = (wav_size - 44) / 2; // 16-bit samples = 2 bytes per sample
scaleVolume((int16_t*)(wav_buffer + 44), sampleCount);
}
File file = SD.open(filename, FILE_WRITE);
if (!file) {
Serial.println("Failed to open file for writing!");
return;
}
if (file.write(wav_buffer, wav_size) != wav_size) {
Serial.println("Failed to write audio data to file!");
return;
}
file.close();
Serial.println("done.");
}
delay(100);
}
Der erste Schritt dieses Programms ist das Einbinden der benötigten Bibliotheken. Diese Bibliotheken ermöglichen es dem ESP32, die I2S-Schnittstelle für den Mikrofoneingang zu nutzen und Dateien auf der SD-Karte zu speichern. Ohne diese könnte der ESP32 keine Audiodaten aufnehmen oder speichern.
#include "ESP_I2S.h" #include "FS.h" #include "SD.h"
Konstanten
Der Code definiert eine Konstante für die Größe des WAV-Dateikopfes. Jede WAV-Datei beginnt mit einem 44-Byte-Header, der Informationen wie Abtastrate, Bit-Tiefe und Kanalanzahl enthält. Durch die Definition von header_sizeweiß der Code, wo die eigentlichen Audiodaten beginnen.
const int header_size = 44;
Objekte
Als Nächstes erstellt das Programm ein I2SClass Objekt. Dieses Objekt übernimmt die gesamte Kommunikation mit dem Mikrofon über das I2S-Protokoll. Durch die Erstellung dieser Instanz kann der ESP32 das Mikrofon initialisieren, seine Parameter einstellen und Audio aufnehmen.
I2SClass i2s;
scaleVolume
Die Funktion scaleVolume()passt die Lautstärke der aufgenommenen Audiodaten an. Sie nimmt rohe Audiosamples, multipliziert sie mit einem Verstärkungsfaktor und begrenzt die Werte, damit sie in den 16-Bit-Audiobereich passen. So wird verhindert, dass der Ton durch Übersteuerung verzerrt.
void scaleVolume(int16_t* audioData, size_t sampleCount) {
const float gain = 16.0;
for (size_t i = 0; i < sampleCount; i++) {
audioData[i] = (int16_t)constrain(audioData[i] * gain, INT16_MIN, INT16_MAX);
}
}
Man könnte die Lautstärke auch normalisieren (typischer Skalierungsfaktor, den ich beobachtet habe, war 20x), aber das funktioniert nicht mit Streaming und verstärkt das Rauschen. Es ist aber gut, eine Vorstellung von einem geeigneten Verstärkungsfaktor zu bekommen, z.B. 8x oder 16x.
void normalizeVolume(int16_t* audioData, size_t sampleCount) {
float maxAmplitude = 0;
for (size_t i = 0; i < sampleCount; i++) {
int16_t absValue = abs(audioData[i]);
if (absValue > maxAmplitude) {
maxAmplitude = absValue;
}
}
float scaleFactor = (float)INT16_MAX / (maxAmplitude + 1);
Serial.printf("scaleFactor %f\n", scaleFactor);
for (size_t i = 0; i < sampleCount; i++) {
float scaledSample = (float)audioData[i] * scaleFactor;
audioData[i] = (int16_t)constrain(scaledSample, INT16_MIN, INT16_MAX);
}
}
setup
Die setup() Funktion bereitet alles vor, bevor die Hauptschleife läuft. Zuerst initialisiert sie den Serial Monitor für Debugging. Dann konfiguriert sie die Mikrofonpins mit i2s.setPinsPdmRx(42, 41) und startet die I2S-Schnittstelle mit einer Abtastrate von 16 kHz im Mono-Modus. Wenn kein Mikrofon erkannt wird, wird eine Fehlermeldung ausgegeben. Danach wird die SD-Karte an Pin 21 initialisiert. Wenn das Mounten der SD-Karte fehlschlägt, erscheint eine weitere Fehlermeldung. Schließlich fordert der ESP32 den Benutzer auf, ein Label über den Serial Monitor einzugeben.
void setup() {
Serial.begin(115200);
while (!Serial) {
delay(10);
}
i2s.setPinsPdmRx(42, 41);
if (!i2s.begin(I2S_MODE_PDM_RX, 16000, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO)) {
Serial.println("Can't find microphone!");
}
if (!SD.begin(21)) {
Serial.println("Failed to mount SD Card!");
}
Serial.println("Enter a label and press Enter to record 1 second of audio:");
}
loop
In der loop() wartet das Programm auf Benutzereingaben im Serial Monitor. Der Benutzer kann ein Label eingeben, z.B. „rot“ oder „rauschen“. Wenn ein Label eingegeben wird, erstellt der Code einen Ordner auf der SD-Karte mit diesem Namen. Diese Organisation erleichtert die Trennung der Aufnahmen und vereinfacht den Upload zur Edge Impulse Plattform.
if (Serial.available() > 0) {
String entered = Serial.readStringUntil('\n');
entered.trim();
if (entered.length() > 0) {
label = entered;
cnt = 1;
String folder = "/" + label;
if (!SD.exists(folder)) {
SD.mkdir(folder);
}
}
Nach der Label-Eingabe generiert das Programm einen Dateinamen für die neue Aufnahme. Es verwendet das Format label/label_number.wav. Zum Beispiel heißt die erste Datei für das Label „rot“ red/red_1.wav. Jede neue Aufnahme erhöht den Zähler, sodass die Dateien in der Reihenfolge gespeichert werden, ohne überschrieben zu werden.
sprintf(filename, "/%s/%s_%d.wav", label.c_str(), label.c_str(), cnt++);
Serial.printf("Recording to: %s\n", filename);
Um Audio aufzunehmen, ruft der Code i2s.recordWAV(1, &wav_size) auf. Diese Funktion nimmt eine Sekunde Audio auf und speichert sie im Speicher als WAV-Datei. Wenn die Aufnahme gültige Audiodaten enthält, wird die scaleVolume() Funktion angewendet, um die Lautstärke der Audiosamples zu erhöhen.
wav_buffer = i2s.recordWAV(1, &wav_size);
if (wav_size > header_size) {
size_t sampleCount = (wav_size - header_size) / 2;
scaleVolume((int16_t*)(wav_buffer + header_size), sampleCount);
}
Nach der Verarbeitung öffnet das Programm eine Datei auf der SD-Karte und schreibt die WAV-Daten hinein. Wenn die Datei nicht geöffnet oder beschrieben werden kann, wird eine Fehlermeldung ausgegeben. Nach dem Speichern wird die Datei geschlossen und eine Bestätigung ausgegeben.
File file = SD.open(filename, FILE_WRITE);
if (!file) {
Serial.println("Failed to open file for writing!");
return;
}
if (file.write(wav_buffer, wav_size) != wav_size) {
Serial.println("Failed to write audio data to file!");
return;
}
file.close();
Serial.println("done.");
Zum Schluss wartet die Schleife 100 Millisekunden, bevor sie erneut auf Benutzereingaben prüft. Diese kleine Verzögerung verhindert, dass der ESP32 die serielle Eingabe überlastet.
delay(100);
Dieses Projekt verwandelt deinen ESP32 in einen praktischen Audiorekorder, der einsekündige Clips von einem I2S-Mikrofon aufnimmt und als WAV-Dateien auf einer SD-Karte speichert. Mit beschrifteten Ordnern ist es besonders nützlich, um Datensätze für Machine-Learning-Projekte wie Spracherkennung zu erstellen.
Code zum Sammeln von Samples ausführen
Flashe dein XIAO-ESP32-S3-Sense mit dem Code und öffne den Serial Monitor. Gib das Label für die Klasse ein, für die du Samples sammeln möchtest, z.B. „gelb“, und drücke Enter.

Der Serial Monitor zeigt den Dateinamen für das Audiosample an, z.B. „/gelb/gelb_1.wav“, und du hast nun eine Sekunde Zeit, das Wort „gelb“ zu sprechen. Sobald du fertig bist, wird „done.“ im Serial Monitor angezeigt. Unten ein Beispiel für eine aufgenommene Audiodatei für das Wort „gelb“:
Für weitere Aufnahmen derselben Klasse, z.B. „gelb“, drücke einfach Enter. Gib das Label nicht erneut ein. Wenn du bereit bist, Samples für die nächste Klasse aufzunehmen, z.B. „rot“, gib „rot“ ein, drücke Enter und fahre auf die gleiche Weise fort.
Der Code erstellt für jede Klasse („rot“, „grün“, „gelb“, „rauschen“) einen neuen Ordner und speichert die Audiodaten darin:

Ich habe 50 Samples pro Farbe und 100 Samples für die Klasse „rauschen“ aufgenommen. Für die Klasse „rauschen“: nimm Stille, Umgebungsgeräusche, Musik, Sprache auf – alles außer den Wörtern „rot“, „grün“ oder „gelb“. Je mehr Samples du hast, desto besser, aber mit 50 Samples erreichst du schon eine ordentliche Erkennungsgenauigkeit – nicht großartig, aber solide ; )
Trainieren des Sprachsteuerungsmodells mit Edge Impulse
In diesem Abschnitt gehen wir die notwendigen Schritte durch, um unsere Trainingsdaten hochzuladen, die Datenvorverarbeitung und das Modell-Pipeline (Impulse) zu erstellen und unser TinyML-Modell zu trainieren.
Erstelle ein neues Edge Impulse Projekt, zum Beispiel mit dem Namen „Voice Control“, und dann sind wir bereit, unsere Trainingsdaten hochzuladen.
Trainingsdaten hochladen
Entferne zuerst die SD-Karte aus dem XIAO-ESP32-S3-Sense und verbinde sie mit deinem Computer, damit wir die Daten hochladen können. Sie erscheint als USB-Laufwerk, wenn du einen externen SD-Kartenleser benutzt.
Klicke dann auf „Data acquisition“ -> „+ Add data“ -> „Upload data“, um den Dialog zum Hochladen der Daten zu öffnen:

Im Dialog aktiviere „Select a folder“ und gib unten das Label für die Klasse ein, für die du Daten hochladen möchtest, z.B. „rot“:

Klicke nun auf „Choose files“ und wähle den Ordner „rot“ mit deinen Audiodaten aus. Klicke dann auf „Upload data“ und du solltest sehen, wie die Daten hochgeladen werden:

Wiederhole diesen Vorgang für die anderen Farben und die Klasse „rauschen“. Achte darauf, vor dem Hochladen das richtige Label unter „Enter label“ einzugeben. Du kannst später versehentlich falsch gelabelte Samples löschen.
Wenn du fertig bist, schließe den Dialog durch Klicken auf das x oben rechts:

Daten erkunden
Du solltest dann den Datensatz im Data Explorer sehen. Oben links siehst du ein Tortendiagramm, das die Verteilung deiner Klassen und die Train/Test-Aufteilung zeigt. Darunter ist der Datensatz, in dem du einzelne Samples anklicken und anhören kannst:

Wenn du Fehler in deinen Daten findest, gibt es auch Funktionen zum Löschen von Samples. Wenn du zufrieden bist, ist der nächste Schritt, ein Impulse (Datenvorverarbeitung + Modell) zu erstellen.
Impulse erstellen
Klicke unter „Impulse design“ auf „Create impulse“:

Füge dann drei Blöcke hinzu: „Time series data“, „Audio (MFE)“ und „Transfer Learning (Keyword Spotting)“:

Behalte alle Standardeinstellungen der Blöcke bei. In den nächsten Abschnitten konfigurieren und trainieren wir die einzelnen Blöcke.
MFE
Klicke unter „Impulse design“ auf „MFE“:

Das öffnet den Einstellungsdialog für den MFE-Block:

MFE steht für Mel Frequency Energy und ist eine Methode der digitalen Signalverarbeitung, um Merkmale aus einem Audiosignal zu extrahieren. Wir belassen die Einstellungen für MFE wie sie sind. Drücke einfach „Save parameters“, um sie zu speichern.
Im Feature Explorer kannst du dann sehen, wie gut diese extrahierten Merkmale funktionieren:

Jeder Punkt im obigen Diagramm repräsentiert eines unserer Audiosamples (ein gesprochenes Wort oder Rauschen). Idealerweise sollten Punkte derselben Klasse eng beieinander liegen und die Gruppen klar getrennt sein.
Das Diagramm zeigt eine gewisse Gruppierung, aber nicht optimal. Man sieht, dass die Gruppen für die Farbwörter („rot“, „grün“, „gelb“) etwas auseinandergezogen sind. Wahrscheinlich, weil ich beim Sprechen den Abstand zum Mikrofon variiert habe.
Die Klasse „rauschen“ hingegen ist kreisförmig, da die meisten Rauschsamples in der Lautstärke ähnlich, aber im Inhalt unterschiedlich sind.
Aufgrund dieser Clusterbildung würde ich keine fantastische Erkennungsgenauigkeit erwarten. Du kannst mit den Einstellungen des MFE-Blocks experimentieren und mehr Samples sammeln, was das Modell robuster machen sollte.
Transfer Learning
Um das Modell zu trainieren, klicke unter „Impulse design“ auf „Transfer learning“:

Das öffnet den Einstellungsdialog für das neuronale Netzwerk, wie unten gezeigt:

Du kannst zwischen zwei Modellen wählen: MobileNetV1 0.1 oder MobileNetV2 0.35. Das zweite ist wahrscheinlich genauer, aber auch viel größer, und ich konnte es nicht auf dem XIAO-ESP32-S3-Sense zum Laufen bringen. Deshalb habe ich das MobileNetV1 0.1 Modell gewählt.

Ich habe die Anzahl der Trainingszyklen auf 60 erhöht, die Lernrate auf 0,01 gesetzt und die CPU als Trainingsprozessor belassen. Keiner dieser Parameter ist jedoch besonders sensibel oder entscheidend für die finale Genauigkeit, und die Standardeinstellungen funktionieren in der Regel gut.
Klicke dann auf „Save & Train“, um das Modell zu trainieren. Am Ende des Trainingsprozesses siehst du eine Konfusionsmatrix und weitere Metriken zur Netzwerkleistung:

Wie du siehst, lag die Gesamtgenauigkeit bei mir bei 95 % und das Modell neigte dazu, Farbwörter mit Rauschen zu verwechseln, was zu erwarten ist. Die Genauigkeit könnte mit mehr Trainingsdaten verbessert werden, aber das Modell ist sehr klein und seine Erkennungsfähigkeit daher begrenzt.
Modell bereitstellen
Zum Schluss müssen wir das Modell bereitstellen, um es auf dem XIAO-ESP32-S3-Sense auszuführen. Klicke unter „Impulse design“ auf „Deployment“:

Oben rechts auf dem Bildschirm kannst du den Zielprozessor für die Bereitstellung auswählen. Stand August 2025 unterstützt Edge Impulse den XIAO-ESP32-S3-Sense nicht direkt. Stattdessen wählen wir den Espressif ESP-EYE:

Wenn du darauf klickst, öffnet sich der Konfigurationsdialog für das Zielgerät:

Später passen wir den bereitgestellten Code an, damit er mit dem XIAO-ESP32-S3-Sense funktioniert. Für den Bereitstellungstyp wählen wir „Arduino library“ und „TensorFlow Lite“:

Klicke dann auf den Build-Button, um das Modell zu bauen und bereitzustellen:

Es wird das Modell als ZIP-Datei herunterladen, in meinem Fall: „ei-voice-control-arduino-1.0.8.zip„. Der Name der ZIP-Datei hängt vom Namen deines Edge Impulse Projekts („Voice Control“) ab.

Um die Bibliothek zu verwenden, erstelle einen neuen, leeren Sketch und installiere dann die heruntergeladene Bibliothek (ei-voice-...zip) wie gewohnt in der Arduino IDE über Sketch -> Include Library -> Add .ZIP Library.
LEDs an XIAO-ESP32-S3-Sense anschließen
Bevor wir den Code schreiben, um die LEDs zu steuern, zeige ich dir kurz, wie du die drei LEDs an das XIAO-ESP32-S3-Sense anschließt.

Die Kathode aller drei LEDs ist mit Masse (GND) verbunden. Die Anoden der LEDs sind über einen 220Ω Widerstand mit den GPIO-Pins 1, 2 und 3 verbunden, wie oben gezeigt. Du kannst auch andere GPIO-Pins wählen, musst dann aber den folgenden Code entsprechend anpassen.
Code für Sprachsteuerung der LEDs
Die Bibliothek ei-voice-control-arduino-1.0.8.zip, die wir bereitgestellt haben, enthält Beispielcode, den wir aber nicht verwenden können, da er für den ESP-EYE gedacht ist, der eine andere Mikrofon-Schnittstelle als das XIAO-ESP32-S3-Sense hat.
Ich habe daher den Beispielcode angepasst, damit er mit dem XIAO-ESP32-S3-Sense funktioniert, und den Code zum Schalten der LEDs hinzugefügt.
Der folgende Code hört auf das Mikrofon, sendet das aufgenommene Audiosignal an das Klassifizierungsmodell, erhält das Klassifizierungsergebnis und lässt kurz die rote, gelbe oder grüne LED aufleuchten, wenn ein Farbwörter mit ausreichender Sicherheit erkannt wurde.
Schau dir zuerst den kompletten Code an, danach besprechen wir die Details.Achte darauf, den Import #include "Voice_control_inferencing.h", mit dem Namen deiner bereitgestellten Bibliothek zu ersetzen.!
#include "Voice_control_inferencing.h"
#include "ESP_I2S.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
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;
static bool record_status = true;
static const int8_t I2S_CLK = 42;
static const int8_t I2S_DIN = 41;
static const uint32_t SAMPLERATE = 16000;
static const int8_t redPin = 1;
static const int8_t yellowPin = 2;
static const int8_t greenPin = 3;
I2SClass I2S;
static void audio_inference_callback(uint32_t n_samples) {
for (uint32_t i = 0; i < n_samples; 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 float gain = 16.0;
const uint32_t n_samples_to_read = (uint32_t)arg;
while (record_status) {
for (uint32_t i = 0; i < n_samples_to_read; i++) {
int16_t sample = I2S.read();
sampleBuffer[i] = (int16_t)constrain(sample * gain, INT16_MIN, INT16_MAX);
}
if (record_status) {
audio_inference_callback(n_samples_to_read);
}
}
vTaskDelete(NULL);
}
static bool microphone_inference_start(uint32_t n_samples) {
inference.buffer = (int16_t *)malloc(n_samples * sizeof(int16_t));
if (!inference.buffer) return false;
inference.buf_count = 0;
inference.n_samples = n_samples;
inference.buf_ready = 0;
I2S.setPinsPdmRx(I2S_CLK, I2S_DIN);
if (!I2S.begin(I2S_MODE_PDM_RX, SAMPLERATE, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO)) {
ei_printf("Can't find microphone!\r\n");
return false;
}
record_status = true;
xTaskCreate(capture_samples, "CaptureSamples", 1024 * 32, (void *)sample_buffer_size, 10, NULL);
return true;
}
static bool microphone_inference_record(void) {
while (inference.buf_ready == 0) delay(10);
inference.buf_ready = 0;
return true;
}
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;
}
static void microphone_inference_end(void) {
I2S.end();
free(inference.buffer);
}
void initLEDs() {
pinMode(redPin, OUTPUT);
pinMode(yellowPin, OUTPUT);
pinMode(greenPin, OUTPUT);
}
void switchOffLEDs() {
digitalWrite(redPin, LOW);
digitalWrite(yellowPin, LOW);
digitalWrite(greenPin, LOW);
}
void flashLED(const char* label) {
switchOffLEDs();
if(!strcmp("red", label)) {
digitalWrite(redPin, HIGH);
}
if(!strcmp("yellow", label)) {
digitalWrite(yellowPin, HIGH);
}
if(!strcmp("green", label)) {
digitalWrite(greenPin, HIGH);
}
delay(500);
switchOffLEDs();
}
void setup() {
Serial.begin(115200);
initLEDs();
if (!microphone_inference_start(EI_CLASSIFIER_RAW_SAMPLE_COUNT)) {
ei_printf("ERR: Could not allocate audio buffer\r\n");
return;
}
ei_printf("Listening...\n");
}
void loop() {
if (!microphone_inference_record()) {
ei_printf("ERR: Failed to record audio...\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("ERR: Failed to run classifier (%d)\n", r);
return;
}
float max_val = -1.0;
int max_idx = -1;
auto cl = result.classification;
for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
if (cl[ix].value > max_val && strcmp("noise", cl[ix].label)) {
max_val = cl[ix].value;
max_idx = ix;
}
}
if (max_idx >= 0 && max_val > 0.3) {
ei_printf("Predicted label: %s (%.3f)\n", cl[max_idx].label, max_val);
flashLED(cl[max_idx].label);
}
}
Imports
Die ersten Zeilen binden die benötigten Bibliotheken ein. Jede spielt eine wichtige Rolle bei der Spracherkennung und der Hardwaresteuerung.
#include "Voice_control_inferencing.h" #include "ESP_I2S.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h"
Die Voice_control_inferencing.h Datei wird von Edge Impulse generiert und enthält das trainierte neuronale Netzwerk. Wie bereits erwähnt, hängt der Name von deinem Edge Impulse Projekt und der bereitgestellten Bibliothek ab. Du musst ihn eventuell anpassen.
Die ESP_I2S.h Bibliothek ermöglicht den Zugriff auf die I2S-Mikrofon-Schnittstelle, und die FreeRTOS-Header erlauben es uns, die Audioaufnahme als Hintergrundtask laufen zu lassen.
Konstanten und Puffer
Als Nächstes definieren wir Strukturen und Konstanten, um die Audioaufnahme und Klassifizierung zu verwalten.
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;
static bool record_status = true;
Die inference_t Struktur hält den Audiopuffer und verfolgt, wie viele Samples wir haben. Wir reservieren sampleBuffer als temporären Speicher für den Mikrofoneingang. Die Flagge record_status steuert, ob wir weiter aufnehmen.
Mikrofon-Pins und Abtastrate
Wir definieren, welche ESP32-Pins mit dem Mikrofon verbunden sind und wie schnell wir abtasten.
static const int8_t I2S_CLK = 42; static const int8_t I2S_DIN = 41; static const uint32_t SAMPLERATE = 16000;
Das Mikrofon ist über I2S angeschlossen. Pin 42 liefert das Taktsignal, Pin 41 ist der Dateneingang. Wir sampeln mit 16 kHz, was ideal für Spracherkennung ist.
LED-Pins
Wir vergeben drei Pins für die visuelle Rückmeldung.
static const int8_t redPin = 1; static const int8_t yellowPin = 2; static const int8_t greenPin = 3;
Jeder Pin steuert eine LED. Sie blinken, wenn der Klassifizierer das entsprechende Farbwörter erkennt.
Audio-Inferenz-Callback
Diese Funktion verschiebt Samples vom temporären Puffer in den Hauptinferenzpuffer.
static void audio_inference_callback(uint32_t n_samples) {
for (uint32_t i = 0; i < n_samples; i++) {
inference.buffer[inference.buf_count++] = sampleBuffer[i];
if (inference.buf_count >= inference.n_samples) {
inference.buf_count = 0;
inference.buf_ready = 1;
}
}
}
Sobald der Puffer voll ist, markieren wir ihn als bereit, damit der Klassifizierer ihn verwenden kann.
Mikrofon-Samples erfassen
Anstatt Audio in der Hauptschleife aufzunehmen, läuft diese Aufgabe im Hintergrund.
static void capture_samples(void *arg) {
const float gain = 16.0;
const uint32_t n_samples_to_read = (uint32_t)arg;
while (record_status) {
for (uint32_t i = 0; i < n_samples_to_read; i++) {
int16_t sample = I2S.read();
sampleBuffer[i] = (int16_t)constrain(sample * gain, INT16_MIN, INT16_MAX);
}
if (record_status) {
audio_inference_callback(n_samples_to_read);
}
}
vTaskDelete(NULL);
}
Wir lesen kontinuierlich Samples vom Mikrofon mit I2S.read(). Jedes Sample wird mit einem Verstärkungsfaktor multipliziert, bevor es gespeichert wird. Wenn genug Samples gesammelt sind, werden sie zur Inferenz weitergegeben.
Mikrofon-Inferenz starten
Diese Funktion initialisiert das Mikrofon und startet die Aufnahmeaufgabe.
static bool microphone_inference_start(uint32_t n_samples) {
inference.buffer = (int16_t *)malloc(n_samples * sizeof(int16_t));
if (!inference.buffer) return false;
inference.buf_count = 0;
inference.n_samples = n_samples;
inference.buf_ready = 0;
I2S.setPinsPdmRx(I2S_CLK, I2S_DIN);
if (!I2S.begin(I2S_MODE_PDM_RX, SAMPLERATE, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO)) {
ei_printf("Can't find microphone!\r\n");
return false;
}
record_status = true;
xTaskCreate(capture_samples, "CaptureSamples", 1024 * 32, (void *)sample_buffer_size, 10, NULL);
return true;
}
Wir reservieren Speicher für den Inferenzpuffer und konfigurieren I2S für PDM-Mikrofone. Wenn alles klappt, startet eine FreeRTOS-Task, die im Hintergrund Samples aufnimmt.
Auf neues Audio warten
Diese Funktion wartet, bis der Puffer mit neuen Daten gefüllt ist.
static bool microphone_inference_record(void) {
while (inference.buf_ready == 0) delay(10);
inference.buf_ready = 0;
return true;
}
Der Code pausiert, bis genug Audio für die Klassifizierung gesammelt wurde.
Audio-Daten konvertieren
Der Edge Impulse Klassifizierer erwartet Audio-Samples als Fließkommazahlen. Diese Funktion wandelt rohe 16-Bit-Integer in Floats um.
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;
}
Mikrofon stoppen
Wenn wir keinen Audioeingang mehr brauchen, stoppen wir I2S und geben den Speicher frei.
static void microphone_inference_end(void) {
I2S.end();
free(inference.buffer);
}
LED-Steuerfunktionen
Als Nächstes bereiten wir Hilfsfunktionen vor, um die LEDs zu steuern.
void initLEDs() {
pinMode(redPin, OUTPUT);
pinMode(yellowPin, OUTPUT);
pinMode(greenPin, OUTPUT);
}
void switchOffLEDs() {
digitalWrite(redPin, LOW);
digitalWrite(yellowPin, LOW);
digitalWrite(greenPin, LOW);
}
void flashLED(const char* label) {
switchOffLEDs();
if(!strcmp("red", label)) {
digitalWrite(redPin, HIGH);
}
if(!strcmp("yellow", label)) {
digitalWrite(yellowPin, HIGH);
}
if(!strcmp("green", label)) {
digitalWrite(greenPin, HIGH);
}
delay(500);
switchOffLEDs();
}
initLEDs() konfiguriert die Pins als Ausgänge.switchOffLEDs() sorgt dafür, dass keine LED an bleibt.flashLED() schaltet die LED ein, die zum erkannten Label passt, und schaltet sie nach einer halben Sekunde wieder aus.
Setup
In der Setup-Funktion initialisieren wir den Serial Monitor, die LEDs und das Mikrofon.
void setup() {
Serial.begin(115200);
initLEDs();
if (!microphone_inference_start(EI_CLASSIFIER_RAW_SAMPLE_COUNT)) {
ei_printf("ERR: Could not allocate audio buffer\r\n");
return;
}
ei_printf("Listening...\n");
}
Wenn das Mikrofon erfolgreich startet, sind wir bereit, Spracheingaben zu erfassen.
Loop
Die Loop läuft kontinuierlich, um Audio zu verarbeiten und gesprochene Befehle zu klassifizieren.
void loop() {
if (!microphone_inference_record()) {
ei_printf("ERR: Failed to record audio...\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("ERR: Failed to run classifier (%d)\n", r);
return;
}
float max_val = -1.0;
int max_idx = -1;
auto cl = result.classification;
for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
if (cl[ix].value > max_val && strcmp("noise", cl[ix].label)) {
max_val = cl[ix].value;
max_idx = ix;
}
}
if (max_idx >= 0 && max_val > 0.3) {
ei_printf("Predicted label: %s (%.3f)\n", cl[max_idx].label, max_val);
flashLED(cl[max_idx].label);
}
}
Wir warten auf einen neuen Audiopuffer, bereiten ein Signalobjekt vor und führen den Klassifizierer aus. Der Klassifizierer gibt Labels mit Wahrscheinlichkeiten aus. Wir ignorieren das Label "noise" und wählen die Klasse mit der höchsten Wahrscheinlichkeit. Wenn die Wahrscheinlichkeit größer als 0,3 ist, blinken die LEDs passend zur Vorhersage.
Dieser Code funktioniert gut, reagiert aber etwas langsam auf Sprachbefehle, da wir warten müssen, bis ein komplettes Inferenzfenster gefüllt ist. Der folgende Streaming-Ansatz ist schneller, allerdings ist die Klassifikationsgenauigkeit etwas geringer.
Streaming-Code für Sprachsteuerung
Der kontinuierliche Klassifizierer im folgenden Code ist darauf ausgelegt, gleitende Fenster von Audio zu verarbeiten. Er verarbeitet überlappende Ausschnitte des Eingangs, sodass wir die Inferenz nicht jedes Mal stoppen und neu starten müssen. Das macht das System reaktionsschneller auf Schlüsselwörter (allerdings leidet die Genauigkeit etwas). Siehe die folgende Demo:
Schau dir zuerst den kompletten Code an, danach gehen wir auf die Unterschiede zum vorherigen Code ein.
#define EIDSP_QUANTIZE_FILTERBANK 0
#include "Voice_control_inferencing.h"
#include "ESP_I2S.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
I2SClass I2S;
static const uint32_t slice_size = EI_CLASSIFIER_SLICE_SIZE;
static signed short sampleBuffer[slice_size];
static bool debug_nn = false;
static int slice_ctn = 0;
static bool record_status = true;
static const int8_t I2S_CLK = 42;
static const int8_t I2S_DIN = 41;
static const uint32_t SAMPLERATE = 16000;
static const int8_t redPin = 1;
static const int8_t yellowPin = 2;
static const int8_t greenPin = 3;
static void capture_samples(void* arg) {
const float gain = 16.0;
while (record_status) {
for (uint32_t i = 0; i < slice_size; i++) {
int16_t sample = I2S.read();
sampleBuffer[i] = (int16_t)constrain(sample * gain, INT16_MIN, INT16_MAX);
}
vTaskDelay(1); // give some breathing room for the scheduler
}
vTaskDelete(NULL);
}
static bool microphone_inference_start(uint32_t n_samples) {
I2S.setPinsPdmRx(I2S_CLK, I2S_DIN);
if (!I2S.begin(I2S_MODE_PDM_RX, SAMPLERATE, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO)) {
ei_printf("Can't find microphone!\r\n");
return false;
}
record_status = true;
xTaskCreate(capture_samples, "CaptureSamples", 1024 * 16, NULL, 10, NULL);
return true;
}
static bool microphone_inference_record(void) {
delay(1);
return true;
}
static int microphone_audio_signal_get_data(size_t offset, size_t length, float* out_ptr) {
numpy::int16_to_float(&sampleBuffer[offset], out_ptr, length);
return 0;
}
static void microphone_inference_end(void) {
I2S.end();
}
void initLEDs() {
pinMode(redPin, OUTPUT);
pinMode(yellowPin, OUTPUT);
pinMode(greenPin, OUTPUT);
}
void switchOffLEDs() {
digitalWrite(redPin, LOW);
digitalWrite(yellowPin, LOW);
digitalWrite(greenPin, LOW);
}
void flashLED(const char* label) {
switchOffLEDs();
if (!strcmp("red", label)) {
digitalWrite(redPin, HIGH);
}
if (!strcmp("yellow", label)) {
digitalWrite(yellowPin, HIGH);
}
if (!strcmp("green", label)) {
digitalWrite(greenPin, HIGH);
}
delay(500);
switchOffLEDs();
}
void setup() {
Serial.begin(115200);
initLEDs();
run_classifier_init();
ei_sleep(1000);
if (!microphone_inference_start(slice_size)) {
ei_printf("ERR: Could not start microphone\r\n");
return;
}
ei_printf("Listening continously...\n");
}
void loop() {
if (!microphone_inference_record()) {
ei_printf("ERR: Failed to record audio...\n");
return;
}
signal_t signal;
signal.total_length = slice_size;
signal.get_data = µphone_audio_signal_get_data;
ei_impulse_result_t result = { 0 };
EI_IMPULSE_ERROR r = run_classifier_continuous(&signal, &result, debug_nn);
if (r != EI_IMPULSE_OK) {
ei_printf("ERR: Failed to run classifier (%d)\n", r);
return;
}
float max_val = -1.0;
int max_idx = -1;
auto cl = result.classification;
for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
if (cl[ix].value > max_val && strcmp("noise", cl[ix].label)) {
max_val = cl[ix].value;
max_idx = ix;
}
}
slice_ctn = max(--slice_ctn, 0);
if (max_idx >= 0 && max_val > 0.5 && slice_ctn <= 0) {
ei_printf("Predicted label: %s (%.3f)\n", cl[max_idx].label, max_val);
flashLED(cl[max_idx].label);
slice_ctn = EI_CLASSIFIER_SLICES_PER_MODEL_WINDOW;
}
}
Präprozessor-Direktive
Ganz oben führt dieser Code eine neue Definition ein:
#define EIDSP_QUANTIZE_FILTERBANK 0
Diese deaktiviert die Filterbank-Quantisierung in der Edge Impulse DSP-Pipeline. Das Modell verwendet volle Genauigkeit statt komprimierter Werte. Die erste Version enthielt diese Zeile nicht und nutzte die Standardquantisierung.
Imports
Die Imports sind wie zuvor: Edge Impulse Inferenzcode, I2S-Unterstützung und FreeRTOS für Multitasking. Wie zuvor hängt der Name der Voice_control_inferencing.h Bibliothek von deinem Edge Impulse Projekt und der bereitgestellten Bibliothek ab. Du musst ihn eventuell anpassen.
#include "Voice_control_inferencing.h" #include "ESP_I2S.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h"
Puffer und Variablen
Anstatt einen großen Inferenzpuffer zu erstellen, arbeitet diese Version mit Audio-Scheiben.
static const uint32_t slice_size = EI_CLASSIFIER_SLICE_SIZE; static signed short sampleBuffer[slice_size]; static int slice_ctn = 0;
Im vorherigen Code hielt der Puffer ein komplettes Sample-Fenster (EI_CLASSIFIER_RAW_SAMPLE_COUNT). Hier wird jeweils nur eine Scheibe verarbeitet. Die Variable slice_ctn hilft, Vorhersagen zu drosseln, damit sie nicht zu häufig ausgelöst werden.
Audioaufnahme-Task
Die Aufnahmeschleife sieht vertraut aus, hat aber eine große Änderung:
static void capture_samples(void* arg) {
const float gain = 16.0;
while (record_status) {
for (uint32_t i = 0; i < slice_size; i++) {
int16_t sample = I2S.read();
sampleBuffer[i] = (int16_t)constrain(sample * gain, INT16_MIN, INT16_MAX);
}
vTaskDelay(1); // give some breathing room for the scheduler
}
vTaskDelete(NULL);
}
Hier füllen wir kontinuierlich nur eine Scheibe statt eines kompletten Fensters. Der vTaskDelay(1) Aufruf erlaubt FreeRTOS, andere Tasks flüssiger zu planen. Die erste Version fütterte Samples mit Callbacks in einen Inferenzpuffer, diese aktualisiert nur den Scheibenpuffer.
Mikrofon-Setup
Beide Versionen konfigurieren das Mikrofon mit I2S, aber diese ist einfacher.
static bool microphone_inference_start(uint32_t n_samples) {
I2S.setPinsPdmRx(I2S_CLK, I2S_DIN);
if (!I2S.begin(I2S_MODE_PDM_RX, SAMPLERATE, I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO)) {
ei_printf("Can't find microphone!\r\n");
return false;
}
record_status = true;
xTaskCreate(capture_samples, "CaptureSamples", 1024 * 16, NULL, 10, NULL);
return true;
}
Beachte, dass es keine dynamische Speicherallokation (malloc) und keine Inferenzpuffer-Einrichtung gibt. Das reduziert die Komplexität und vermeidet Speicherfragmentierung.
Audio aufnehmen
Die Aufnahmefunktion ist jetzt ein Platzhalter:
static bool microphone_inference_record(void) {
delay(1);
return true;
}
In der ersten Version wartete diese Funktion, bis der Puffer bereit war. Hier brauchen wir das nicht. Die Klassifizierung erfolgt Scheibe für Scheibe.
Klassifizierer-Eingang
Die Funktion zur Umwandlung roher Samples in Fließkommazahlen ist in beiden Versionen gleich:
static int microphone_audio_signal_get_data(size_t offset, size_t length, float* out_ptr) {
numpy::int16_to_float(&sampleBuffer[offset], out_ptr, length);
return 0;
}
LEDs
Die LED-Funktionen sind identisch: Sie leuchten die LED des erkannten Labels auf und schalten sie nach einer halben Sekunde wieder aus.
void flashLED(const char* label) { ... }
Setup-Funktion
Die Setup-Funktion hat einige neue Schritte:
run_classifier_init(); ei_sleep(1000);
Der Klassifizierer wird einmalig vor der Nutzung initialisiert, was für die kontinuierliche Klassifizierung erforderlich ist. Die frühere Version brauchte das nicht, da sie nur Einzelinferenz durchführte.
Das Mikrofon startet ebenfalls mit slice_size statt der vollen Rohsample-Anzahl, was Streaming ermöglicht.
Loop-Funktion
Hier liegt der größte Unterschied: Statt run_classifier() wird hier aufgerufen:
EI_IMPULSE_ERROR r = run_classifier_continuous(&signal, &result, debug_nn);
Der kontinuierliche Klassifizierer verarbeitet gleitende Fenster von Audio. Er verarbeitet überlappende Ausschnitte, sodass du die Inferenz nicht stoppen und neu starten musst. Das macht das System reaktionsschneller.
Eine weitere wichtige Änderung ist, wie Vorhersagen gedrosselt werden:
slice_ctn = max(--slice_ctn, 0);
if (max_idx >= 0 && max_val > 0.5 && slice_ctn <= 0) {
ei_printf("Predicted label: %s (%.3f)\n", cl[max_idx].label, max_val);
blinkLED(cl[max_idx].label);
slice_ctn = EI_CLASSIFIER_SLICES_PER_MODEL_WINDOW;
}
Nach der Erkennung eines Labels wartet das Programm ein komplettes Modellfenster, bevor ein neuer Trigger erlaubt wird. So blinkt die LED nicht mehrfach für dasselbe Wort. Der frühere Code hatte keinen solchen Mechanismus und konnte sofort erneut auslösen.
Zusammenfassung der Unterschiede
Die erste Version verwendet einen gepufferten, einmaligen Ansatz:
- Sammelt ein komplettes Inferenzfenster.
- Führt die Klassifizierung einmal pro Puffer aus.
- Muss warten, bis der Puffer bereit ist.
- Verwendet
run_classifier().
Die zweite Version verwendet einen Streaming-, kontinuierlichen Ansatz:
- Arbeitet mit kleinen Audioscheiben.
- Führt die Klassifizierung kontinuierlich mit Überlappung aus.
- Kein Warten auf Pufferbereitschaft.
- Verwendet
run_classifier_continuous(). - Fügt eine Abkühlzeit (
slice_ctn) hinzu, um wiederholte Auslösungen zu vermeiden.
Fazit und Kommentare
In diesem Tutorial hast du gelernt, wie man eine Sprachsteuerungsanwendung mit dem XIAO-ESP32-S3-Sense Board und der Edge Impulse Plattform erstellt. Edge Impulse bietet noch viel mehr als hier gezeigt, und ich empfehle dir, das Edge Impulse Documentation zu lesen.
Das Beispiel in diesem Tutorial ermöglichte es dir, drei LEDs mit deiner Stimme zu steuern. Du kannst aber auch leistungsfähigere Steueranwendungen bauen, indem du eine Wortfolge und eine Zustandsmaschine verwendest. Zum Beispiel ein Weckwort wie „Jarvis“, gefolgt von einem Ort „Schreibtisch“, „Ecke“, „Regal“, gefolgt von einem Gerät „Licht“ oder „Ventilator“, gefolgt von einer Aktion „An“ oder „Aus“:
[Jarvis] -> [Desk, Corner, Shelf] -> [Light, Fan] -> [On, Off]
Mit acht verschiedenen Wörtern kannst du bereits 1x3x2x2 = 12 verschiedene Aktionen steuern, z.B. „Jarvis Schreibtisch Licht An“
Abgesehen von der Hausautomation gibt es viele weitere interessante Anwendungen der Klangklassifikation. Denk an Umweltgeräusche, Vibrations-/Geräusch-Anomalieerkennung, Vogelgesangsklassifikation und so weiter.
Wenn du Gesichter oder Personen erkennen möchtest, schau dir unsere Face Detection with XIAO ESP32-S3-Sense and SenseCraft AI und Edge AI Room Occupancy Sensor with ESP32 and Person Detection Tutorials an.
Wenn du Fragen hast, hinterlasse sie gerne im Kommentarbereich.
Viel Spaß beim Tüfteln 😉

