OxiFFT spectrum analyzer

Spectrogram (frequency content over time)
Reduced motion is enabled, so only the instantaneous spectrum is shown.
Instantaneous spectrum plot
Input signal

FFT size
Window function
oxifftv— (Pure Rust, Apache-2.0)
targetwasm32-unknown-unknown
wasm size179 KB (gzip 74 KB)
FFT size2048
frame time
frames computed0
server round-trips0
C / C++ / Fortran0 bytes

Implementation code

// crates/oxifft-wasm/src/analyzer.rs:107-140 — verbatim, the code running above
pub fn process_inner(&mut self, samples: &[f32]) -> Result<Vec<f32>, OxiFftWasmError> {
    if samples.len() != self.fft_size {
        return Err(OxiFftWasmError::FrameLength {
            expected: self.fft_size,
            got: samples.len(),
        });
    }

    // 1. Apply the window.
    for ((windowed, &sample), &coefficient) in
        self.scratch.iter_mut().zip(samples).zip(&self.window)
    {
        *windowed = sample * coefficient;
    }

    // 2. Unnormalised forward real FFT -> fft_size/2 + 1 complex bins.
    self.solver
        .execute(&self.scratch, &mut self.spectrum_scratch);

    // 3. Magnitude -> single-sided amplitude -> dBFS.
    //    Bin 0 is DC and has no mirror partner, so it is not doubled. The
    //    Nyquist bin (fft_size/2) is the other undoubled bin, but it is
    //    dropped here: the contract is a spectrum of length fft_size/2.
    let bins = self.fft_size / 2;
    let mut decibels = Vec::with_capacity(bins);
    for (index, bin) in self.spectrum_scratch.iter().take(bins).enumerate() {
        let fold = if index == 0 { 1.0_f32 } else { 2.0_f32 };
        let amplitude = fold * bin.norm() * self.inv_gain;
        decibels.push(20.0_f32 * amplitude.max(AMPLITUDE_FLOOR).log10());
    }

    self.frames = self.frames.saturating_add(1);
    Ok(decibels)
}

This is the code running above, right now.