| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285 |
- /*******************************************************************************************
- *
- * raylib [audio] example - fft spectrum visualizer
- *
- * Example complexity rating: [★★★☆] 3/4
- *
- * Example originally created with raylib 6.0, last time updated with raylib 5.6-dev
- *
- * Inspired by Inigo Quilez's https://www.shadertoy.com/
- * Resources/specification: https://gist.github.com/soulthreads/2efe50da4be1fb5f7ab60ff14ca434b8
- *
- * Example created by created by IANN (@meisei4) reviewed by Ramon Santamaria (@raysan5)
- *
- * 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) 2025 IANN (@meisei4)
- *
- ********************************************************************************************/
- #include "raylib.h"
- #include "raymath.h"
- #include <math.h>
- #include <stdlib.h>
- #include <string.h>
- #if defined(PLATFORM_DESKTOP)
- #define GLSL_VERSION 330
- #else // PLATFORM_ANDROID, PLATFORM_WEB
- #define GLSL_VERSION 100
- #endif
- #define MONO 1
- #define SAMPLE_RATE 44100
- #define SAMPLE_RATE_F 44100.0f
- #define FFT_WINDOW_SIZE 1024
- #define BUFFER_SIZE 512
- #define PER_SAMPLE_BIT_DEPTH 16
- #define AUDIO_STREAM_RING_BUFFER_SIZE (FFT_WINDOW_SIZE*2)
- #define EFFECTIVE_SAMPLE_RATE (SAMPLE_RATE_F*0.5f)
- #define WINDOW_TIME ((double)FFT_WINDOW_SIZE/(double)EFFECTIVE_SAMPLE_RATE)
- #define FFT_HISTORICAL_SMOOTHING_DUR 2.0f
- #define MIN_DECIBELS (-100.0f) // https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/minDecibels
- #define MAX_DECIBELS (-30.0f) // https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/maxDecibels
- #define INVERSE_DECIBEL_RANGE (1.0f/(MAX_DECIBELS - MIN_DECIBELS))
- #define DB_TO_LINEAR_SCALE (20.0f/2.302585092994046f)
- #define SMOOTHING_TIME_CONSTANT 0.8f // https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/smoothingTimeConstant
- #define TEXTURE_HEIGHT 1
- #define FFT_ROW 0
- #define UNUSED_CHANNEL 0.0f
- typedef struct FFTComplex { float real, imaginary; } FFTComplex;
- typedef struct FFTData {
- FFTComplex *spectrum;
- FFTComplex *workBuffer;
- float *prevMagnitudes;
- float (*fftHistory)[BUFFER_SIZE];
- int fftHistoryLen;
- int historyPos;
- double lastFftTime;
- float tapbackPos;
- } FFTData;
- static void CaptureFrame(FFTData *fftData, const float *audioSamples);
- static void RenderFrame(const FFTData *fftData, Image *fftImage);
- static void CooleyTukeyFFTSlow(FFTComplex *spectrum, int n);
- //------------------------------------------------------------------------------------
- // Program main entry point
- //------------------------------------------------------------------------------------
- int main(void)
- {
- // Initialization
- //----------------------------------------------------------------------------------- ---
- const int screenWidth = 800;
- const int screenHeight = 450;
- InitWindow(screenWidth, screenHeight, "raylib [audio] example - fft spectrum visualizer");
- Image fftImage = GenImageColor(BUFFER_SIZE, TEXTURE_HEIGHT, WHITE);
- Texture2D fftTexture = LoadTextureFromImage(fftImage);
- RenderTexture2D bufferA = LoadRenderTexture(screenWidth, screenHeight);
- Vector2 iResolution = { (float)screenWidth, (float)screenHeight };
- Shader shader = LoadShader(0, TextFormat("resources/shaders/glsl%i/fft.fs", GLSL_VERSION));
- int iResolutionLocation = GetShaderLocation(shader, "iResolution");
- int iChannel0Location = GetShaderLocation(shader, "iChannel0");
- SetShaderValue(shader, iResolutionLocation, &iResolution, SHADER_UNIFORM_VEC2);
- SetShaderValueTexture(shader, iChannel0Location, fftTexture);
- InitAudioDevice();
- SetAudioStreamBufferSizeDefault(AUDIO_STREAM_RING_BUFFER_SIZE);
- // WARNING: Memory out-of-bounds on PLATFORM_WEB
- Wave wav = LoadWave("resources/country.mp3");
- WaveFormat(&wav, SAMPLE_RATE, PER_SAMPLE_BIT_DEPTH, MONO);
- AudioStream audioStream = LoadAudioStream(SAMPLE_RATE, PER_SAMPLE_BIT_DEPTH, MONO);
- PlayAudioStream(audioStream);
- int fftHistoryLen = (int)ceilf(FFT_HISTORICAL_SMOOTHING_DUR/WINDOW_TIME) + 1;
- FFTData fft = {
- .spectrum = RL_CALLOC(sizeof(FFTComplex), FFT_WINDOW_SIZE),
- .workBuffer = RL_CALLOC(sizeof(FFTComplex), FFT_WINDOW_SIZE),
- .prevMagnitudes = RL_CALLOC(BUFFER_SIZE, sizeof(float)),
- .fftHistory = RL_CALLOC(fftHistoryLen, sizeof(float[BUFFER_SIZE])),
- .fftHistoryLen = fftHistoryLen,
- .historyPos = 0,
- .lastFftTime = 0.0,
- .tapbackPos = 0.01f
- };
- size_t wavCursor = 0;
- const short *wavPCM16 = wav.data;
- short chunkSamples[AUDIO_STREAM_RING_BUFFER_SIZE] = { 0 };
- float audioSamples[FFT_WINDOW_SIZE] = { 0 };
- SetTargetFPS(60);
- //----------------------------------------------------------------------------------
- // Main game loop
- while (!WindowShouldClose()) // Detect window close button or ESC key
- {
- // Update
- //----------------------------------------------------------------------------------
- while (IsAudioStreamProcessed(audioStream))
- {
- for (int i = 0; i < AUDIO_STREAM_RING_BUFFER_SIZE; i++)
- {
- int left = (wav.channels == 2)? wavPCM16[wavCursor*2 + 0] : wavPCM16[wavCursor];
- int right = (wav.channels == 2)? wavPCM16[wavCursor*2 + 1] : left;
- chunkSamples[i] = (short)((left + right)/2);
- if (++wavCursor >= wav.frameCount) wavCursor = 0;
- }
- UpdateAudioStream(audioStream, chunkSamples, AUDIO_STREAM_RING_BUFFER_SIZE);
- for (int i = 0; i < FFT_WINDOW_SIZE; i++) audioSamples[i] = (chunkSamples[i*2] + chunkSamples[i*2 + 1])*0.5f/32767.0f;
- }
- CaptureFrame(&fft, audioSamples);
- RenderFrame(&fft, &fftImage);
- UpdateTexture(fftTexture, fftImage.data);
- //----------------------------------------------------------------------------------
- // Draw
- //----------------------------------------------------------------------------------
- BeginDrawing();
- ClearBackground(RAYWHITE);
- BeginShaderMode(shader);
- SetShaderValueTexture(shader, iChannel0Location, fftTexture);
- DrawTextureRec(bufferA.texture,
- (Rectangle){ 0, 0, (float)screenWidth, (float)-screenHeight },
- (Vector2){ 0, 0 }, WHITE);
- EndShaderMode();
- EndDrawing();
- //------------------------------------------------------------------------------
- }
- // De-Initialization
- //--------------------------------------------------------------------------------------
- UnloadShader(shader);
- UnloadRenderTexture(bufferA);
- UnloadTexture(fftTexture);
- UnloadImage(fftImage);
- UnloadAudioStream(audioStream);
- UnloadWave(wav);
- CloseAudioDevice();
- RL_FREE(fft.spectrum);
- RL_FREE(fft.workBuffer);
- RL_FREE(fft.prevMagnitudes);
- RL_FREE(fft.fftHistory);
- CloseWindow(); // Close window and OpenGL context
- //----------------------------------------------------------------------------------
- return 0;
- }
- // Cooley–Tukey FFT https://en.wikipedia.org/wiki/Cooley%E2%80%93Tukey_FFT_algorithm#Data_reordering,_bit_reversal,_and_in-place_algorithms
- static void CooleyTukeyFFTSlow(FFTComplex *spectrum, int n)
- {
- int j = 0;
- for (int i = 1; i < n - 1; i++)
- {
- int bit = n >> 1;
- while (j >= bit)
- {
- j -= bit;
- bit >>= 1;
- }
- j += bit;
- if (i < j)
- {
- FFTComplex temp = spectrum[i];
- spectrum[i] = spectrum[j];
- spectrum[j] = temp;
- }
- }
- for (int len = 2; len <= n; len <<= 1)
- {
- float angle = -2.0f*PI/len;
- FFTComplex twiddleUnit = { cosf(angle), sinf(angle) };
- for (int i = 0; i < n; i += len)
- {
- FFTComplex twiddleCurrent = { 1.0f, 0.0f };
- for (int j = 0; j < len/2; j++)
- {
- FFTComplex even = spectrum[i + j];
- FFTComplex odd = spectrum[i + j + len/2];
- FFTComplex twiddledOdd = {
- odd.real*twiddleCurrent.real - odd.imaginary*twiddleCurrent.imaginary,
- odd.real*twiddleCurrent.imaginary + odd.imaginary*twiddleCurrent.real
- };
- spectrum[i + j].real = even.real + twiddledOdd.real;
- spectrum[i + j].imaginary = even.imaginary + twiddledOdd.imaginary;
- spectrum[i + j + len/2].real = even.real - twiddledOdd.real;
- spectrum[i + j + len/2].imaginary = even.imaginary - twiddledOdd.imaginary;
- float twiddleRealNext = twiddleCurrent.real*twiddleUnit.real - twiddleCurrent.imaginary*twiddleUnit.imaginary;
- twiddleCurrent.imaginary = twiddleCurrent.real*twiddleUnit.imaginary + twiddleCurrent.imaginary*twiddleUnit.real;
- twiddleCurrent.real = twiddleRealNext;
- }
- }
- }
- }
- static void CaptureFrame(FFTData *fftData, const float *audioSamples)
- {
- for (int i = 0; i < FFT_WINDOW_SIZE; i++)
- {
- float x = (2.0f*PI*i)/(FFT_WINDOW_SIZE - 1.0f);
- float blackmanWeight = 0.42f - 0.5f*cosf(x) + 0.08f*cosf(2.0f*x); // https://en.wikipedia.org/wiki/Window_function#Blackman_window
- fftData->workBuffer[i].real = audioSamples[i]*blackmanWeight;
- fftData->workBuffer[i].imaginary = 0.0f;
- }
- CooleyTukeyFFTSlow(fftData->workBuffer, FFT_WINDOW_SIZE);
- memcpy(fftData->spectrum, fftData->workBuffer, sizeof(FFTComplex)*FFT_WINDOW_SIZE);
- float smoothedSpectrum[BUFFER_SIZE];
- for (int bin = 0; bin < BUFFER_SIZE; bin++)
- {
- float re = fftData->workBuffer[bin].real;
- float im = fftData->workBuffer[bin].imaginary;
- float linearMagnitude = sqrtf(re*re + im*im)/FFT_WINDOW_SIZE;
- float smoothedMagnitude = SMOOTHING_TIME_CONSTANT*fftData->prevMagnitudes[bin] + (1.0f - SMOOTHING_TIME_CONSTANT)*linearMagnitude;
- fftData->prevMagnitudes[bin] = smoothedMagnitude;
- float db = logf(fmaxf(smoothedMagnitude, 1e-40f))*DB_TO_LINEAR_SCALE;
- float normalized = (db - MIN_DECIBELS)*INVERSE_DECIBEL_RANGE;
- smoothedSpectrum[bin] = Clamp(normalized, 0.0f, 1.0f);
- }
- fftData->lastFftTime = GetTime();
- memcpy(fftData->fftHistory[fftData->historyPos], smoothedSpectrum, sizeof(smoothedSpectrum));
- fftData->historyPos = (fftData->historyPos + 1)%fftData->fftHistoryLen;
- }
- static void RenderFrame(const FFTData *fftData, Image *fftImage)
- {
- double framesSinceTapback = floor(fftData->tapbackPos/WINDOW_TIME);
- framesSinceTapback = Clamp(framesSinceTapback, 0.0, fftData->fftHistoryLen - 1);
- int historyPosition = (fftData->historyPos - 1 - (int)framesSinceTapback)%fftData->fftHistoryLen;
- if (historyPosition < 0) historyPosition += fftData->fftHistoryLen;
- const float *amplitude = fftData->fftHistory[historyPosition];
- for (int bin = 0; bin < BUFFER_SIZE; bin++) ImageDrawPixel(fftImage, bin, FFT_ROW, ColorFromNormalized((Vector4){ amplitude[bin], UNUSED_CHANNEL, UNUSED_CHANNEL, UNUSED_CHANNEL }));
- }
|