Capture áudio com o microfone PDM do Nano 33 BLE Sense, extraia coeficientes MFCC e classifique palavras faladas em tempo real com uma CNN — tudo na borda.
Conceito
Keyword Spotting é a tarefa de detectar palavras específicas em um fluxo contínuo de áudio. É a tecnologia por trás dos assistentes de voz ("Hey Siri", "OK Google"), e uma das aplicações mais estudadas em TinyML porque o modelo precisa rodar 24/7 consumindo pouquíssima energia.
Nesta prática classificaremos 4 palavras em português: "sim", "não", "liga" e "desliga", além de uma classe "silêncio" para rejeitar áudio sem comando.
Biblioteca PDM no Arduino IDE e librosa + sounddevice no Python.
Gravar clipes WAV de 1s para cada palavra com o Arduino ou microfone do PC.
Converter WAV em imagens de espectrograma Mel com librosa.
Treinar uma CNN 1D com Keras usando as imagens MFCC como entrada.
Quantizar e exportar o modelo como array C para o Arduino.
Compilar o sketch de inferência e classificar palavras em tempo real.
Teoria Aplicada
O microfone captura uma onda de pressão no tempo. Redes neurais aprendem melhor com representações que destacam as características perceptuais do som — os Mel-Frequency Cepstral Coefficients (MFCCs) fazem exatamente isso.
O ouvido humano percebe diferenças de frequência de forma logarítmica — distingue melhor entre 100 Hz e 200 Hz do que entre 8000 Hz e 8100 Hz. A escala Mel imita esse comportamento, tornando os coeficientes muito mais discriminativos para sons da voz do que uma FFT linear.
Projeto
Dependências
Biblioteca nativa do Arduino Mbed OS para captura de áudio PDM do microfone MP34DT05. Já inclusa no pacote de placas "Arduino Mbed OS Nano Boards".
Mesma biblioteca da Parte 1 — TFLite Micro empacotado para Arduino. Buscar por "TinyMLibrary" na Library Manager.
# Ativar o ambiente virtual
source env/bin/activate
# Instalar dependências novas (além das da Parte 1)
pip install librosa sounddevice soundfile audiomentations
| Pacote | Finalidade | Versão testada |
|---|---|---|
librosa | Extração de MFCC e pré-processamento de áudio | 0.10+ |
sounddevice | Gravação de áudio pelo microfone do PC | 0.4+ |
soundfile | Leitura e escrita de arquivos WAV | 0.12+ |
audiomentations | Data augmentation de áudio (ruído, pitch shift…) | 0.36+ |
tensorflow | Treinamento CNN + conversão TFLite | 2.x / 2.16 |
numpy | Manipulação de arrays | 1.26+ |
Tutorial
Siga as etapas abaixo na ordem indicada.
A biblioteca PDM já vem com o pacote de placas do Nano 33 BLE Sense. Para verificar se o microfone funciona, carregue o exemplo de teste:
// No Arduino IDE:
// File → Examples → PDM → PDMSerialPlotter
Abra o Serial Plotter (115200 baud) e fale próximo ao Arduino — você deve ver a forma de onda do áudio em tempo real.
O MP34DT05 fica na face inferior da placa (lado oposto ao USB), próximo ao canto. Mantenha essa face voltada para a fonte sonora durante a coleta.
Há duas abordagens para coletar as amostras WAV de 1 segundo:
Grave diretamente pelo Python com sounddevice:
import sounddevice as sd
import soundfile as sf
import os, time
palavras = ['sim', 'nao', 'liga', 'desliga', 'silencio']
AMOSTRAS = 80 # por classe
SR = 16000 # Hz
for palavra in palavras:
os.makedirs(f'dados_audio/{palavra}', exist_ok=True)
for i in range(AMOSTRAS):
input(f"\nPressione Enter e diga '{palavra}' ({i+1}/{AMOSTRAS})...")
audio = sd.rec(SR, samplerate=SR, channels=1, dtype='int16')
sd.wait()
sf.write(f'dados_audio/{palavra}/{palavra}_{i:03d}.wav', audio, SR)
print('Gravado!')
Carregue o sketch coleta_audio.ino, que captura 16.000 amostras
(1 segundo a 16 kHz) e transmite via Serial em formato binário.
Um script Python lê a porta serial e salva cada gravação como WAV.
Colete no mínimo 80 amostras por classe. Varie a distância, intensidade de voz e ambiente (com e sem ruído de fundo) para aumentar a robustez do modelo.
Antes de treinar, cada arquivo WAV é convertido em uma "imagem" de coeficientes MFCC. Execute o notebook de treinamento que inclui a extração:
import librosa
import numpy as np
def extrair_mfcc(caminho_wav, n_mfcc=40, max_frames=49):
"""Retorna array (max_frames, n_mfcc) normalizado."""
y, sr = librosa.load(caminho_wav, sr=16000, duration=1.0)
mfcc = librosa.feature.mfcc(y=y, sr=sr,
n_mfcc=n_mfcc,
n_fft=512,
hop_length=160, # 10 ms
win_length=400) # 25 ms
# Padeia ou trunca para max_frames
if mfcc.shape[1] < max_frames:
mfcc = np.pad(mfcc, ((0,0),(0, max_frames - mfcc.shape[1])))
else:
mfcc = mfcc[:, :max_frames]
# Normaliza por instância (zero mean, unit variance)
mfcc = (mfcc - mfcc.mean()) / (mfcc.std() + 1e-8)
return mfcc.T # shape: (49, 40)
Janela de 25 ms (400 amostras a 16 kHz) com salto de 10 ms (160 amostras) gera ≈ 99 frames por segundo. Usando 1s de áudio e truncando em 49 frames obtemos input de forma 49 × 40 para a CNN.
Execute o notebook de treinamento:
jupyter notebook notebooks/03_treinamento_kws.ipynb
Arquitetura da CNN usada (leve o suficiente para MCU):
from tensorflow import keras
model = keras.Sequential([
keras.layers.Input(shape=(49, 40)), # frames × coef
keras.layers.Conv1D(32, kernel_size=3, activation='relu'),
keras.layers.BatchNormalization(),
keras.layers.MaxPooling1D(pool_size=2),
keras.layers.Conv1D(64, kernel_size=3, activation='relu'),
keras.layers.BatchNormalization(),
keras.layers.GlobalAveragePooling1D(),
keras.layers.Dense(64, activation='relu'),
keras.layers.Dropout(0.3),
keras.layers.Dense(5, activation='softmax') # 5 classes
])
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
Use audiomentations para adicionar ruído de fundo, mudança
de pitch e variação de velocidade antes de extrair os MFCCs.
Isso aumenta muito a robustez em ambientes reais.
Execute o notebook de conversão:
jupyter notebook notebooks/04_conversao_kws_tflite.ipynb
O processo é análogo à Parte 1, mas o dataset representativo usa amostras MFCC:
import tensorflow as tf
def representative_dataset():
for x in X_val[:200]:
yield [x[np.newaxis].astype(np.float32)] # (1, 49, 40)
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_dataset
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.int8
converter.inference_output_type = tf.int8
tflite_model = converter.convert()
# Salvar e exportar como array C
with open('outputs/kws_int8.tflite', 'wb') as f:
f.write(tflite_model)
Execute xxd -i outputs/kws_int8.tflite > arduino/inferencia_kws/kws_model_data.h
para gerar o array C.
O sketch inferencia_kws.ino executa o seguinte loop continuamente:
scale e zero_pointMicroInterpreter// Trecho principal — inferência_kws.ino
if (novo_audio_disponivel) {
calcular_mfcc(buffer_audio, mfcc_input); // 49×40 → input tensor
for (int i = 0; i < 49 * 40; i++) {
int32_t v = (int32_t)(mfcc_input[i] / input_scale) + input_zp;
input->data.int8[i] = (int8_t)constrain(v, -128, 127);
}
interpreter->Invoke();
int melhor = argmax_int8(output, kNumClasses);
float confianca = (output->data.int8[melhor] - output_zp) * output_scale;
if (confianca > 0.75f) {
Serial.print("→ "); Serial.println(kLabels[melhor]);
}
}
A FFT e os filtros Mel precisam ser reimplementados em C++ sem uso de
float complexo. Utilize a biblioteca
CMSIS-DSP (já inclusa no core do Nano 33 BLE Sense)
ou a implementação de referência do TFLite Micro
(micro_features/ na Harvard_TinyMLx).
Abra o Serial Monitor a 115200 baud, fale cada palavra a ~20 cm do microfone e observe a saída:
=====================================
TinyML Parte 2 — KWS em Português
Arduino Nano 33 BLE Sense
=====================================
Modelo: 18432 bytes
Arena: 40 KB
Classes: sim | nao | liga | desliga | silencio
=====================================
Aguardando comando de voz...
→ sim (91.2%) | inferência: 22.1 ms
→ desliga (87.5%) | inferência: 21.8 ms
→ silencio (98.0%) | inferência: 21.3 ms
LED RGB: verde = sim, vermelho = não, azul = liga, amarelo = desliga, apagado = silêncio.
Links Úteis
Esta prática é baseada no tutorial de Keyword Spotting do livro TinyML (Warden & Situnayake) e nos recursos oficiais do Arduino e TensorFlow.
Solução de Problemas
| Problema | Causa provável | Solução |
|---|---|---|
| PDM não inicializa | Pacote de placa desatualizado | Atualizar "Arduino Mbed OS Nano Boards" para v4.1+ |
| Áudio com muito ruído | Taxa de amostragem errada | Fixar em 16.000 Hz — o MP34DT05 opera melhor nessa frequência |
AllocateTensors() failed |
Arena insuficiente para a CNN | Aumentar kTensorArenaSize para 48 * 1024 |
| Sempre classifica "silêncio" | MFCC calculado diferente no MCU vs Python | Verificar parâmetros: n_fft=512, hop=160, win=400, n_mfcc=40 |
| Baixa acurácia em produção | Pouco áudio de treino ou sem augmentation | Aumentar para 150+ amostras/classe e aplicar ruído de fundo |
librosa não encontrado |
Ambiente virtual não ativado | Executar source env/bin/activate antes do Jupyter |