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


5 — Decisor completo + LED + otimizações (coeficientes pré-computados, EMA, histerese) e integração no RP2040

Nesta seção vamos transformar o “processador de frame” da Seção 4 em um detector de assobio pronto para uso, com:

  • Coeficientes do Goertzel pré-computados (evita cosf() repetido)
  • Gate por energia
  • Critério espectral (fundamental + harmônicos)
  • Histerese para evitar “pisca-pisca”
  • Suavização temporal (EMA) do score
  • Acionamento do LED via GPIO
  • Integração com ADC + timer no RP2040

Ainda não é o “arquivo final completo”. Vamos fechar agora a arquitetura; na próxima seção eu entrego o código completo e funcional em um único main.c, como no artigo anterior.


5.1 Otimização essencial: pré-computar os detectores da varredura

Na Seção 4, goertzel_power_at() calculava k, w, cosf() toda vez. Isso é ótimo para didática, mas em firmware real podemos fazer melhor:

  • Criar uma lista de frequências de varredura
  • Pré-calcular o coeficiente coeff = 2*cos(w)
  • No frame, só rodar a recorrência

5.1.1 Estrutura do “detector preparado”

typedef struct {
    float f;       // frequência (Hz)
    float coeff;   // 2*cos(2*pi*k/N)
} goertzel_bin_t;

5.1.2 Construindo a lista de bins (800–3000 Hz, passo 2 bins)

#define FS_HZ    8000
#define FRAME_N  256

#define F_MIN    800.0f
#define F_MAX    3000.0f

#define MAX_BINS  64

static goertzel_bin_t g_bins[MAX_BINS];
static int g_bins_count = 0;

static void goertzel_bins_init(void)
{
    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

    g_bins_count = 0;

    for (float f = F_MIN; f <= F_MAX && g_bins_count < MAX_BINS; f += step) {
        int k = (int)(0.5f + ((float)FRAME_N * f) / fs);
        float w = (2.0f * (float)M_PI * (float)k) / (float)FRAME_N;

        g_bins[g_bins_count].f = f;
        g_bins[g_bins_count].coeff = 2.0f * cosf(w);
        g_bins_count++;
    }
}

Agora a varredura não precisa mais de cosf() em runtime.


5.2 Goertzel “rápido” usando coeff pré-calculado

Com coeff pronto, o loop fica bem enxuto:

static float goertzel_power_coeff(const float *x, float coeff)
{
    float s1 = 0.0f;
    float s2 = 0.0f;

    for (int n = 0; n < FRAME_N; n++) {
        float s0 = x[n] + coeff * s1 - s2;
        s2 = s1;
        s1 = s0;
    }

    return (s1*s1) + (s2*s2) - (coeff*s1*s2);
}

5.3 Encontrar o melhor candidato f0 (varredura)

