Dans ce tutoriel, vous apprendrez à créer une application de contrôle vocal avec la carte XIAO-ESP32-S3-Sense et la plateforme Edge Impulse. Nous commencerons par collecter des données audio avec le microphone de la XIAO-ESP32-S3-Sense. Ensuite, nous téléchargerons ces données audio sur la plateforme Edge Impulse pour entraîner un modèle de reconnaissance de mots-clés. Enfin, nous déploierons ce modèle TinyML sur la XIAO-ESP32-S3-Sense, ce qui nous permettra de contrôler des LEDs par la voix.
Le modèle TinyML sera entraîné pour reconnaître les mots « red », « yellow » et « green ». Nous connecterons trois LEDs (rouge, jaune, verte) à la carte XIAO-ESP32-S3-Sense, que vous pourrez ensuite allumer via des commandes vocales. Découvrez le système en action ci-dessous :
Si vous n’avez jamais utilisé la XIAO-ESP32-S3 Sense auparavant, jetez un œil au Getting started with XIAO-ESP32-S3-Sense tutoriel d’abord, sinon commençons …
Pièces requises
Vous aurez besoin d’une carte XIAO ESP32 S3 Sense pour tester les exemples de code. Notez que la carte peut chauffer et vous souhaiterez peut-être fixer un petit Heatsink à l’arrière (voir la pièce listée ci-dessous).
Ensuite, vous aurez besoin d’une carte SD pour stocker les échantillons audio collectés. J’ai listé une carte de 32 Go, mais une plus petite (8 Go) conviendra également. Enfin, si votre ordinateur ne dispose pas d’un lecteur de carte SD intégré, vous en aurez aussi besoin.

Seeed Studio XIAO ESP32 S3 Sense

Câble USB C

Petit dissipateur thermique 9×9 mm

Carte SD 32 Go

Lecteur de carte SD
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.
La carte XIAO-ESP32-S3-Sense
La carte XIAO-ESP32-S3-Sense est basée sur l’ESP32-S3, une puce d’Espressif conçue pour les tâches d’IA et de calcul en périphérie. La carte se compose de quatre parties : la carte principale, le Sense Hat, la caméra et une antenne Wi-Fi externe :

Le Sense Hat se branche sur la carte principale et comprend un emplacement pour carte SD, un connecteur pour la caméra et un microphone. La photo ci-dessous montre la carte XIAO-ESP32-S3-Sense entièrement assemblée avec une carte SD insérée :

Cependant, pour ce projet, nous n’aurons pas besoin de l’antenne WiFi ni de la caméra. La configuration ci-dessous suffit et vous n’aurez même plus besoin de la carte SD une fois les échantillons audio collectés.

Notez que l’alimentation de la carte peut être fournie soit par le port USB Type-C, soit par l’interface de charge de batterie connectable à une batterie LiPo 3,7 V.
Microphone de la XIAO-ESP32-S3-Sense
La XIAO-ESP32-S3-Sense est équipée d’un microphone numérique intégré MEMS Microphone de type MSM261D3526H1CPM situé sur le Sense Hat. Voir la photo ci-dessous :

Vous pouvez communiquer avec le microphone via deux lignes de signal (PDM_CLK, PDM_DATA) en utilisant I2S protocol connectées aux broches IO42 et IO41 comme indiqué dans le schéma ci-dessous :

Pour plus d’informations détaillées, consultez notre Record Audio with XIAO-ESP32-S3-Sense tutoriel.
Brochage de la carte XIAO-ESP32-S3-Sense
Enfin, jetons un œil au brochage de la carte XIAO-ESP32-S3-Sense :

La broche 5V correspond au 5V du port USB. La broche 3V3 fournit la sortie du régulateur embarqué et peut délivrer jusqu’à 700 mA. La broche GND est la masse.
Concernant les GPIO : la carte offre 11 GPIO numériques/analogiques mais GPIO0, GPIO3, GPIO43 et GPIO44 sont des broches de strap qui doivent être dans un état spécifique au démarrage – soyez donc prudent avec celles-ci. Une fois le microcontrôleur lancé, ces broches fonctionnent comme des IO classiques.
Si vous avez besoin d’aide supplémentaire, consultez le Getting started with XIAO-ESP32-S3-Sense tutoriel.
Formater la carte SD pour la collecte audio
Avant de commencer à collecter des échantillons audio pour entraîner notre modèle TinyML, il faut s’assurer que la carte SD est correctement formatée.
Insérez la carte SD dans le lecteur de carte SD, intégré à votre ordinateur ou via un lecteur externe (listé dans les pièces requises).
Ouvrez ensuite l’Explorateur (sous Windows), repérez le nouveau lecteur USB, faites un clic droit pour ouvrir le menu, sélectionnez « Afficher plus d’options » puis « Formater… » pour ouvrir la boîte de dialogue de formatage :

