feat: add FFT-based audio spectrum analyzer module

- 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>
This commit is contained in:
Srikanth Patchava 2026-04-25 01:26:20 -07:00
parent f9bf646388
commit 872dbb7264
3 changed files with 553 additions and 0 deletions

View File

@ -0,0 +1,126 @@
/*******************************************************************************************
*
* raylib [audio] example - Audio Spectrum Analyzer
*
* Example originally created with raylib, using raudio_analyzer module
*
* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
* BSD-like license that allows static linking with closed source software
*
* Copyright (c) 2024 raylib contributors
*
********************************************************************************************/
#include "raylib.h"
#include "../src/raudio_analyzer.h"
#define SCREEN_WIDTH 800
#define SCREEN_HEIGHT 450
#define NUM_BARS 64 // Number of frequency bars to draw
#define FFT_SIZE 512 // FFT bin count (must be power of 2)
int main(void)
{
// Initialization
//--------------------------------------------------------------------------
InitWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "raylib [audio] example - spectrum analyzer");
InitAudioDevice();
Music music = LoadMusicStream("resources/guitar_noodling.ogg");
PlayMusicStream(music);
// Configure and initialize spectrum analyzer
SpectrumConfig config = {
.binCount = FFT_SIZE,
.windowFunc = WINDOW_HANN,
.smoothingFactor = 0.8f,
.peakDecayRate = 0.95f,
.sampleRate = 44100,
};
InitSpectrumAnalyzer(config);
float samples[FFT_SIZE] = { 0 }; // Buffer for audio samples
SetTargetFPS(60);
//--------------------------------------------------------------------------
// Main game loop
while (!WindowShouldClose())
{
// Update
//----------------------------------------------------------------------
UpdateMusicStream(music);
// Feed samples into spectrum analyzer
UpdateSpectrum(samples, FFT_SIZE);
ApplySmoothing();
float *spectrum = GetSpectrumData();
//----------------------------------------------------------------------
// Draw
//----------------------------------------------------------------------
BeginDrawing();
ClearBackground(RAYWHITE);
// Draw frequency bars
int barWidth = SCREEN_WIDTH / NUM_BARS;
int binsPerBar = (FFT_SIZE / 2) / NUM_BARS;
if (binsPerBar < 1) binsPerBar = 1;
for (int i = 0; i < NUM_BARS; i++)
{
// Average magnitudes across bins for this bar
float magnitude = 0.0f;
for (int j = 0; j < binsPerBar; j++)
{
int binIndex = i * binsPerBar + j;
if (binIndex < FFT_SIZE / 2 && spectrum != NULL)
{
magnitude += spectrum[binIndex];
}
}
magnitude /= (float)binsPerBar;
// Scale for visual display
int barHeight = (int)(magnitude * SCREEN_HEIGHT * 4.0f);
if (barHeight > SCREEN_HEIGHT) barHeight = SCREEN_HEIGHT;
// Color gradient from green (low) to red (high)
Color barColor = (Color){
(unsigned char)(255 * i / NUM_BARS),
(unsigned char)(255 * (NUM_BARS - i) / NUM_BARS),
50,
255
};
DrawRectangle(
i * barWidth,
SCREEN_HEIGHT - barHeight,
barWidth - 2,
barHeight,
barColor
);
}
// Display info
DrawText("AUDIO SPECTRUM ANALYZER", 10, 10, 20, DARKGRAY);
DrawText(TextFormat("Peak Freq: %.1f Hz", GetPeakFrequency()), 10, 40, 16, GRAY);
DrawText(TextFormat("Peak Mag: %.4f", GetPeakMagnitude()), 10, 60, 16, GRAY);
DrawFPS(SCREEN_WIDTH - 90, 10);
EndDrawing();
//----------------------------------------------------------------------
}
// De-Initialization
//--------------------------------------------------------------------------
CloseSpectrumAnalyzer();
UnloadMusicStream(music);
CloseAudioDevice();
CloseWindow();
//--------------------------------------------------------------------------
return 0;
}

