MCU & FPGA DSP Detecção de Assobio Humano com o Algoritmo de Goertzel no RP2040

Detecção de Assobio Humano com o Algoritmo de Goertzel no RP2040


4 — Goertzel em tempo real no RP2040: ADC + frames + múltiplas frequências (fundamental e harmônicos)

Agora vamos sair do “código de bancada” e colocar Goertzel para rodar em tempo real no RP2040, como fizemos no artigo anterior: amostragem estável, coleta de frame e processamento incremental.

A diferença é que aqui não montamos FFT nem cepstrum. Em vez disso, vamos calcular a energia em poucas frequências (fundamental + harmônicos) e criar um critério de decisão.


4.1 Definindo o frame e a taxa de amostragem

Vamos manter as escolhas do artigo anterior para facilitar comparação:

  • \(f_s = 8000\ \text{Hz}\)
  • \(N = 256\) amostras por frame

Resolução em frequência:

\[
\Delta f = \frac{f_s}{N} \approx 31{,}25\ \text{Hz}
\]

Isso é suficiente para assobio humano.


4.2 Estratégia “Goertzel multibanda”

A pergunta prática: quais frequências devo testar?

Para assobio, há duas abordagens comuns:

A) Varredura de bins na faixa (mais robusta)

  • Testa vários (k) entre 800 e 3000 Hz
  • Escolhe o pico

B) “Fundamental + harmônicos” (mais seletiva)

  • Escolhe um pico candidato na faixa
  • Confirma energia em (2f_0) e (3f_0)

Aqui vamos adotar uma estratégia híbrida, eficiente e robusta:

  1. Rodar Goertzel em uma pequena varredura (ex.: 800–3000 Hz, passo ~62,5 Hz = 2 bins)
  2. Encontrar o melhor candidato (f_0)
  3. Confirmar harmônicos (2f_0) e (3f_0) com mais 2 ou 3 chamadas de Goertzel

Isso reduz falsos positivos sem explodir CPU.


4.3 Aquisição em tempo real (ADC + timer), igual ao artigo anterior

Vamos estruturar igual:

  • Callback de timer coleta amostra do ADC
  • Empurra para buffer de frame
  • Loop principal processa quando frame fecha

4.3.1 Buffer de frame (o mesmo padrão)

#define FS_HZ      8000
#define FRAME_N    256

typedef struct {
    float data[FRAME_N];
    uint32_t idx;
    bool full;
} frame_buf_t;

static inline void frame_buf_init(frame_buf_t *b) {
    b->idx = 0;
    b->full = false;
}

static inline void frame_buf_push(frame_buf_t *b, float v) {
    b->data[b->idx++] = v;
    if (b->idx >= FRAME_N) {
        b->idx = 0;
        b->full = true;
    }
}

4.3.2 Leitura ADC normalizada

static inline float read_adc_normalized(void)
{
    uint16_t raw = adc_read(); // 0..4095
    return ((float)raw - 2048.0f) / 2048.0f;
}

Observação: se seu microfone tiver offset diferente, você pode substituir por uma média móvel depois. Por enquanto, isso já funciona bem na maioria dos módulos.


4.4 Pré-processamento mínimo: remoção de DC por média móvel (mais robusto)

No mundo real, o offset do microfone varia. Vamos remover DC com um filtro de 1ª ordem:

\[
\mu[n] = \mu[n-1] + \beta (x[n] – \mu[n-1])
\]
\[
x_{ac}[n] = x[n] – \mu[n]
\]

Implementação:

typedef struct {
    float mean;
} dc_block_t;

static inline void dc_block_init(dc_block_t *d) { d->mean = 0.0f; }

static inline float dc_block_step(dc_block_t *d, float x)
{
    const float beta = 0.0015f; // ajuste leve
    d->mean += beta * (x - d->mean);
    return x - d->mean;
}

4.5 Varredura com Goertzel (faixa 800–3000 Hz)

Vamos definir a faixa:

  • F_MIN = 800 Hz
  • F_MAX = 3000 Hz