Vérifiez que le système de fichiers est bien « FAT32 » et cliquez sur « Démarrer ».Assurez-vous d’avoir sélectionné le bon lecteur USB, car le formatage effacera toutes les données existantes sur ce lecteur !
Je renomme généralement le lecteur, par exemple en « SAMPLES », pour être sûr de ne pas formater le mauvais lecteur par erreur.
Collecte des échantillons audio avec XIAO-ESP32-S3-Sense
Ensuite, nous écrivons le code pour collecter les échantillons audio nécessaires à l’entraînement de notre modèle TinyML pour le contrôle vocal. Assurez-vous d’avoir formaté et inséré correctement la carte SD dans la XIAO-ESP32-S3-Sense, comme montré ci-dessous :

Nous voulons utiliser notre voix pour allumer une LED rouge, jaune ou verte. Les mots de contrôle seront donc « red », « yellow » et « green », mais vous pouvez choisir d’autres mots dans n’importe quelle langue.

Vous pourriez collecter ces échantillons audio avec votre téléphone ou le microphone de votre ordinateur, mais les caractéristiques (son) de ces microphones diffèrent de celui de la XIAO-ESP32-S3-Sense. Il est donc préférable d’utiliser le microphone intégré à la XIAO-ESP32-S3-Sense, ce qui nécessite un peu de code pour enregistrer les échantillons audio et les stocker sur la carte SD.
Le code suivant collecte les échantillons audio. Vous devez entrer l’étiquette (« red », « yellow » ou « green ») dans le moniteur série et appuyer sur Entrée.

