Fundamentos · 01

FFT e Janelas

Todo número que o Spectra mostra — barra de RTA, dB(A) do SPL, magnitude da função de transferência — nasce da mesma transformada. Esta página descreve o motor espectral comum a todas as telas.

A transformada

O FFTProcessor usa a FFT real-empacotada da Accelerate (vDSP_fft_zrip), radix-2 (Cooley-Tukey), o que exige que o tamanho da janela de análise seja sempre potência de 2 — de 1024 a 65536 amostras no Spectra.

A entrada real é empacotada em formato complexo (vDSP_ctoz) antes da transformada direta. No layout packed-real do vDSP, o bin 0 do resultado mistura o componente DC (parte real) e o componente de Nyquist (parte imaginária) — o Spectra não desembaraça os dois porque ambos ficam fora da faixa de interesse (20 Hz–20 kHz).

Além da magnitude normalizada (usada pelo RTA e pelo SPL), o processador expõe o espectro complexo bruto, sem normalização (complexSpectrum) — é o que alimenta a função de transferência e o delay finder, onde razões entre espectros cancelam qualquer fator de escala comum — e uma FFT inversa (inverseReal) usada para trazer a correlação cruzada de volta ao domínio do tempo no delay finder.

Janelas

Quatro janelas de análise estão disponíveis, geradas pelas funções nativas da Accelerate: retangular (nenhuma, na prática), Hann, Hamming e Blackman. Cada uma troca resolução em frequência por vazamento espectral de forma diferente — três propriedades resumem essa troca:

JanelaGanho coerenteENBW (bins)Lóbulo principal (bins)
Retangular1,01,02
Hann0,51,54
Hamming0,541,36284
Blackman0,421,72696

Valores nominais de referência1. O FFTProcessor não confia nesses números diretamente: ele remede o ganho coerente e a ENBW a partir dos coeficientes reais gerados pela Accelerate para cada janela, então qualquer diferença de implementação entre o vDSP e a literatura clássica já entra corrigida.

Normalização independente de janela

Este é o ponto mais sutil do motor espectral — e o que motivou a revisão de precisão da v0.9.0. A convenção de escala do Spectra é: um tom senoidal de amplitude de pico 1,0, alinhado a um bin, soma 1,0 linear (0 dB) ao redor do seu lóbulo principal — sempre, não importa o tamanho da FFT nem a janela escolhida.

Normalização de potência por bin
$$ \text{normalizado}[k] = \dfrac{|X[k]|^2}{N^2 \cdot \overline{w}^{\,2} \cdot \mathrm{ENBW}} $$

onde N é o tamanho da FFT (o vDSP_fft_zrip devolve o resultado escalado por N em amplitude, logo em potência), é o ganho coerente e ENBW a largura de banda de ruído equivalente — ambos medidos a partir da janela em uso.

Por que isso importa Sem dividir por ENBW, a largura de banda de ruído da janela "vaza" para dentro de toda soma de bins. Antes da correção, o SPL lia 1,76 dB alto com janela Hann, e trocar Hamming por Blackman movia a leitura 2,4 dB — sem nada mudar no microfone. A calibração de SPL precisou ser refeita uma vez depois dessa correção.

Consequência direta: o pico de um único bin isolado (sem somar a energia do lóbulo inteiro) fica −10·log₁₀(ENBW) abaixo de 0 dB — −1,76 dB em Hann, por exemplo. Isso é revertido explicitamente onde faz sentido (ver interpolação de pico, abaixo).

Interpolação parabólica de pico

A grade de bins de uma FFT tem resolução finita (Δf = taxa de amostragem / N): um tom de 100 Hz raramente cai exatamente sobre um bin. Ler "a frequência é a do bin mais alto" arredonda para a grade — o PROGRESS do projeto registra um tom de 100 Hz sendo rotulado 105,9 Hz antes desta correção.

O SpectralPeak ajusta uma parábola sobre o logaritmo (dB) das magnitudes de três bins vizinhos ao pico — o lóbulo principal de uma janela Hann/Blackman perto do topo tem formato aproximadamente parabólico em escala log:

Interpolação parabólica de 3 pontos (log-magnitude)
$$ \delta = \frac{1}{2} \cdot \frac{\alpha - \gamma}{\alpha - 2\beta + \gamma} \qquad\qquad f_{\text{pico}} = (k + \delta)\cdot \Delta f $$

onde α, β, γ são as magnitudes em dB dos bins k−1, k e k+1.

Esta é a forma clássica de interpolação parabólica de pico espectral2. O deslocamento δ é limitado a ±0,5 bin: um valor maior indicaria que os três pontos não formam um único lóbulo isolado (ruído, ou dois tons se batendo), e extrapolar além do bin vizinho inventaria uma frequência que não está no sinal. A potência interpolada é multiplicada de volta pela ENBW da janela, revertendo a perda de escalonamento (scalloping loss) descrita acima — sem isso, o nível do pico isolado sairia sistematicamente baixo.

Resultado medido: tons localizados com erro abaixo de 0,03 Hz e nível dentro de 0,35 dB do valor teórico, em toda a faixa de frequência testada.

Resolução automática de FFT

Quanto mais fina a fração de oitava (1/48, por exemplo), mais estreita é cada banda — e uma banda pode ficar mais estreita do que o lóbulo principal da janela consegue resolver. O AnalysisResolution calcula, para cada combinação de frequência/fração/janela, se a FFT em uso resolve a banda inteira ou não, e escolhe automaticamente um tamanho de FFT suficiente.

Resolução por bin e duração da janela
$$ \Delta f = \frac{f_s}{N} \qquad\qquad T_{\text{janela}} = \frac{N}{f_s} $$

Um tom qualquer se espalha por lóbulo principal (bins) × Δf Hz de espectro. Uma banda de fração 1/n em frequência f tem largura f · largura relativa(n). A banda só contém o tom inteiro quando é mais larga que esse espalhamento — dois limiares, calibrados por medição direta sobre todas as combinações de fração, tamanho de FFT e frequência que o app oferece:

Fração de oitavaFFT automática
até 1/34096
até 1/88192
até 1/1616384
1/24 e acima (até 1/48)32768

Tamanhos manuais vão até 65536 (Ajustes). Custo no pior caso (1/48, FFT de 32768): 0,15 ms de FFT + 0,22 ms de integração de bandas por quadro — contra um orçamento de 33 ms a 30 quadros por segundo.

Essa limitação não é um bug corrigível por fator de calibração: é física — nenhuma FFT de comprimento fixo resolve uma fração estreita o bastante em frequências baixas (uma banda de 1/48 em 125 Hz tem 1,8 Hz de largura; a janela Hann já espalha um tom por 4 bins). As únicas saídas reais são uma janela mais longa (mais latência) ou um banco de filtros dedicado — por isso o Spectra declara o limite em vez de escondê-lo.