Pular para o conteúdo principal

XIAO ESP32S3-Sense Detecção de Palavras-chave

Este tutorial irá guiá-lo na implementação de um sistema de Detecção de Palavras‑chave (KWS) usando TinyML na placa microcontroladora XIAO ESP32S3 Sense, com a ajuda do Edge Impulse para coleta de dados e treinamento do modelo. A KWS é essencial para sistemas de reconhecimento de voz e, com o poder do TinyML, é possível implementá-la em dispositivos menores e de baixo consumo de energia. Vamos construir nosso próprio sistema de KWS usando Edge Impulse e XIAO ESP32S3 Sense!

1. Primeiros Passos

Antes de iniciar este projeto, siga as etapas de preparação abaixo para preparar o software e o hardware necessários.

Hardware

Para realizar este projeto com sucesso, você precisa preparar o seguinte hardware.

  • XIAO ESP32S3 Sense
  • Cartão microSD (não maior que 32GB)
  • Leitor de cartão microSD
  • Cabo de dados USB-C
info

Use a versão 2.x do arduino-esp32, pois não é compatível com a 3.x.

Insira o cartão microSD no slot para microSD. Observe a direção de inserção: o lado com o conector dourado deve ficar voltado para dentro.

Software

Se esta é a sua primeira vez usando o XIAO ESP32S3 Sense, antes de começar, sugerimos que leia os dois Wikis a seguir para aprender como utilizá-lo.

2. Capturando Dados de Áudio (offline)

Etapa 1. Salvar amostras de som gravadas como arquivos de áudio .wav em um cartão microSD.

Vamos usar o leitor de cartão SD onboard para salvar arquivos de áudio .wav, primeiro precisamos habilitar a PSRAM do XIAO.

Em seguida, compile e envie o seguinte programa para o XIAO ESP32S3.

dica

Este código grava áudio usando a interface I2S da placa Seeed XIAO ESP32S3 Sense, salva a gravação como um arquivo .wav em um cartão SD e permite controlar o processo de gravação por meio de comandos enviados do monitor serial. O nome do arquivo de áudio é personalizável (deve ser os rótulos de classe a serem usados no treinamento), e várias gravações podem ser feitas, cada uma salva em um novo arquivo. O código também inclui funcionalidade para aumentar o volume das gravações.

/* 
* WAV Recorder for Seeed XIAO ESP32S3 Sense
*
* NOTE: To execute this code, we will need to use the PSRAM
* function of the ESP-32 chip, so please turn it on before uploading.
* Tools>PSRAM: "OPI PSRAM"
*
* Adapted by M.Rovai @May23 from original Seeed code
*/

#include <I2S.h>
#include "FS.h"
#include "SD.h"
#include "SPI.h"

// make changes as needed
#define RECORD_TIME 10 // seconds, The maximum value is 240
#define WAV_FILE_NAME "data"