Cela lance un enregistrement d’une seconde pendant lequel vous prononcez le mot de contrôle (par exemple « red »). L’enregistrement est ensuite sauvegardé sur la carte SD comme échantillon audio. Jetez un œil au code complet d’abord, puis nous en discuterons en détail :
#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);
}
La première étape du programme est d’inclure les bibliothèques nécessaires. Ces bibliothèques permettent à l’ESP32 d’utiliser l’interface I2S pour l’entrée microphone et de stocker des fichiers sur une carte SD. Sans elles, l’ESP32 ne pourrait ni enregistrer ni sauvegarder l’audio.
#include "ESP_I2S.h" #include "FS.h" #include "SD.h"
Constantes
Le code définit une constante pour la taille de l’en-tête du fichier WAV. Chaque fichier WAV commence par un en-tête de 44 octets contenant des informations comme la fréquence d’échantillonnage, la profondeur de bits et le nombre de canaux. En définissant header_size, le code sait où commencent les données audio réelles.
const int header_size = 44;
Objets
Ensuite, le programme crée un objet I2SClass. Cet objet gère toute la communication avec le microphone via le protocole I2S. En créant cette instance, l’ESP32 peut initialiser le microphone, régler ses paramètres et enregistrer l’audio.
I2SClass i2s;
scaleVolume
La fonction scaleVolume() ajuste le volume de l’audio enregistré. Elle prend les échantillons bruts, les multiplie par un facteur de gain, puis contraint les valeurs pour qu’elles restent dans la plage audio 16 bits. Cela évite la distorsion due à un dépassement.
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);
}
}
Vous pouvez aussi normaliser le volume (un facteur de 20x est typique), mais cela ne fonctionne pas en streaming et amplifie le bruit. C’est utile pour avoir une idée d’un gain adapté, par exemple 8x ou 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
La fonction setup() prépare tout avant la boucle principale. Elle initialise d’abord le moniteur série pour le débogage, configure les broches du microphone avec i2s.setPinsPdmRx(42, 41) et démarre l’interface I2S à 16 kHz en mono. Si aucun microphone n’est détecté, un message d’erreur s’affiche. Ensuite, la carte SD est initialisée sur la broche 21. Si le montage échoue, un autre message d’erreur apparaît. Enfin, l’ESP32 invite l’utilisateur à entrer une étiquette via le moniteur série.
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
Dans la loop(), le programme attend une entrée utilisateur depuis le moniteur série. L’utilisateur peut taper une étiquette, comme « red » ou « noise ». Lorsqu’une étiquette est saisie, le code crée un dossier sur la carte SD portant ce nom. Cette organisation facilite la séparation des enregistrements et simplifie le téléchargement vers Edge Impulse.
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);
}
}
Après avoir défini l’étiquette, le programme génère un nom de fichier pour le nouvel enregistrement. Il utilise le format label/label_number.wav. Par exemple, si l’étiquette est “red”, le premier fichier sera red/red_1.wav. Chaque nouvel enregistrement incrémente le compteur, évitant ainsi d’écraser les fichiers existants.
sprintf(filename, "/%s/%s_%d.wav", label.c_str(), label.c_str(), cnt++);
Serial.printf("Recording to: %s\n", filename);
Pour enregistrer l’audio, le code appelle i2s.recordWAV(1, &wav_size). Cette fonction capture une seconde d’audio et la stocke en mémoire sous forme de fichier WAV. Si l’enregistrement contient des données valides, le programme applique la fonction scaleVolume() pour augmenter le gain des échantillons audio.
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);
}
Une fois l’audio traité, le programme ouvre un fichier sur la carte SD et y écrit les données WAV. Si le fichier ne peut pas être ouvert ou écrit, un message d’erreur s’affiche. Après la sauvegarde, le fichier est fermé et un message de confirmation est affiché.
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.");
Enfin, la boucle attend 100 millisecondes avant de vérifier à nouveau une nouvelle entrée utilisateur. Ce petit délai évite de surcharger le traitement de l’entrée série.
delay(100);
Ce projet transforme votre ESP32 en un enregistreur audio pratique capable de capturer des clips d’une seconde depuis un microphone I2S et de les sauvegarder en fichiers WAV sur une carte SD. Avec des dossiers étiquetés, il devient particulièrement utile pour créer des jeux de données pour des projets d’apprentissage automatique, comme l’entraînement d’un modèle de reconnaissance vocale.
Exécution du code pour collecter les échantillons
Flashez votre XIAO-ESP32-S3-Sense avec le code et ouvrez le moniteur série. Entrez l’étiquette de la classe pour laquelle vous souhaitez collecter des échantillons dans la zone de message, par exemple « yellow », puis appuyez sur Entrée.

Le moniteur série affichera le nom du fichier de l’échantillon audio, par exemple « /yellow/yellow_1.wav », et vous aurez une seconde pour prononcer le mot « yellow ». Une fois terminé, le texte « done. » s’affichera dans le moniteur série. Voici un exemple d’échantillon audio enregistré pour le mot « yellow » :
Pour les enregistrements suivants de la même classe, par exemple « yellow », appuyez simplement sur Entrée. Ne ressaisissez pas la même étiquette. Lorsque vous êtes prêt à enregistrer les échantillons pour la classe suivante, par exemple « red », entrez « red », appuyez sur Entrée et continuez à enregistrer de la même manière.
Le code créera un nouveau dossier pour chaque classe (« red », « green », « yellow », « noise ») et y stockera les échantillons audio :

J’ai enregistré 50 échantillons par couleur et 100 pour le bruit. Pour la classe « noise » : enregistrez du silence, du bruit ambiant, de la musique, de la parole, tout sauf les mots « red », « green » ou « yellow ». Plus vous avez d’échantillons par classe, mieux c’est, mais avec 50 échantillons vous obtenez déjà une précision de détection correcte – pas parfaite, mais correcte ; )
Entraînement du modèle de contrôle vocal avec Edge Impulse
Dans cette section, nous verrons les étapes nécessaires pour télécharger nos données d’entraînement, créer le pipeline de prétraitement des données et du modèle (impulse) et entraîner notre modèle TinyML.
Créez un nouveau projet Edge Impulse, par exemple nommé « Voice Control », puis nous serons prêts à télécharger nos données d’entraînement.
Télécharger les données d’entraînement
Retirez d’abord la carte SD de la XIAO-ESP32-S3-Sense et connectez-la à votre ordinateur pour pouvoir y accéder. Elle apparaîtra comme un lecteur USB si vous utilisez un lecteur de carte SD externe.
Cliquez ensuite sur « Data acquisition » -> « + Add data » -> « Upload data » pour ouvrir la boîte de dialogue de téléchargement des données :

