4 — Cepstrum para detectar o assobio (conceito + coleta de frames + log via Taylor)
4.1 O que é “cepstrum” e por que ele funciona tão bem para assobios
O assobio é quase periódico. Se o período do sinal for (T_0), então a frequência fundamental é:
\[
f_0 = \frac{1}{T_0}
\]
O cepstrum é uma técnica que “transforma periodicidade em pico”. A intuição é:
- Um sinal periódico tem harmônicas no espectro (picos em \(f_0, 2f_0, 3f_0, \dots)\).
- Quando você aplica log no espectro, você “comprime” diferenças de amplitude e evidencia a estrutura harmônica.
- Ao fazer a transformada inversa (no caso do real cepstrum, é IFFT do log-magnitude), aparece um pico em quefrência perto de \(T_0\).
A “quefrência” (que vem de brincar com “frequency”) é um eixo em tempo medido em amostras ou segundos. Se (f_s) é a taxa de amostragem, então o pico de quefrência em amostras (q_0) se relaciona com a frequência fundamental por:
\[
f_0 \approx \frac{f_s}{q_0}
\]
Para assobio, esse pico costuma ser muito mais estável do que em fala.
4.2 Qual cepstrum vamos implementar no RP2040
Vamos implementar o real cepstrum clássico:
- Pegamos um frame \(x[n]\) do sinal filtrado
- Calculamos FFT \(\rightarrow X[k]\)
- Magnitude: \(|X[k]|\)
- Log-magnitude: \(\log(|X[k]| + \epsilon)\)
- IFFT \(\rightarrow c[n]\) (o cepstrum)
- Procuramos o pico de (c[n]) numa faixa de quefrência compatível com assobio
Isso é robusto e prático — e no RP2040 dá para fazer com FFT pequena (ex.: 256 pontos) em tempo real.
Importante: nós já usamos Taylor como “operador diferencial”. Agora vamos usar Taylor também para aproximar o log, evitando logf() pesada.
4.3 Coleta de frame (buffer) do sinal já filtrado
Vamos trabalhar com:
- \(f_s \approx 8000\ \text{Hz}\)
- FFT de 256 pontos (frame curto, bom para tempo real)
Faixa típica de assobio (800–3000 Hz) → períodos entre:
\[
T_{min} = \frac{1}{3000} \approx 0{,}333\text{ ms}
\quad,\quad
T_{max} = \frac{1}{800} = 1{,}25\text{ ms}
\]
Em amostras (com (f_s=8000)):
\[
q_{min} \approx \frac{8000}{3000} \approx 2{,}67 \Rightarrow 3
\quad,\quad
q_{max} = \frac{8000}{800} = 10
\]
Então o pico de quefrência esperado fica mais ou menos entre 3 e 10 amostras (bem pequeno!). Na prática, por janela/ruído, é comum procurar numa faixa um pouco maior, tipo 3 a 20.
Vamos adicionar um buffer circular simples e um coletor de frame.
4.3.1 Código incremental: buffer e coleta
#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;
}
}
full=truesignifica: “já tenho um frame completo pronto para análise”.
4.4 Logaritmo aproximado com Série de Taylor (sem logf())
O log natural pode ser aproximado por:
\[
\ln(1+u) = u – \frac{u^2}{2} + \frac{u^3}{3} – \frac{u^4}{4} + \dots
\quad \text{para } |u|<1
\]
Para usar isso em qualquer valor positivo (x), fazemos redução de faixa:
- Reescreva \(x = m \cdot 2^e\), onde \(m \in [1,2)\)
- Então:
\[
\ln(x) = \ln(m) + e\ln(2)
\] - E como (m \in [1,2)), definimos (u = m-1 \in [0,1)) e aplicamos Taylor em (\ln(1+u))
No C, dá para fazer frexpf() (bem leve) para obter mantissa/expoente.
4.4.1 Implementação (Taylor truncada)
#include <math.h>
static inline float ln_taylor_1pu(float u)
{
// ln(1+u), u em [0, 1)
// Série truncada (5 termos): u - u^2/2 + u^3/3 - u^4/4 + u^5/5
float u2 = u * u;
float u3 = u2 * u;
float u4 = u2 * u2;
float u5 = u4 * u;
return (u)
- (0.5f * u2)
+ (0.3333333f * u3)
- (0.25f * u4)
+ (0.2f * u5);
}
static inline float ln_approx(float x)
{
// x > 0
int e = 0;
float m = frexpf(x, &e); // x = m * 2^e, m in [0.5, 1)
// vamos trazer m para [1,2): m2 = m*2, e2 = e-1
float m2 = m * 2.0f;
int e2 = e - 1;
const float LN2 = 0.69314718056f;
float u = m2 - 1.0f; // u in [0,1)
return ln_taylor_1pu(u) + ((float)e2) * LN2;
}
Essa aproximação é ótima para “log-magnitude” do cepstrum porque:
- A gente só precisa de monotonicidade e compressão
- Erro pequeno não quebra a detecção por pico
- Evita custo de
logf()em loop
4.5 Loop já pronto: filtro + coleta de frame
Agora juntamos o que já temos (seção anterior) com o buffer de frame:
static taylor_filter_t g_filter = {0};
static frame_buf_t g_frame;
void dsp_init(void) {
frame_buf_init(&g_frame);
}
void dsp_step(void)
{
float x = read_adc_normalized();
float y = taylor_bandpass_step(&g_filter, x);
frame_buf_push(&g_frame, y);
}
E no main() (ainda incremental):
dsp_init();
while (true) {
dsp_step();
sleep_us(1000000 / FS_HZ);
if (g_frame.full) {
// Próxima seção: FFT -> log -> IFFT -> cepstrum -> pico -> LED
g_frame.full = false;
}
}
Até aqui:
- Já filtramos o áudio
- Já coletamos frames
- Já temos
ln_approx()pronto para usar no cepstrum