From 872dbb72640ebbdf383af793ad08c88db92912ce Mon Sep 17 00:00:00 2001 From: Srikanth Patchava Date: Sat, 25 Apr 2026 01:26:20 -0700 Subject: [PATCH] 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 --- examples/audio/audio_spectrum.c | 126 ++++++++++++ src/raudio_analyzer.c | 332 ++++++++++++++++++++++++++++++++ src/raudio_analyzer.h | 95 +++++++++ 3 files changed, 553 insertions(+) create mode 100644 examples/audio/audio_spectrum.c create mode 100644 src/raudio_analyzer.c create mode 100644 src/raudio_analyzer.h diff --git a/examples/audio/audio_spectrum.c b/examples/audio/audio_spectrum.c new file mode 100644 index 000000000..359bce1f4 --- /dev/null +++ b/examples/audio/audio_spectrum.c @@ -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; +} diff --git a/src/raudio_analyzer.c b/src/raudio_analyzer.c new file mode 100644 index 000000000..649d4b4d7 --- /dev/null +++ b/src/raudio_analyzer.c @@ -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 +#include +#include + +#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; + } + } +} diff --git a/src/raudio_analyzer.h b/src/raudio_analyzer.h new file mode 100644 index 000000000..73428a044 --- /dev/null +++ b/src/raudio_analyzer.h @@ -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 + +//---------------------------------------------------------------------------------- +// 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