E usar passo de 2*Δf, ou seja, ~62,5 Hz, para reduzir custo.

Número de bins aproximado:

\[
\text{bins} \approx \frac{3000-800}{62.5} \approx 35
\]

Ou seja: ~35 detectores por frame (bem viável no RP2040).

4.5.1 Função: potência Goertzel para uma frequência f0 em um frame

Nós vamos reaproveitar as funções da Seção 3, mas agora em um helper:

static float goertzel_power_at(const float *x, float f0, float fs, int N)
{
    goertzel_t g;
    goertzel_init(&g, f0, fs, N);

    for (int n = 0; n < N; n++) {
        goertzel_step(&g, x[n]);
    }
    return goertzel_power(&g);
}

Sim, aqui re-inicializamos o Goertzel a cada frame. Isso é deliberado: torna o código claro e fácil de manter. Na próxima seção podemos otimizar pré-computando coeficientes.


4.6 Encontrando o candidato de fundamental (pico na faixa)

Agora vamos varrer e pegar o melhor:

#define F_MIN   800.0f
#define F_MAX   3000.0f

static float find_best_f0(const float *frame, float *best_power_out)
{
    const float fs = (float)FS_HZ;
    const float df = fs / (float)FRAME_N;     // ~31.25 Hz
    const float step = 2.0f * df;             // ~62.5 Hz

    float best_f = F_MIN;
    float best_p = -1.0f;

    for (float f = F_MIN; f <= F_MAX; f += step) {
        float p = goertzel_power_at(frame, f, fs, FRAME_N);
        if (p > best_p) {
            best_p = p;
            best_f = f;
        }
    }

    if (best_power_out) *best_power_out = best_p;
    return best_f;
}

4.7 Confirmação por harmônicos (2f0 e 3f0)

Com o candidato f0, confirmamos:

  • energia em 2*f0
  • energia em 3*f0 (opcional)
  • comparações relativas (para não depender só de um limiar absoluto)
static bool harmonic_confirm(const float *frame, float f0, float p0)
{
    const float fs = (float)FS_HZ;

    float f2 = 2.0f * f0;
    float f3 = 3.0f * f0;

    // harmônicos devem estar dentro do Nyquist (fs/2)
    if (f2 > fs * 0.5f) return false;

    float p2 = goertzel_power_at(frame, f2, fs, FRAME_N);
    float p3 = (f3 <= fs * 0.5f) ? goertzel_power_at(frame, f3, fs, FRAME_N) : 0.0f;

    // Critério prático:
    // - fundamental precisa ser forte
    // - 2º harmônico deve existir com fração mínima
    // - 3º harmônico ajuda, mas pode ser fraco dependendo do assobio
    const float r2 = (p2 / (p0 + 1e-9f));
    const float r3 = (p3 / (p0 + 1e-9f));

    bool ok2 = (r2 > 0.08f);     // ajuste prático
    bool ok3 = (r3 > 0.02f);     // opcional

    return ok2 || ok3;
}

4.8 Integração: montar o “processador de frame” (ainda sem LED)

Agora juntamos:

  1. energia do frame (gate de silêncio)
  2. encontrar melhor f0
  3. confirmar harmônicos
  4. retornar um “score” de detecção
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;
}

static bool process_frame_goertzel(const float *frame, float *f0_out, float *p0_out)
{
    const float E = frame_energy(frame);
    const float E_MIN = 0.0008f; // ajuste

    if (E < E_MIN) return false;

    float p0 = 0.0f;
    float f0 = find_best_f0(frame, &p0);

    if (!harmonic_confirm(frame, f0, p0)) return false;

    if (f0_out) *f0_out = f0;
    if (p0_out) *p0_out = p0;
    return true;
}

0 0 votos
Classificação do artigo
Inscrever-se
Notificar de
guest
0 Comentários
mais antigos
mais recentes Mais votado
Feedbacks embutidos
Ver todos os comentários

Related Post

0
Adoraria saber sua opinião, comente.x