// do not change for best
#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("Failed to initialize I2S!");
while (1) ;
}
if(!SD.begin(21)){
Serial.println("Failed to mount SD Card!");
while (1) ;
}
Serial.printf("Enter with the label name\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; // reset file number each time a new base file name is set
Serial.printf("Send rec for starting recording label \n");
}
}
if (isRecording && baseFileName != "") {
String fileName = "/" + baseFileName + "." + String(fileNumber) + ".wav";
fileNumber++;
record_wav(fileName);
delay(1000); // delay to avoid recording multiple files at once
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("Start recording ...\n");

File file = SD.open(fileName.c_str(), FILE_WRITE);
// Write the header to the WAV file
uint8_t wav_header[WAV_HEADER_SIZE];
generate_wav_header(wav_header, record_size, SAMPLE_RATE);
file.write(wav_header, WAV_HEADER_SIZE);

// PSRAM malloc for recording
rec_buffer = (uint8_t *)ps_malloc(record_size);
if (rec_buffer == NULL) {
Serial.printf("malloc failed!\n");
while(1) ;
}
Serial.printf("Buffer: %d bytes\n", ESP.getPsramSize() - ESP.getFreePsram());

// Start recording
esp_i2s::i2s_read(esp_i2s::I2S_NUM_0, rec_buffer, record_size, &sample_size, portMAX_DELAY);
if (sample_size == 0) {
Serial.printf("Record Failed!\n");
} else {
Serial.printf("Record %d bytes\n", sample_size);
}

// Increase volume
for (uint32_t i = 0; i < sample_size; i += SAMPLE_BITS/8) {
(*(uint16_t *)(rec_buffer+i)) <<= VOLUME_GAIN;
}

// Write data to the WAV file
Serial.printf("Writing to the file ...\n");
if (file.write(rec_buffer, record_size) != record_size)
Serial.printf("Write file Failed!\n");

free(rec_buffer);
file.close();
Serial.printf("Recording complete: \n");
Serial.printf("Send rec for a new sample or enter a new label\n\n");
}

void generate_wav_header(uint8_t *wav_header, uint32_t wav_size, uint32_t sample_rate)
{
// See this for reference: 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));
}

Agora, faça o upload do código para o XIAO e colete amostras das palavras‑chave (hello e stop). Você também pode capturar ruídos e outras palavras. O monitor Serial solicitará que você envie o rótulo a ser gravado.

Envie o rótulo (por exemplo, hello). O programa aguardará outro comando: rec.

E o programa começará a gravar novas amostras sempre que um comando rec for enviado. Os arquivos serão salvos como hello.1.wav, hello.2.wav, hello.3.wav, etc. até que um novo rótulo (por exemplo, stop) seja enviado. Nesse caso, você deve enviar o comando rec para cada nova amostra, que será salva como stop.1.wav, stop.2.wav, stop.3.wav, etc.

Por fim, teremos os arquivos salvos no cartão SD.

nota

Recomendamos que você tenha sons suficientes para cada amostra de rótulo. Você pode repetir suas palavras‑chave várias vezes durante cada sessão de gravação de dez segundos, e segmentaremos as amostras nas etapas subsequentes. Mas é necessário haver algum espaçamento entre as palavras‑chave.

3. Aquisição de dados para treinamento

Etapa 2. Enviando os dados de som coletados

Quando o conjunto de dados bruto estiver definido e coletado, devemos iniciar um novo projeto no Edge Impulse. Uma vez criado o projeto, selecione a ferramenta Upload Existing Data na seção Data Acquisition. Escolha os arquivos a serem enviados.

E faça o upload deles para o Studio (você pode dividir automaticamente os dados em treino/teste). Repita para todas as classes e todos os dados brutos.

Todos os dados do conjunto possuem 1s de duração, mas as amostras gravadas na seção anterior têm 10s e devem ser divididas em amostras de 1s para serem compatíveis. Clique nos três pontos após o nome da amostra e selecione Split sample.

Dentro da ferramenta, divida os dados em registros de 1 segundo. Se necessário, adicione ou remova segmentos.

Esse procedimento deve ser repetido para todas as amostras.

Etapa 3. Criando o Impulse (Pré‑processamento / Definição do modelo)

Um impulse recebe dados brutos, usa processamento de sinal para extrair características e, em seguida, usa um bloco de aprendizado para classificar novos dados.

Primeiro, pegaremos os pontos de dados com uma janela de 1 segundo, aumentando os dados, deslizando essa janela a cada 500ms. Observe que a opção zero-pad data está definida. Isso é importante para preencher com zeros as amostras menores que 1 segundo (em alguns casos, reduzi a janela de 1000 ms na split tool para evitar ruídos e picos).

Cada amostra de áudio de 1 segundo deve ser pré‑processada e convertida em uma imagem (por exemplo, 13 x 49 x 1). Usaremos MFCC, que extrai características de sinais de áudio usando Coeficientes Cepstrais na Escala Mel, que são ótimos para voz humana.

