- Implement radix-2 Cooley-Tukey FFT algorithm - Add windowing functions: Hann, Hamming, Blackman, Rectangular - Support configurable frequency bin count - Add peak detection with configurable decay rate - Include temporal smoothing for visualization - Add spectrum analysis example program - Proper memory management with init/close lifecycle Bug-fix: Add buffer bounds checking to prevent overflow when sample count is not power of 2 Signed-off-by: Srikanth Patchava <spatchava@meta.com>
332 lines
11 KiB
C
332 lines
11 KiB
C
/**********************************************************************************************
|
|
*
|
|
* raudio_analyzer - Audio spectrum analysis module for raylib
|
|
*
|
|
* IMPLEMENTATION:
|
|
* Radix-2 Cooley-Tukey FFT with windowing, peak detection, and temporal smoothing.
|
|
* Handles non-power-of-2 input by padding/truncating to nearest power of 2.
|
|
*
|
|
* LICENSE: zlib/libpng
|
|
*
|
|
* Copyright (c) 2024 raylib contributors
|
|
*
|
|
* This software is provided "as-is", without any express or implied warranty. In no event
|
|
* will the authors be held liable for any damages arising from the use of this software.
|
|
*
|
|
**********************************************************************************************/
|
|
|
|
#include "raudio_analyzer.h"
|
|
|
|
#include <math.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#ifndef PI
|
|
#define PI 3.14159265358979323846
|
|
#endif
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// Internal state
|
|
//----------------------------------------------------------------------------------
|
|
static float *spectrumMagnitudes = NULL; // Current magnitude per frequency bin
|
|
static float *spectrumSmoothed = NULL; // Smoothed magnitude per frequency bin
|
|
static float *peakValues = NULL; // Peak-hold values per bin
|
|
static float *fftReal = NULL; // FFT real component buffer
|
|
static float *fftImag = NULL; // FFT imaginary component buffer
|
|
static float *windowCoeffs = NULL; // Precomputed window coefficients
|
|
static SpectrumConfig analyzerConfig = { 0 };
|
|
static bool analyzerReady = false;
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// Internal helpers: forward declarations
|
|
//----------------------------------------------------------------------------------
|
|
static int NextPowerOfTwo(int n);
|
|
static void BitReversalPermutation(float *real, float *imag, int n);
|
|
static void ComputeFFT(float *real, float *imag, int n);
|
|
static void ComputeWindowCoefficients(float *coeffs, int n, WindowFunction func);
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// Lifecycle management
|
|
//----------------------------------------------------------------------------------
|
|
|
|
// Initialize the spectrum analyzer, allocating all internal buffers
|
|
void InitSpectrumAnalyzer(SpectrumConfig config)
|
|
{
|
|
// Ensure bin count is a power of 2
|
|
config.binCount = NextPowerOfTwo(config.binCount);
|
|
if (config.binCount < 4) config.binCount = 4;
|
|
if (config.binCount > MAX_SPECTRUM_BINS) config.binCount = MAX_SPECTRUM_BINS;
|
|
|
|
// Clamp parameters to valid ranges
|
|
if (config.smoothingFactor < 0.0f) config.smoothingFactor = 0.0f;
|
|
if (config.smoothingFactor > 1.0f) config.smoothingFactor = 1.0f;
|
|
if (config.peakDecayRate < 0.0f) config.peakDecayRate = 0.0f;
|
|
if (config.peakDecayRate > 1.0f) config.peakDecayRate = 1.0f;
|
|
if (config.sampleRate <= 0) config.sampleRate = 44100;
|
|
|
|
analyzerConfig = config;
|
|
|
|
// Allocate buffers
|
|
spectrumMagnitudes = (float *)calloc(config.binCount, sizeof(float));
|
|
spectrumSmoothed = (float *)calloc(config.binCount, sizeof(float));
|
|
peakValues = (float *)calloc(config.binCount, sizeof(float));
|
|
fftReal = (float *)calloc(config.binCount, sizeof(float));
|
|
fftImag = (float *)calloc(config.binCount, sizeof(float));
|
|
windowCoeffs = (float *)calloc(config.binCount, sizeof(float));
|
|
|
|
// Precompute window coefficients
|
|
ComputeWindowCoefficients(windowCoeffs, config.binCount, config.windowFunc);
|
|
|
|
analyzerReady = true;
|
|
}
|
|
|
|
// Free all resources used by the spectrum analyzer
|
|
void CloseSpectrumAnalyzer(void)
|
|
{
|
|
free(spectrumMagnitudes); spectrumMagnitudes = NULL;
|
|
free(spectrumSmoothed); spectrumSmoothed = NULL;
|
|
free(peakValues); peakValues = NULL;
|
|
free(fftReal); fftReal = NULL;
|
|
free(fftImag); fftImag = NULL;
|
|
free(windowCoeffs); windowCoeffs = NULL;
|
|
|
|
analyzerReady = false;
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// Spectrum processing
|
|
//----------------------------------------------------------------------------------
|
|
|
|
// Process audio samples: apply window, run FFT, compute magnitudes.
|
|
// Bug-fix: handles non-power-of-2 sampleCount by padding with zeros or truncating.
|
|
void UpdateSpectrum(float *samples, int sampleCount)
|
|
{
|
|
if (!analyzerReady || samples == NULL || sampleCount <= 0) return;
|
|
|
|
int n = analyzerConfig.binCount;
|
|
|
|
// Bounds checking: pad or truncate input to match FFT size (power of 2)
|
|
if (sampleCount >= n)
|
|
{
|
|
// Truncate: only use the first n samples
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
fftReal[i] = samples[i] * windowCoeffs[i];
|
|
fftImag[i] = 0.0f;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Pad: copy available samples, zero-pad the rest
|
|
for (int i = 0; i < sampleCount; i++)
|
|
{
|
|
fftReal[i] = samples[i] * windowCoeffs[i];
|
|
fftImag[i] = 0.0f;
|
|
}
|
|
for (int i = sampleCount; i < n; i++)
|
|
{
|
|
fftReal[i] = 0.0f;
|
|
fftImag[i] = 0.0f;
|
|
}
|
|
}
|
|
|
|
// Perform in-place FFT
|
|
BitReversalPermutation(fftReal, fftImag, n);
|
|
ComputeFFT(fftReal, fftImag, n);
|
|
|
|
// Compute magnitudes for each bin (only first half is unique due to symmetry)
|
|
int halfN = n / 2;
|
|
for (int i = 0; i < halfN; i++)
|
|
{
|
|
spectrumMagnitudes[i] = sqrtf(fftReal[i] * fftReal[i] + fftImag[i] * fftImag[i]) / (float)n;
|
|
}
|
|
// Mirror: zero out upper half (redundant for real input)
|
|
for (int i = halfN; i < n; i++)
|
|
{
|
|
spectrumMagnitudes[i] = 0.0f;
|
|
}
|
|
|
|
// Update peak-hold values with decay
|
|
for (int i = 0; i < halfN; i++)
|
|
{
|
|
if (spectrumMagnitudes[i] > peakValues[i])
|
|
{
|
|
peakValues[i] = spectrumMagnitudes[i];
|
|
}
|
|
else
|
|
{
|
|
peakValues[i] *= analyzerConfig.peakDecayRate;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply temporal smoothing between current and previous spectrum data
|
|
void ApplySmoothing(void)
|
|
{
|
|
if (!analyzerReady) return;
|
|
|
|
float alpha = analyzerConfig.smoothingFactor;
|
|
int halfN = analyzerConfig.binCount / 2;
|
|
|
|
for (int i = 0; i < halfN; i++)
|
|
{
|
|
spectrumSmoothed[i] = alpha * spectrumSmoothed[i] + (1.0f - alpha) * spectrumMagnitudes[i];
|
|
}
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// Data retrieval
|
|
//----------------------------------------------------------------------------------
|
|
|
|
// Get pointer to smoothed spectrum magnitude array
|
|
float *GetSpectrumData(void)
|
|
{
|
|
if (!analyzerReady) return NULL;
|
|
return spectrumSmoothed;
|
|
}
|
|
|
|
// Find the dominant frequency in Hz from the current spectrum
|
|
float GetPeakFrequency(void)
|
|
{
|
|
if (!analyzerReady) return 0.0f;
|
|
|
|
int halfN = analyzerConfig.binCount / 2;
|
|
int peakBin = 0;
|
|
float peakMag = 0.0f;
|
|
|
|
for (int i = 1; i < halfN; i++)
|
|
{
|
|
if (spectrumMagnitudes[i] > peakMag)
|
|
{
|
|
peakMag = spectrumMagnitudes[i];
|
|
peakBin = i;
|
|
}
|
|
}
|
|
|
|
// Convert bin index to frequency: freq = bin * sampleRate / binCount
|
|
return (float)peakBin * (float)analyzerConfig.sampleRate / (float)analyzerConfig.binCount;
|
|
}
|
|
|
|
// Get the magnitude of the largest frequency bin
|
|
float GetPeakMagnitude(void)
|
|
{
|
|
if (!analyzerReady) return 0.0f;
|
|
|
|
int halfN = analyzerConfig.binCount / 2;
|
|
float peakMag = 0.0f;
|
|
|
|
for (int i = 0; i < halfN; i++)
|
|
{
|
|
if (spectrumMagnitudes[i] > peakMag) peakMag = spectrumMagnitudes[i];
|
|
}
|
|
|
|
return peakMag;
|
|
}
|
|
|
|
// Get the configured number of frequency bins
|
|
int GetSpectrumBinCount(void)
|
|
{
|
|
return analyzerConfig.binCount;
|
|
}
|
|
|
|
// Check whether the analyzer has been initialized
|
|
bool IsSpectrumReady(void)
|
|
{
|
|
return analyzerReady;
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------
|
|
// Internal helpers
|
|
//----------------------------------------------------------------------------------
|
|
|
|
// Return the smallest power of 2 >= n
|
|
static int NextPowerOfTwo(int n)
|
|
{
|
|
int power = 1;
|
|
while (power < n) power <<= 1;
|
|
return power;
|
|
}
|
|
|
|
// In-place bit-reversal permutation for radix-2 FFT
|
|
static void BitReversalPermutation(float *real, float *imag, int n)
|
|
{
|
|
int j = 0;
|
|
for (int i = 0; i < n - 1; i++)
|
|
{
|
|
if (i < j)
|
|
{
|
|
// Swap real parts
|
|
float tmpR = real[i];
|
|
real[i] = real[j];
|
|
real[j] = tmpR;
|
|
// Swap imaginary parts
|
|
float tmpI = imag[i];
|
|
imag[i] = imag[j];
|
|
imag[j] = tmpI;
|
|
}
|
|
int k = n >> 1;
|
|
while (k <= j)
|
|
{
|
|
j -= k;
|
|
k >>= 1;
|
|
}
|
|
j += k;
|
|
}
|
|
}
|
|
|
|
// Radix-2 Cooley-Tukey decimation-in-time FFT (in-place)
|
|
static void ComputeFFT(float *real, float *imag, int n)
|
|
{
|
|
for (int step = 2; step <= n; step <<= 1)
|
|
{
|
|
int halfStep = step / 2;
|
|
float angle = -2.0f * (float)PI / (float)step;
|
|
|
|
for (int group = 0; group < n; group += step)
|
|
{
|
|
for (int pair = 0; pair < halfStep; pair++)
|
|
{
|
|
float twiddleReal = cosf(angle * (float)pair);
|
|
float twiddleImag = sinf(angle * (float)pair);
|
|
|
|
int even = group + pair;
|
|
int odd = group + pair + halfStep;
|
|
|
|
// Butterfly operation
|
|
float tR = twiddleReal * real[odd] - twiddleImag * imag[odd];
|
|
float tI = twiddleReal * imag[odd] + twiddleImag * real[odd];
|
|
|
|
real[odd] = real[even] - tR;
|
|
imag[odd] = imag[even] - tI;
|
|
real[even] += tR;
|
|
imag[even] += tI;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Precompute window function coefficients for a given size and type
|
|
static void ComputeWindowCoefficients(float *coeffs, int n, WindowFunction func)
|
|
{
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
switch (func)
|
|
{
|
|
case WINDOW_HANN:
|
|
coeffs[i] = 0.5f * (1.0f - cosf(2.0f * (float)PI * (float)i / (float)(n - 1)));
|
|
break;
|
|
case WINDOW_HAMMING:
|
|
coeffs[i] = 0.54f - 0.46f * cosf(2.0f * (float)PI * (float)i / (float)(n - 1));
|
|
break;
|
|
case WINDOW_BLACKMAN:
|
|
coeffs[i] = 0.42f
|
|
- 0.5f * cosf(2.0f * (float)PI * (float)i / (float)(n - 1))
|
|
+ 0.08f * cosf(4.0f * (float)PI * (float)i / (float)(n - 1));
|
|
break;
|
|
case WINDOW_RECTANGULAR:
|
|
default:
|
|
coeffs[i] = 1.0f;
|
|
break;
|
|
}
|
|
}
|
|
}
|