Consumidor: estimando a frequência principal do stream e imprimindo na UART
Agora que o produtor já entrega apenas os trechos “ativos” (delimitados pelo VAD), o consumidor precisa responder: “qual é a frequência dominante desse trecho?” e imprimir isso continuamente na porta serial.
Em MCU, existem duas abordagens práticas:
- Goertzel (recomendado aqui)
Você escolhe uma faixa de frequências e “varre” bins (por exemplo 80 Hz até 2 kHz). O Goertzel calcula a energia de cada bin sem FFT completa. É excelente quando você quer o pico dominante e quer controlar custo. - FFT curta (512/1024 pontos)
Funciona, mas aumenta dependência de libs DSP e costuma ser mais sensível a detalhes (janela, normalização, espectro de ruído, etc.). Para um artigo didático e robusto no Zephyr+ESP32, Goertzel é o melhor “primeiro degrau”.
Abaixo eu implemento Goertzel com:
- janela Hann (reduz vazamento espectral),
- acúmulo de janela de tamanho fixo (ex.: 1024 amostras),
- varredura de bins com passo ajustável,
- impressão na UART via
printk().
Observação: “frequência principal” em voz humana é um tema delicado (pitch vs formantes). O algoritmo abaixo te dá a frequência dominante do espectro naquela janela. Para pitch “de verdade”, depois você pode evoluir para autocorrelação/CEPSTRUM/AMDF. Mas como requisito do artigo: frequência principal do stream coletado, isso atende bem e é ótimo como base.
4.1 Parâmetros do DSP
No seu app_stream.h (ou num dsp_cfg.h), defina:
/* DSP: tamanho da janela analisada */
#define DSP_WIN_N 1024u
/* taxa de amostragem: deve bater com o produtor (ADC_SAMPLE_RATE_HZ) */
#define DSP_FS_HZ 16000u
/* varredura: faixa e passo (ajuste para custo x resolução) */
#define DSP_FMIN_HZ 80u
#define DSP_FMAX_HZ 2000u
#define DSP_FSTEP_HZ 20u /* 20 Hz por bin -> custo moderado */
/* “ignorar silêncio”: energia mínima para reportar */
#define DSP_ENERGY_MIN 800u
4.2 Implementação do Goertzel + janela Hann (tudo em C puro)
Crie src/dsp_goertzel.c:
#include <stdint.h>
#include <math.h>
/*
* Janela Hann: w[n] = 0.5 - 0.5*cos(2*pi*n/(N-1))
* Em MCU, você pode:
* - pré-computar w[] em float (custo de ROM),
* - ou calcular uma vez e manter,
* - ou usar aproximação fixa.
*
* Aqui eu calculo 1 vez e guardo em float.
*/
static float s_hann[DSP_WIN_N];
static int s_hann_init;
static void hann_init(void)
{
if (s_hann_init) return;
const float two_pi = 6.283185307179586f;
for (uint32_t n = 0; n < DSP_WIN_N; n++) {
float x = (float)n / (float)(DSP_WIN_N - 1);
s_hann[n] = 0.5f - 0.5f * cosf(two_pi * x);
}
s_hann_init = 1;
}
/*
* Goertzel:
* s[n] = x[n] + 2*cos(w)*s[n-1] - s[n-2]
* P = s[N-1]^2 + s[N-2]^2 - 2*cos(w)*s[N-1]*s[N-2]
*
* Onde w = 2*pi*f/fs.
*/
static float goertzel_power(const float *x, uint32_t N, float fs_hz, float f_hz)
{
float w = 2.0f * 3.141592653589793f * (f_hz / fs_hz);
float cw = cosf(w);
float coeff = 2.0f * cw;
float s0 = 0.0f, s1 = 0.0f, s2 = 0.0f;
for (uint32_t n = 0; n < N; n++) {
s0 = x[n] + coeff * s1 - s2;
s2 = s1;
s1 = s0;
}
float p = (s1 * s1) + (s2 * s2) - (coeff * s1 * s2);
return p;
}
/*
* Calcula frequência dominante:
* - aplica Hann
* - estima energia média (absmean) para ignorar silêncio
* - varre bins com Goertzel e pega o maior
*/
uint32_t dsp_find_dominant_freq_hz(const int16_t *samples, uint32_t N, uint32_t fs_hz, uint32_t *out_energy)
{
hann_init();
/* buffer float local (pode ser estático para evitar stack grande) */
static float x[DSP_WIN_N];
/* Energia simples (absmean) + janela */
uint32_t acc_abs = 0;
for (uint32_t i = 0; i < N; i++) {
int32_t a = samples[i];
if (a < 0) a = -a;
acc_abs += (uint32_t)a;
/* normaliza int16 para float e aplica Hann */
x[i] = ((float)samples[i] / 32768.0f) * s_hann[i];
}
uint32_t e = (N > 0) ? (acc_abs / N) : 0;
if (out_energy) *out_energy = e;
if (e < DSP_ENERGY_MIN) {
return 0; /* “sem frequência” */
}
float best_p = 0.0f;
uint32_t best_f = 0;
for (uint32_t f = DSP_FMIN_HZ; f <= DSP_FMAX_HZ; f += DSP_FSTEP_HZ) {
float p = goertzel_power(x, N, (float)fs_hz, (float)f);
if (p > best_p) {
best_p = p;
best_f = f;
}
}
return best_f;
}
Se você achar “float pesado”, eu te passo a versão fixa (Q15/Q31) na próxima etapa. Para ESP32, float costuma ser viável, mas depende do seu orçamento de CPU e da taxa de chamadas.
4.3 Consumidor completo: acumulando janela, processando em VOICE_END (e também “ao vivo”)
Agora substituímos o consumidor placeholder.
Estratégia de processamento (robusta):
- Em
VOICE_START: zera estado. - Em cada
AUDIO_FRAME: copia para um buffer de janela circular até completarDSP_WIN_N. - Você pode:
- calcular a frequência continuamente (a cada janela cheia), ou
- calcular somente no final (
VOICE_END) com as últimasDSP_WIN_Namostras do trecho.
Vou fazer os dois: imprime continuamente a cada janela cheia durante fala, e imprime um resumo quando termina.
/* src/consumer_dsp_uart.c */
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#include <stdint.h>
#include <string.h>
#include "app_stream.h"
/* protótipo do DSP */
uint32_t dsp_find_dominant_freq_hz(const int16_t *samples, uint32_t N, uint32_t fs_hz, uint32_t *out_energy);
extern struct k_msgq stream_q;
extern struct k_mem_slab frame_slab;
/* buffer de janela */
static int16_t s_win[DSP_WIN_N];
static uint32_t s_win_fill;
/* estatística do stream */
static uint32_t s_frames_in_stream;
static uint32_t s_last_freq;
static uint32_t s_last_energy;
static void reset_stream_state(void)
{
s_win_fill = 0;
s_frames_in_stream = 0;
s_last_freq = 0;
s_last_energy = 0;
memset(s_win, 0, sizeof(s_win));
}
/* copia amostras para janela até completar; retorna 1 quando encheu */
static int win_push(const int16_t *x, uint32_t n)
{
while (n > 0) {
uint32_t space = DSP_WIN_N - s_win_fill;
uint32_t take = (n < space) ? n : space;
memcpy(&s_win[s_win_fill], x, take * sizeof(int16_t));
s_win_fill += take;
x += take;
n -= take;
if (s_win_fill >= DSP_WIN_N) {
return 1;
}
}
return 0;
}
void dsp_uart_thread(void *p1, void *p2, void *p3)
{
(void)p1; (void)p2; (void)p3;
reset_stream_state();
printk("DSP consumer started (Goertzel dominant frequency)\n");
while (1) {
struct stream_msg msg;
k_msgq_get(&stream_q, &msg, K_FOREVER);
if (msg.type == STREAM_MSG_VOICE_START) {
printk("[VAD] START\n");
reset_stream_state();
continue;
}
if (msg.type == STREAM_MSG_VOICE_END) {
/* resumo final */
printk("[VAD] END frames=%u last_f=%u Hz energy=%u\n",
s_frames_in_stream, s_last_freq, s_last_energy);
reset_stream_state();
continue;
}
if (msg.type == STREAM_MSG_AUDIO_FRAME && msg.frame) {
s_frames_in_stream++;
/* empilha amostras na janela */
int full = win_push(msg.frame->samples, msg.frame->n);
/* devolve frame imediatamente (não segura slab) */
k_mem_slab_free(&frame_slab, (void *)msg.frame);
if (full) {
/* janela completa: calcula frequência dominante */
uint32_t energy = 0;
uint32_t f = dsp_find_dominant_freq_hz(s_win, DSP_WIN_N, DSP_FS_HZ, &energy);
s_last_freq = f;
s_last_energy = energy;
if (f > 0) {
printk("Fdom=%u Hz (energy=%u)\n", f, energy);
}
/* “janela deslizante”: aqui eu reinicio.
Se quiser overlap (ex.: 50%), eu ajusto na próxima seção. */
s_win_fill = 0;
}
}
}
}
No main.c, na criação do consumidor, troque para dsp_uart_thread.
4.4 Resultado esperado na UART
Durante fala (stream ativo), você começa a ver algo como:
[VAD] START
Fdom=180 Hz (energy=1430)
Fdom=200 Hz (energy=1505)
Fdom=180 Hz (energy=1390)
...
[VAD] END frames=73 last_f=180 Hz energy=1412
O número em Hz vai depender:
- do seu microfone/circuito analógico,
- do
DSP_FSTEP_HZ(passo de varredura), - do threshold do VAD e da escala das amostras,
- e do conteúdo do sinal.