Em seguida, selecionamos KERAS para classificação, que constrói nosso modelo do zero fazendo Classificação de Imagens usando uma Rede Neural Convolucional.

Etapa 4. Pré‑processamento (MFCC)

O próximo passo é criar as imagens que serão treinadas na próxima fase. Podemos manter os valores padrão dos parâmetros ou aproveitar a opção Autotuneparameters option do DSP, o que faremos.

4. Construindo um modelo de aprendizado de máquina

Etapa 5. Design e treinamento do modelo

Usaremos um modelo de Rede Neural Convolucional (CNN). A arquitetura básica é definida com dois blocos de Conv1D + MaxPooling (com 8 e 16 neurônios, respectivamente) e um Dropout de 0,25. E na última camada, após o Flatten, quatro neurônios, um para cada classe.

Como hiperparâmetros, teremos uma Taxa de Aprendizado de 0,005 e um modelo que será treinado por 100 épocas. Também incluiremos aumento de dados, como algum ruído. O resultado parece bom.

5. Implantando no XIAO ESP32S3 Sense

Etapa 6. Implantando no XIAO ESP32S3 Sense

O Edge Impulse empacotará todas as bibliotecas necessárias, funções de pré‑processamento e modelos treinados, fazendo o download para o seu computador. Você deve selecionar a opção Arduino Library e, na parte inferior, selecionar Quantized (Int8) e pressionar o botão Build.

No seu Arduino IDE, vá até a aba Sketch e selecione a opção Add .ZIP Library, e escolha o arquivo .zip baixado pelo Edge Impulse.

Você pode encontrar o código completo no GitHub do projeto. Envie o sketch para sua placa e teste algumas inferências reais.

dica

A biblioteca importada no código precisa ser atualizada com o nome da sua biblioteca. A lógica de acendimento também precisa ser modificada com base na ordem dos rótulos que você realmente treinou.

/* 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.
*/

// If your target is limited in memory remove this macro to save 10K RAM
#define EIDSP_QUANTIZE_FILTERBANK 0

/*
** NOTE: If you run into TFLite arena allocation issue.
**
** This may be due to may dynamic memory fragmentation.
** Try defining "-DEI_CLASSIFIER_ALLOCATION_STATIC" in boards.local.txt (create
** if it doesn't exist) and copy this file to
** `<ARDUINO_CORE_INSTALL_PATH>/arduino/hardware/<mbed_core>/<core_version>/`.
**
** See
** (https://support.arduino.cc/hc/en-us/articles/360012076960-Where-are-the-installed-cores-located-)
** to find where Arduino installs cores on your machine.
**
** If the problem persists then there's not enough memory for this model and application.
*/

/* Includes ---------------------------------------------------------------- */
#include <XIAO-ESP32S3-KWS_inferencing.h>

#include <I2S.h>
#define SAMPLE_RATE 16000U
#define SAMPLE_BITS 16

#define LED_BUILT_IN 21

/** Audio buffers, pointers and selectors */
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; // Set this to true to see e.g. features generated from the raw signal
static bool record_status = true;