static float find_best_f0_fast(const float *frame, float *p0_out)
{
    float best_f = g_bins[0].f;
    float best_p = -1.0f;

    for (int i = 0; i < g_bins_count; i++) {
        float p = goertzel_power_coeff(frame, g_bins[i].coeff);
        if (p > best_p) {
            best_p = p;
            best_f = g_bins[i].f;
        }
    }

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

5.4 Harmônicos com cálculo “on demand”

Para harmônicos, não vale manter tabela grande. A gente calcula coeff pontualmente:

static inline float coeff_for_freq(float f)
{
    const float fs = (float)FS_HZ;
    int k = (int)(0.5f + ((float)FRAME_N * f) / fs);
    float w = (2.0f * (float)M_PI * (float)k) / (float)FRAME_N;
    return 2.0f * cosf(w);
}

static float power_at_freq(const float *frame, float f)
{
    float c = coeff_for_freq(f);
    return goertzel_power_coeff(frame, c);
}

5.5 Critério espectral composto (score)

Agora vamos criar um score numérico em vez de true/false. Isso facilita EMA e histerese.

Uma forma boa e simples:

  • p0: potência no candidato f0
  • p2: potência em 2f0
  • p3: potência em 3f0 (se existir)
  • Normalização por energia do frame (para não depender do volume)

Score proposto:

\[
S = \frac{p0}{E+\epsilon} + \lambda_2\frac{p2}{p0+\epsilon} + \lambda_3\frac{p3}{p0+\epsilon}
\]

Implementaçã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 float compute_whistle_score(const float *frame, float *f0_out)
{
    const float eps = 1e-9f;

    float E = frame_energy(frame);
    if (E < E_MIN) return 0.0f;

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

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

    const float nyq = 0.5f * (float)FS_HZ;

    float p2 = (f2 <= nyq) ? power_at_freq(frame, f2) : 0.0f;
    float p3 = (f3 <= nyq) ? power_at_freq(frame, f3) : 0.0f;

    // pesos práticos
    const float l2 = 0.7f;
    const float l3 = 0.35f;

    float s0 = p0 / (E + eps);
    float r2 = p2 / (p0 + eps);
    float r3 = p3 / (p0 + eps);

    float S = s0 + l2 * r2 + l3 * r3;

    if (f0_out) *f0_out = f0;
    return S;
}

5.6 Decisão robusta com EMA + histerese

  • S_filt = EMA(S)
  • Liga quando S_filt > S_ON
  • Desliga quando S_filt < S_OFF
static float g_S_filt = 0.0f;
static bool  g_state = false;

static void update_led_from_score(float S, float f0, uint led_gpio)
{
    // suaviza
    g_S_filt = EMA_ALPHA * g_S_filt + (1.0f - EMA_ALPHA) * S;

    // sanity: garante faixa típica do assobio
    bool f_ok = (f0 >= 700.0f && f0 <= 3500.0f);

    // limiares iniciais (calibração depois)
    const float S_ON  = 25.0f;
    const float S_OFF = 18.0f;

    if (!g_state) {
        if (f_ok && g_S_filt > S_ON) g_state = true;
    } else {
        if (!f_ok || g_S_filt < S_OFF) g_state = false;
    }

    gpio_put(led_gpio, g_state ? 1 : 0);
}

Esses valores (S_ON, S_OFF) dependem do microfone, do ganho e do ambiente — mas o formato do score torna a calibração bem mais previsível do que limiar bruto em p0.


5.7 Integração de tempo real (ADC + DC-block + frame)

Vamos unir tudo num fluxo igual ao artigo anterior.

5.7.1 DC-block

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;
    d->mean += beta * (x - d->mean);
    return x - d->mean;
}

5.7.2 Estado global e callback de amostragem

static frame_buf_t g_frame;
static dc_block_t  g_dc;

static bool sample_timer_cb(repeating_timer_t *rt)
{
    (void)rt;

    float x = read_adc_normalized();
    x = dc_block_step(&g_dc, x);

    frame_buf_push(&g_frame, x);
    return true;
}

5.7.3 Loop processa frame e atualiza LED

static void process_frame_if_ready(uint led_gpio)
{
    if (!g_frame.full) return;
    g_frame.full = false;

    float f0 = 0.0f;
    float S = compute_whistle_score(g_frame.data, &f0);

    update_led_from_score(S, f0, led_gpio);
}

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

LPCC (Linear Prediction Cepstral Coefficients): Fundamentos, Algoritmos e Aplicações em Sistemas EmbarcadosLPCC (Linear Prediction Cepstral Coefficients): Fundamentos, Algoritmos e Aplicações em Sistemas Embarcados

Os coeficientes cepstrais por predição linear (LPCC) são uma técnica clássica e altamente eficiente para extração de características em sinais de fala, vibração e acústica industrial. Neste artigo, apresentamos uma

Quefrequência e Análise Cepstral: Uma Introdução Prática para Sistemas Embarcados (ESP32-P4)Quefrequência e Análise Cepstral: Uma Introdução Prática para Sistemas Embarcados (ESP32-P4)

A análise cepstral e o conceito de quefrequência são técnicas essencialmente poderosas no processamento de sinais de áudio, permitindo separar efeitos de excitação, resposta acústica e periodicidades espectrais que não

0
Adoraria saber sua opinião, comente.x