// // Tiny in-place radix-2 Cooley-Tukey FFT. Module-level scratch buffers so we // don't allocate per frame. // // FFT_SIZE is a compile-time constant (power of two). 1024 at 44.1 kHz gives // ~23 ms windows and ~43 Hz bin resolution — a good balance for the // spectrum visualizer. // FFT_SIZE :: 1024; HALF_FFT :: FFT_SIZE / 2; fft_window: [FFT_SIZE] float; fft_re: [FFT_SIZE] float; fft_im: [FFT_SIZE] float; fft_initted: bool; fft_init :: () { if fft_initted return; // Hann window: smooth attenuation at edges, no spectral leakage from // arbitrary-phase sample boundaries. for 0..FFT_SIZE-1 { t := cast(float) it / cast(float) (FFT_SIZE - 1); fft_window[it] = 0.5 - 0.5 * cos(2 * PI * t); } fft_initted = true; } // // In-place complex FFT on (fft_re, fft_im). // fft :: () { n := FFT_SIZE; // Bit-reverse permutation. j := 0; for i: 0..n-2 { if i < j { t_re := fft_re[i]; t_im := fft_im[i]; fft_re[i] = fft_re[j]; fft_im[i] = fft_im[j]; fft_re[j] = t_re; fft_im[j] = t_im; } bit := n >> 1; while j & bit { j ^= bit; bit >>= 1; } j ^= bit; } // Cooley-Tukey butterflies. seg_len := 2; while seg_len <= n { ang := -2.0 * PI / cast(float) seg_len; w_re := cos(ang); w_im := sin(ang); i := 0; while i < n { tw_re := 1.0; tw_im := 0.0; half := seg_len / 2; for k: 0..half-1 { a := i + k; b := i + k + half; v_re := fft_re[b] * tw_re - fft_im[b] * tw_im; v_im := fft_re[b] * tw_im + fft_im[b] * tw_re; fft_re[b] = fft_re[a] - v_re; fft_im[b] = fft_im[a] - v_im; fft_re[a] = fft_re[a] + v_re; fft_im[a] = fft_im[a] + v_im; next_re := tw_re * w_re - tw_im * w_im; next_im := tw_re * w_im + tw_im * w_re; tw_re = next_re; tw_im = next_im; } i += seg_len; } seg_len *= 2; } }