Dans la boîte, cochez « Select a folder » et saisissez l’étiquette de la classe à télécharger, par exemple « red », en bas :

Cliquez sur « Choose files » et sélectionnez le dossier « red » contenant vos échantillons audio. Puis cliquez sur « Upload data » et vous verrez les données se télécharger :

Répétez cette opération pour les autres couleurs et la classe bruit. Assurez-vous d’entrer la bonne étiquette avant de télécharger. Vous pouvez toutefois supprimer plus tard les échantillons téléchargés avec une mauvaise étiquette.
Une fois terminé, fermez la boîte de dialogue en cliquant sur la croix en haut à droite :

Explorer les données
Vous devriez alors voir le jeu de données dans le Data explorer. En haut à gauche, un diagramme circulaire montre la répartition des classes et la division Train/Test. En dessous, le Dataset permet de cliquer sur chaque échantillon pour l’écouter :

Si vous constatez un problème avec vos données, vous pouvez aussi supprimer des échantillons. Sinon, l’étape suivante est de créer un Impulse (prétraitement + modèle).
Créer un Impulse
Cliquez sur « Create impulse » sous « Impulse design » :

Ajoutez ensuite trois blocs : « Time series data », « Audio (MFE) » et « Transfer Learning (Keyword Spotting) » :

Conservez tous les réglages par défaut des blocs. Dans les sections suivantes, nous configurerons et entraînerons chaque bloc.
MFE
Cliquez sur « MFE » sous « Impulse design » :

Cela ouvre la boîte de dialogue des paramètres du bloc MFE :

MFE signifie Mel Frequency Energy, une méthode de traitement du signal numérique pour extraire des caractéristiques d’un signal audio. Nous laissons les paramètres tels quels. Cliquez simplement sur « Save parameters » pour enregistrer.
Dans le Feature explorer, vous pourrez voir la performance des caractéristiques extraites :

Chaque point sur le graphique représente un de nos échantillons audio (un mot prononcé ou du bruit). Idéalement, les points d’une même classe sont regroupés et les groupes bien séparés.
Le graphique montre un certain regroupement mais ce n’est pas parfait. On voit que les groupes des mots couleur (« red », « green », « yellow ») sont étirés, probablement parce que j’ai varié ma distance au micro en parlant.
La classe « noise », en revanche, a une forme circulaire, car la plupart des échantillons de bruit sont similaires en volume mais différents en contenu.
Avec ce regroupement, je ne m’attends pas à une précision fantastique. Vous pouvez ajuster les paramètres du bloc MFE et collecter plus d’échantillons pour rendre le modèle plus robuste.
Transfer Learning
Pour entraîner le modèle, cliquez sur « Transfer learning » sous « Impulse design » :

Cela ouvre la boîte de dialogue des paramètres du réseau neuronal :

Vous pouvez choisir entre deux modèles : MobileNetV1 0.1 ou MobileNetV2 0.35. Le second est probablement plus précis mais aussi beaucoup plus lourd et je n’ai pas réussi à le faire fonctionner sur la XIAO-ESP32-S3-Sense. J’ai donc choisi MobileNetV1 0.1.

J’ai augmenté le nombre de cycles d’entraînement à 60, fixé le taux d’apprentissage à 0,01 et gardé le CPU pour l’entraînement. Aucun de ces paramètres n’est très sensible ou crucial pour la précision finale, les réglages par défaut fonctionnent bien aussi.
Cliquez ensuite sur « Save & Train » pour lancer l’entraînement. À la fin, une matrice de confusion et d’autres métriques de performance s’affichent :

Comme vous pouvez le voir, dans mon cas la précision globale était de 95 % et le modèle avait tendance à confondre les mots couleur avec le bruit, ce qui est normal. La précision pourrait s’améliorer avec plus d’échantillons, mais le modèle est très petit et sa capacité de reconnaissance limitée.
Déployer le modèle
Enfin, il faut déployer le modèle pour l’exécuter sur la XIAO-ESP32-S3-Sense. Cliquez sur « Deployment » sous « Impulse design » :

En haut à droite, sélectionnez le processeur cible pour le déploiement. En août 2025, Edge Impulse ne supporte pas directement la XIAO-ESP32-S3-Sense. Nous choisissons donc Espressif ESP-EYE :

En cliquant dessus, la boîte de configuration du périphérique cible s’ouvre :