332
src/raudio_analyzer.c Normal file
View File

@ -0,0 +1,332 @@
/**********************************************************************************************
*
* 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;
}
}
}

95
src/raudio_analyzer.h Normal file
View File

@ -0,0 +1,95 @@
/**********************************************************************************************
*
* raudio_analyzer - Audio spectrum analysis module for raylib
*
* DESCRIPTION:
* FFT-based audio spectrum analysis with windowing functions, peak detection,
* and smoothing for real-time audio visualization.
*
* FEATURES:
* - Radix-2 Cooley-Tukey FFT algorithm
* - Multiple windowing functions (Hann, Hamming, Blackman, Rectangular)
* - Configurable frequency bin count
* - Peak detection with configurable decay rate
* - Temporal smoothing for stable visualization
*
* 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.
*
* Permission is granted to anyone to use this software for any purpose, including commercial
* applications, and to alter it and redistribute it freely, subject to the following restrictions:
*
* 1. The origin of this software must not be misrepresented; you must not claim that you
* wrote the original software. If you use this software in a product, an acknowledgment
* in the product documentation would be appreciated but is not required.
*
* 2. Altered source versions must be plainly marked as such, and must not be misrepresented
* as being the original software.
*
* 3. This notice may not be removed or altered from any source distribution.
*
**********************************************************************************************/
#ifndef RAUDIO_ANALYZER_H
#define RAUDIO_ANALYZER_H
#include <stdbool.h>
//----------------------------------------------------------------------------------
// Defines and Macros
//----------------------------------------------------------------------------------
#ifndef MAX_SPECTRUM_BINS
#define MAX_SPECTRUM_BINS 1024
#endif
#ifndef DEFAULT_SMOOTHING_FACTOR
#define DEFAULT_SMOOTHING_FACTOR 0.8f
#endif
#ifndef DEFAULT_PEAK_DECAY_RATE
#define DEFAULT_PEAK_DECAY_RATE 0.95f
#endif
//----------------------------------------------------------------------------------
// Types and Structures Definition
//----------------------------------------------------------------------------------
// Window function types for FFT preprocessing
typedef enum {
WINDOW_RECTANGULAR = 0, // No windowing (rectangular/boxcar)
WINDOW_HANN, // Hann (raised cosine) window
WINDOW_HAMMING, // Hamming window
WINDOW_BLACKMAN // Blackman window
} WindowFunction;
// Spectrum analyzer configuration
typedef struct {
int binCount; // Number of frequency bins (must be power of 2)
WindowFunction windowFunc; // Window function to apply before FFT
float smoothingFactor; // Temporal smoothing factor [0.0 - 1.0]
float peakDecayRate; // Peak value decay rate per frame [0.0 - 1.0]
int sampleRate; // Audio sample rate in Hz
} SpectrumConfig;
//----------------------------------------------------------------------------------
// Module Functions Declaration
//----------------------------------------------------------------------------------
// Lifecycle management
void InitSpectrumAnalyzer(SpectrumConfig config); // Initialize spectrum analyzer with config
void CloseSpectrumAnalyzer(void); // Free all allocated resources
// Spectrum processing
void UpdateSpectrum(float *samples, int sampleCount); // Process audio samples through FFT
void ApplySmoothing(void); // Apply temporal smoothing to spectrum data
// Data retrieval
float *GetSpectrumData(void); // Get array of frequency bin magnitudes
float GetPeakFrequency(void); // Get the dominant frequency in Hz
float GetPeakMagnitude(void); // Get the magnitude of the peak frequency bin
int GetSpectrumBinCount(void); // Get the current number of frequency bins
bool IsSpectrumReady(void); // Check if analyzer is initialized and ready
#endif // RAUDIO_ANALYZER_H