6 — Robustez “de produção”: janela (Hann), gate de silêncio, suavização temporal e integração de periféricos no RP2040
Nesta seção vamos resolver os problemas típicos que aparecem quando você sai do “laboratório silencioso” e vai para um ambiente real:
- Leakage espectral (vazamento) no frame → melhora com janela Hann.
- Falsos positivos em ruído / vento / batida → melhora com gate por energia (RMS).
- LED “tremendo” com pequenas variações → melhora com suavização temporal (EMA) e histerese.
- Amarrar tudo com inicialização de ADC, clock de amostragem e GPIO no Pico SDK.
Importante: ainda não vou despejar o “arquivo único final” aqui; vamos encaixando as peças, e na próxima seção eu entrego o código completo e funcional (um
main.cpronto para compilar).
6.1 Janela Hann (por que e como)
Quando você pega um frame de 256 amostras, você está implicitamente “cortando” um trecho do sinal. Esse corte gera descontinuidade nas bordas, o que espalha energia para bins vizinhos na FFT (leakage).
A janela Hann é:
\[
w[n] = 0.5 \Bigl(1 – \cos(\frac{2\pi n}{N-1})\Bigr)
\]
Aplicação:
\[
x_w[n] = x[n]\cdot w[n]
\]
6.1.1 Tabela da janela (pré-calculada)
static float g_hann[FRAME_N];
static void window_init_hann(void)
{
for (int n = 0; n < FRAME_N; n++) {
float a = 2.0f * (float)M_PI * (float)n / (float)(FRAME_N - 1);
g_hann[n] = 0.5f * (1.0f - cosf(a));
}
}
6.1.2 Aplicar janela antes da FFT
No compute_cepstrum_and_detect() (seção anterior), troque o carregamento do frame por:
for (int i = 0; i < FRAME_N; i++) {
buf[i].re = frame[i] * g_hann[i];
buf[i].im = 0.0f;
}
6.2 Gate de silêncio por energia RMS (para cortar falsos positivos)
Se o ambiente está silencioso, o cepstrum pode “inventar” picos pequenos por ruído numérico. A solução clássica é não rodar detecção quando a energia do frame é muito baixa.
RMS:
\[
\text{RMS} = \sqrt{\frac{1}{N}\sum_{n=0}^{N-1} x[n]^2}
\]
Como queremos eficiência, usamos energia média sem sqrt:
\[
E = \frac{1}{N}\sum x[n]^2
\]
6.2.1 Função de energia do frame
static float frame_energy(const float *x)
{
float acc = 0.0f;
for (int i = 0; i < FRAME_N; i++) {
acc += x[i] * x[i];
}
return acc / (float)FRAME_N;
}
6.2.2 Usar o gate antes do cepstrum
static bool process_frame(frame_buf_t *fb, uint led_gpio)
{
if (!fb->full) return false;
fb->full = false;
float E = frame_energy(fb->data);
// Ajuste inicial: energia mínima para “vale a pena analisar”
const float E_MIN = 0.0008f; // você calibra depois
if (E < E_MIN) {
// ambiente silencioso: tende a desligar LED com histerese
// aqui podemos forçar "peak baixo" para cair pelo TH_OFF
whistle_update_led(0.0f, 10, led_gpio);
return true;
}
int q = 0;
float peak = compute_cepstrum_and_detect(fb->data, &q);
whistle_update_led(peak, q, led_gpio);
return true;
}
6.3 Suavização temporal (EMA) do “peak” do cepstrum
O pico do cepstrum é uma medida ruidosa frame-a-frame. Uma suavização leve resolve.
EMA (Exponential Moving Average):
\[
p_f[n] = \alpha p_f[n-1] + (1-\alpha)p[n]
\]
- \(\alpha\) perto de 1 → mais suave, responde mais lento
- \(\alpha\) menor → mais rápido, mais nervoso
6.3.1 Aplicando EMA dentro do detector
Adicione um estado global:
static float g_peak_filt = 0.0f;
E no processamento do frame:
float peak = compute_cepstrum_and_detect(fb->data, &q);
// suaviza
const float ALPHA = 0.85f;
g_peak_filt = ALPHA * g_peak_filt + (1.0f - ALPHA) * peak;
whistle_update_led(g_peak_filt, q, led_gpio);
Agora os limiares TH_ON/TH_OFF ficam bem mais estáveis.
6.4 Amostragem estável no RP2040 (sem “sleep torto”)
sleep_us(1000000/FS) funciona, mas tem jitter. Para deixar mais “engenharia de verdade”, usamos timer repetitivo (Pico SDK) para chamar a rotina de amostragem com período fixo.
6.4.1 Callback de amostragem
#include "pico/stdlib.h"
#include "hardware/adc.h"
#include "pico/time.h"
static repeating_timer_t g_timer;
static taylor_filter_t g_filter = {0};
static frame_buf_t g_frame;
static inline float read_adc_normalized(void)
{
uint16_t raw = adc_read(); // 0..4095
return ((float)raw - 2048.0f) / 2048.0f;
}
static bool sample_timer_cb(repeating_timer_t *rt)
{
(void)rt;
float x = read_adc_normalized();
float y = taylor_bandpass_step(&g_filter, x);
frame_buf_push(&g_frame, y);
return true; // keep repeating
}
6.5 Inicialização do ADC e do LED (Pico SDK)
Aqui é onde você amarra com o hardware real. Exemplo típico:
- ADC do RP2040: entradas ADC0..ADC3 (GPIO 26..29)
- Vamos supor microfone/saída analógica no ADC0 (GPIO26)
- LED em algum GPIO (ex.: o LED onboard do Pico é 25; na BitDogLab pode variar — você ajusta o define)
#define ADC_GPIO 28
#define ADC_CH 0
#define LED_GPIO 13
static void hw_init(void)
{
stdio_init_all();
// LED
gpio_init(LED_GPIO);
gpio_set_dir(LED_GPIO, GPIO_OUT);
gpio_put(LED_GPIO, 0);
// ADC
adc_init();
adc_gpio_init(ADC_GPIO);
adc_select_input(ADC_CH);
}
6.6 Inicialização “DSP”: twiddles, janela e buffers
static void dsp_init_all(void)
{
frame_buf_init(&g_frame);
fft_init_twiddles();
window_init_hann();
g_peak_filt = 0.0f;
}
6.7 Main “quase final”: timer coleta, loop processa frames
int main(void)
{
hw_init();
dsp_init_all();
// agenda amostragem a cada 1/FS segundos
const int sample_period_us = 1000000 / FS_HZ;
add_repeating_timer_us(-sample_period_us, sample_timer_cb, NULL, &g_timer);
while (true) {
// processa quando frame fecha
process_frame(&g_frame, LED_GPIO);
// o loop pode dormir um pouco; a amostragem continua no timer
sleep_ms(1);
}
}
Esse “main” já fecha o sistema:
- Timer garante amostragem estável
- Frame fecha em 256 amostras (~32 ms em 8 kHz)
- Cepstrum roda ~31 vezes por segundo
- LED aciona por detecção robusta