Nous corrigerons plus tard le code déployé pour qu’il fonctionne avec la XIAO-ESP32-S3-Sense. Pour le type de déploiement, sélectionnez « Arduino library » et « TensorFlow Lite » :

Cliquez ensuite sur le bouton de compilation pour construire et déployer le modèle :

Le modèle sera téléchargé sous forme de fichier ZIP, dans mon cas : « ei-voice-control-arduino-1.0.8.zip« . Le nom du fichier ZIP dépendra du nom choisi pour le projet Edge Impulse (« Voice Control »).

Pour utiliser la bibliothèque, créez un nouveau sketch vide puis installez la bibliothèque téléchargée (ei-voice-...zip) comme d’habitude dans l’IDE Arduino via Sketch -> Include Library -> Add .ZIP library.
Connexion des LEDs à la XIAO-ESP32-S3-Sense
Avant d’écrire le code pour contrôler les LEDs, je vous montre rapidement comment connecter les trois LEDs à la XIAO-ESP32-S3-Sense :

La cathode des trois LEDs est reliée à la masse (GND). Les anodes sont connectées aux broches GPIO 1, 2 et 3 via une résistance de 220 Ω comme montré ci-dessus. Vous pouvez choisir d’autres broches GPIO, mais dans ce cas, pensez à adapter le code suivant.
Code pour le contrôle vocal des LEDs
La bibliothèque ei-voice-control-arduino-1.0.8.zip que nous avons déployée contient du code exemple, mais nous ne pouvons pas l’utiliser car il est prévu pour l’ESP-EYE, qui a une interface microphone différente de la XIAO-ESP32-S3-Sense.
J’ai donc repris le code exemple et l’ai modifié pour qu’il fonctionne avec la XIAO-ESP32-S3-Sense. J’y ai ajouté le code pour commander les LEDs.
Le code suivant écoute le microphone, envoie le signal audio enregistré au modèle de classification, récupère le résultat et allume brièvement la LED rouge, jaune ou verte si un mot couleur est reconnu avec une confiance suffisante.
Jetez un œil au code complet d’abord, puis nous en discuterons. Pensez à remplacer l’import #include "Voice_control_inferencing.h" par le nom de votre bibliothèque déployée !
#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
Les premières lignes importent les bibliothèques nécessaires. Chacune joue un rôle important dans la reconnaissance vocale et le contrôle matériel.
#include "Voice_control_inferencing.h" #include "ESP_I2S.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h"
Le fichier Voice_control_inferencing.h est généré par Edge Impulse et contient le réseau neuronal entraîné. Comme mentionné, ce nom dépendra de votre projet Edge Impulse et de la bibliothèque déployée. Vous devrez peut-être l’ajuster.
La bibliothèque ESP_I2S.h donne accès à l’interface microphone I2S et les headers FreeRTOS permettent d’exécuter la capture audio en tâche de fond.
Constantes et buffers
Nous définissons ensuite des structures et constantes pour gérer l’enregistrement audio et la classification.
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;
La structure inference_t contient le buffer audio et suit le nombre d’échantillons. Nous allouons sampleBuffer comme espace temporaire pour l’entrée microphone. Le drapeau record_status contrôle si l’enregistrement continue.
Broches microphone et fréquence d’échantillonnage
Nous définissons les broches ESP32 connectées au microphone et la fréquence d’échantillonnage.
static const int8_t I2S_CLK = 42; static const int8_t I2S_DIN = 41; static const uint32_t SAMPLERATE = 16000;
Le microphone est connecté via I2S. La broche 42 fournit l’horloge, la broche 41 reçoit les données. Nous échantillonnons à 16 kHz, idéal pour la reconnaissance vocale.
Broches LEDs
Nous assignons trois broches pour le retour visuel.
static const int8_t redPin = 1; static const int8_t yellowPin = 2; static const int8_t greenPin = 3;
Chaque broche pilote une LED. Elles clignotent quand le classificateur détecte le mot couleur correspondant.
Callback d’inférence audio
Cette fonction déplace les échantillons du buffer temporaire vers le buffer principal d’inférence.
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;
}
}
}
Quand le buffer est plein, il est marqué prêt pour que le classificateur l’utilise.
Capture des échantillons microphone
Au lieu d’enregistrer dans la boucle principale, cette tâche tourne en arrière-plan.
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);
}
Nous lisons continuellement des échantillons du microphone avec I2S.read(). Chaque échantillon est amplifié avant stockage. Quand assez d’échantillons sont collectés, ils sont envoyés pour inférence.
Démarrage de l’inférence microphone
Cette fonction initialise le microphone et lance la tâche de capture.
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;
}
Nous allouons la mémoire pour le buffer d’inférence et configurons I2S pour microphones PDM. Si tout va bien, une tâche FreeRTOS commence la capture en arrière-plan.
Attente de nouvel audio
Cette fonction attend que le buffer soit prêt avec de nouvelles données.
static bool microphone_inference_record(void) {
while (inference.buf_ready == 0) delay(10);
inference.buf_ready = 0;
return true;
}
Le code fait une pause jusqu’à ce qu’assez d’audio soit collecté pour la classification.
Conversion des données audio
Le classificateur Edge Impulse attend des échantillons audio en flottant. Cette fonction convertit les entiers 16 bits bruts en floats.
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;
}
Arrêt du microphone
Quand l’audio n’est plus nécessaire, on arrête I2S et libère la mémoire.
static void microphone_inference_end(void) {
I2S.end();
free(inference.buffer);
}
Fonctions de contrôle des LEDs
Nous préparons ensuite des fonctions d’aide pour contrôler les LEDs.
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() configure les broches en sorties.switchOffLEDs() éteint toutes les LEDs.flashLED() allume la LED correspondant à l’étiquette reconnue puis l’éteint après une demi-seconde.
Setup
Dans la fonction setup, nous initialisons le moniteur série, les LEDs et le microphone.
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");
}
Si le microphone démarre correctement, nous sommes prêts à capturer la voix.
Loop
La boucle tourne en continu pour traiter l’audio et classifier les commandes vocales.
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);
}
}
Nous attendons un nouveau buffer audio, préparons un objet signal et lançons le classificateur. Celui-ci renvoie des étiquettes avec probabilités. Nous ignorons l’étiquette "noise" et choisissons la classe avec la confiance la plus élevée. Si la probabilité dépasse 0,3, nous faisons clignoter la LED correspondante.
Ce code fonctionne bien mais réagit un peu lentement aux commandes vocales, car il faut attendre que la fenêtre d’inférence soit remplie. L’approche suivante en streaming est plus rapide, mais la précision est un peu moindre.
Code streaming pour le contrôle vocal
Le classificateur continu dans ce code gère des fenêtres glissantes d’audio. Il traite des tranches qui se chevauchent, évitant d’arrêter et redémarrer l’inférence à chaque fois. Cela rend le système plus réactif aux mots-clés (mais la précision baisse un peu). Voici la démo :
Jetez un œil au code complet, puis nous verrons les différences avec le code précédent.
#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;
}
}
Directive de préprocesseur
Tout en haut, ce code introduit une nouvelle définition :
#define EIDSP_QUANTIZE_FILTERBANK 0
Cela désactive la quantification du filterbank dans la chaîne DSP Edge Impulse. Le modèle utilise la pleine précision au lieu de valeurs compressées. La première version ne contenait pas cette ligne, donc la quantification par défaut était utilisée.
Imports
Les imports sont les mêmes qu’avant : code d’inférence Edge Impulse, support I2S et FreeRTOS pour le multitâche. Comme précédemment, le nom de la bibliothèque Voice_control_inferencing.h dépendra de votre projet Edge Impulse et de la bibliothèque déployée. Vous devrez peut-être l’ajuster.
#include "Voice_control_inferencing.h" #include "ESP_I2S.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h"
Buffers et variables
Au lieu de créer un grand buffer d’inférence, cette version travaille avec des tranches d’audio.
static const uint32_t slice_size = EI_CLASSIFIER_SLICE_SIZE; static signed short sampleBuffer[slice_size]; static int slice_ctn = 0;
Dans le code précédent, le buffer contenait une fenêtre d’échantillonnage complète (EI_CLASSIFIER_RAW_SAMPLE_COUNT). Ici, une seule tranche est traitée à la fois. La variable slice_ctn aide à limiter la fréquence des prédictions pour éviter qu’elles ne se déclenchent trop souvent.
Tâche de capture audio
La boucle de capture est similaire mais avec un grand changement :
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);
}
Ici, on remplit continuellement une seule tranche au lieu d’une fenêtre complète. L’appel vTaskDelay(1) permet à FreeRTOS de mieux gérer les autres tâches. La première version alimentait un buffer d’inférence via des callbacks, celle-ci met juste à jour le buffer de tranche.
Configuration du microphone
Les deux versions configurent le microphone avec I2S, mais celle-ci est plus simple.
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;
}
Notez qu’il n’y a pas d’allocation dynamique (malloc) ni de configuration de buffer d’inférence. Cela réduit la complexité et évite la fragmentation mémoire.
Enregistrement audio
La fonction d’enregistrement est maintenant un simple placeholder :
static bool microphone_inference_record(void) {
delay(1);
return true;
}
Dans la première version, cette fonction attendait que le buffer soit prêt. Ici, ce n’est plus nécessaire. La classification se fait tranche par tranche.
Entrée du classificateur
La fonction de conversion des échantillons bruts en flottants est la même dans les deux versions :
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
Les fonctions LED sont identiques : allumer la LED correspondant à l’étiquette reconnue et l’éteindre après une demi-seconde.
void flashLED(const char* label) { ... }
Fonction setup
La fonction setup comporte quelques étapes nouvelles :
run_classifier_init(); ei_sleep(1000);
Le classificateur est initialisé une fois avant usage, ce qui est nécessaire pour la classification continue. La version précédente n’en avait pas besoin car elle ne faisait que des inférences ponctuelles.
Le microphone démarre aussi avec slice_size au lieu du nombre complet d’échantillons bruts, ce qui permet le streaming continu.
Fonction loop
C’est ici que réside la plus grande différence. Au lieu d’utiliser run_classifier(), cette version appelle :
EI_IMPULSE_ERROR r = run_classifier_continuous(&signal, &result, debug_nn);
Le classificateur continu gère des fenêtres glissantes d’audio. Il traite des tranches qui se chevauchent, évitant d’arrêter et redémarrer l’inférence à chaque fois. Cela rend le système plus réactif aux mots-clés.
Un autre changement clé est la limitation des prédictions :
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;
}
Après détection d’une étiquette, le programme attend une fenêtre complète du modèle avant d’autoriser un nouveau déclenchement. Cela évite que la LED clignote plusieurs fois pour un même mot. Le code précédent n’avait pas ce mécanisme et pouvait se déclencher immédiatement.
Résumé des différences
La première version utilise une approche bufferisée et ponctuelle :
- Collecte une fenêtre d’inférence complète.
- Effectue la classification une fois par buffer.
- Nécessite d’attendre que le buffer soit prêt.
- Utilise
run_classifier().
Cette deuxième version utilise une approche streaming et continue :
- Travaille avec de petites tranches d’audio.
- Effectue la classification en continu avec chevauchement.
- Pas d’attente de disponibilité du buffer.
- Utilise
run_classifier_continuous(). - Ajoute un délai de refroidissement (
slice_ctn) pour éviter les déclenchements répétés.
Conclusions et commentaires
Dans ce tutoriel, vous avez appris à créer une application de contrôle vocal avec la carte XIAO-ESP32-S3-Sense et la plateforme Edge Impulse. Edge Impulse offre bien plus que ce qui a été couvert ici, et je vous recommande de lire le Edge Impulse Documentation.
L’exemple simple de ce tutoriel vous a permis de contrôler trois LEDs avec votre voix. Notez que vous pouvez créer des applications de contrôle plus puissantes en utilisant une séquence de mots et une machine à états. Par exemple, un mot d’activation comme « Jarvis », suivi d’un lieu « Desk », « Corner », « Shelf », suivi d’un appareil « Light » ou « Fan », puis d’une action « On » ou « Off » :
[Jarvis] -> [Desk, Corner, Shelf] -> [Light, Fan] -> [On, Off]
Avec huit mots différents, vous pouvez déjà contrôler 1x3x2x2 = 12 actions différentes comme « Jarvis Desk Light On »
Au-delà de la domotique, il existe de nombreuses autres applications intéressantes de classification sonore : classification des sons environnementaux, détection d’anomalies par vibration/son, classification des chants d’oiseaux, etc.
Enfin, si vous souhaitez détecter des visages ou des personnes, consultez nos Face Detection with XIAO ESP32-S3-Sense and SenseCraft AI et Edge AI Room Occupancy Sensor with ESP32 and Person Detection tutoriels.
Si vous avez des questions, n’hésitez pas à les poser dans la section commentaires.
Bon bricolage 😉