/**
* @brief Arduino setup function
*/
void setup()
{
// put your setup code here, to run once:
Serial.begin(115200);
// comment out the below line to cancel the wait for USB connection (needed for native USB)
while (!Serial);
Serial.println("Edge Impulse Inferencing Demo");

pinMode(LED_BUILT_IN, OUTPUT); // Set the pin as output
digitalWrite(LED_BUILT_IN, HIGH); //Turn off

I2S.setAllPins(-1, 42, 41, -1, -1);
if (!I2S.begin(PDM_MONO_MODE, SAMPLE_RATE, SAMPLE_BITS)) {
Serial.println("Failed to initialize I2S!");
while (1) ;
}

// summary of inferencing settings (from model_metadata.h)
ei_printf("Inferencing settings:\n");
ei_printf("\tInterval: ");
ei_printf_float((float)EI_CLASSIFIER_INTERVAL_MS);
ei_printf(" ms.\n");
ei_printf("\tFrame size: %d\n", EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE);
ei_printf("\tSample length: %d ms.\n", EI_CLASSIFIER_RAW_SAMPLE_COUNT / 16);
ei_printf("\tNo. of classes: %d\n", sizeof(ei_classifier_inferencing_categories) / sizeof(ei_classifier_inferencing_categories[0]));

ei_printf("\nStarting continious inference in 2 seconds...\n");
ei_sleep(2000);

if (microphone_inference_start(EI_CLASSIFIER_RAW_SAMPLE_COUNT) == false) {
ei_printf("ERR: Could not allocate audio buffer (size %d), this could be due to the window length of your model\r\n", EI_CLASSIFIER_RAW_SAMPLE_COUNT);
return;
}

ei_printf("Recording...\n");
}

/**
* @brief Arduino main function. Runs the inferencing loop.
*/
void loop()
{
bool m = microphone_inference_record();
if (!m) {
ei_printf("ERR: Failed to record audio...\n");
return;
}

signal_t signal;
signal.total_length = EI_CLASSIFIER_RAW_SAMPLE_COUNT;
signal.get_data = &microphone_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;
}

int pred_index = 0; // Initialize pred_index
float pred_value = 0; // Initialize pred_value

// print the predictions
ei_printf("Predictions ");
ei_printf("(DSP: %d ms., Classification: %d ms., Anomaly: %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;
}
}
// Display inference result
if (pred_index == 3){
digitalWrite(LED_BUILT_IN, LOW); //Turn on
}
else{
digitalWrite(LED_BUILT_IN, HIGH); //Turn off
}


#if EI_CLASSIFIER_HAS_ANOMALY == 1
ei_printf(" anomaly score: ");
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) {

/* read data at once from i2s - Modified for XIAO ESP2S3 Sense and I2S.h library */
// 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("Error in I2S read : %d", bytes_read);
}
else {
if (bytes_read < i2s_bytes_to_read) {
ei_printf("Partial I2S read");
}

// scale the data (otherwise the sound is too quiet)
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 Init inferencing struct and setup/start PDM
*
* @param[in] n_samples The n samples
*
* @return { description_of_the_return_value }
*/
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("Failed to start I2S!");
// }

ei_sleep(100);

record_status = true;

xTaskCreate(capture_samples, "CaptureSamples", 1024 * 32, (void*)sample_buffer_size, 10, NULL);

return true;
}

/**
* @brief Wait on new data
*
* @return True when finished
*/
static bool microphone_inference_record(void)
{
bool ret = true;

while (inference.buf_ready == 0) {
delay(10);
}

inference.buf_ready = 0;
return ret;
}

/**
* Get raw audio signal data
*/
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 Stop PDM and release buffers
*/
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 "Invalid model for current sensor."
#endif

A ideia é que o LED fique LIGADO sempre que a palavra-chave HELLO for detectada. Da mesma forma, em vez de acender um LED, isso pode ser um "gatilho" para um dispositivo externo, como vimos na introdução.

Para Fazer

  • Construa seu próprio projeto de KWS e execute no XIAO ESPS3 Sense.

Agradecimentos Especiais

Agradecimentos especiais a MJRoBot (Marcelo Rovai) pelo conteúdo do tutorial sobre o acesso do XIAO ESP32S3 Sense ao Edge Impulse. O artigo original é muito detalhado e contém muito conhecimento sobre aprendizado de máquina.

Se você quiser ler o conteúdo original deste artigo, pode ir diretamente ao artigo original rolando a página para baixo.

MJRoBot também tem muitos projetos interessantes sobre o XIAO ESP32S3.

Loading Comments...