audio_fft_spectrum_visualizer.c 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. /*******************************************************************************************
  2. *
  3. * raylib [audio] example - fft spectrum visualizer
  4. *
  5. * Example complexity rating: [★★★☆] 3/4
  6. *
  7. * Example originally created with raylib 6.0, last time updated with raylib 5.6-dev
  8. *
  9. * Inspired by Inigo Quilez's https://www.shadertoy.com/
  10. * Resources/specification: https://gist.github.com/soulthreads/2efe50da4be1fb5f7ab60ff14ca434b8
  11. *
  12. * Example created by created by IANN (@meisei4) reviewed by Ramon Santamaria (@raysan5)
  13. *
  14. * Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
  15. * BSD-like license that allows static linking with closed source software
  16. *
  17. * Copyright (c) 2025 IANN (@meisei4)
  18. *
  19. ********************************************************************************************/
  20. #include "raylib.h"
  21. #include "raymath.h"
  22. #include <math.h>
  23. #include <stdlib.h>
  24. #include <string.h>
  25. #if defined(PLATFORM_DESKTOP)
  26. #define GLSL_VERSION 330
  27. #else // PLATFORM_ANDROID, PLATFORM_WEB
  28. #define GLSL_VERSION 100
  29. #endif
  30. #define MONO 1
  31. #define SAMPLE_RATE 44100
  32. #define SAMPLE_RATE_F 44100.0f
  33. #define FFT_WINDOW_SIZE 1024
  34. #define BUFFER_SIZE 512
  35. #define PER_SAMPLE_BIT_DEPTH 16
  36. #define AUDIO_STREAM_RING_BUFFER_SIZE (FFT_WINDOW_SIZE*2)
  37. #define EFFECTIVE_SAMPLE_RATE (SAMPLE_RATE_F*0.5f)
  38. #define WINDOW_TIME ((double)FFT_WINDOW_SIZE/(double)EFFECTIVE_SAMPLE_RATE)
  39. #define FFT_HISTORICAL_SMOOTHING_DUR 2.0f
  40. #define MIN_DECIBELS (-100.0f) // https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/minDecibels
  41. #define MAX_DECIBELS (-30.0f) // https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/maxDecibels
  42. #define INVERSE_DECIBEL_RANGE (1.0f/(MAX_DECIBELS - MIN_DECIBELS))
  43. #define DB_TO_LINEAR_SCALE (20.0f/2.302585092994046f)
  44. #define SMOOTHING_TIME_CONSTANT 0.8f // https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/smoothingTimeConstant
  45. #define TEXTURE_HEIGHT 1
  46. #define FFT_ROW 0
  47. #define UNUSED_CHANNEL 0.0f
  48. typedef struct FFTComplex { float real, imaginary; } FFTComplex;
  49. typedef struct FFTData {
  50. FFTComplex *spectrum;
  51. FFTComplex *workBuffer;
  52. float *prevMagnitudes;
  53. float (*fftHistory)[BUFFER_SIZE];
  54. int fftHistoryLen;
  55. int historyPos;
  56. double lastFftTime;
  57. float tapbackPos;
  58. } FFTData;
  59. static void CaptureFrame(FFTData *fftData, const float *audioSamples);
  60. static void RenderFrame(const FFTData *fftData, Image *fftImage);
  61. static void CooleyTukeyFFTSlow(FFTComplex *spectrum, int n);
  62. //------------------------------------------------------------------------------------
  63. // Program main entry point
  64. //------------------------------------------------------------------------------------
  65. int main(void)
  66. {
  67. // Initialization
  68. //----------------------------------------------------------------------------------- ---
  69. const int screenWidth = 800;
  70. const int screenHeight = 450;
  71. InitWindow(screenWidth, screenHeight, "raylib [audio] example - fft spectrum visualizer");
  72. Image fftImage = GenImageColor(BUFFER_SIZE, TEXTURE_HEIGHT, WHITE);
  73. Texture2D fftTexture = LoadTextureFromImage(fftImage);
  74. RenderTexture2D bufferA = LoadRenderTexture(screenWidth, screenHeight);
  75. Vector2 iResolution = { (float)screenWidth, (float)screenHeight };
  76. Shader shader = LoadShader(0, TextFormat("resources/shaders/glsl%i/fft.fs", GLSL_VERSION));
  77. int iResolutionLocation = GetShaderLocation(shader, "iResolution");
  78. int iChannel0Location = GetShaderLocation(shader, "iChannel0");
  79. SetShaderValue(shader, iResolutionLocation, &iResolution, SHADER_UNIFORM_VEC2);
  80. SetShaderValueTexture(shader, iChannel0Location, fftTexture);
  81. InitAudioDevice();
  82. SetAudioStreamBufferSizeDefault(AUDIO_STREAM_RING_BUFFER_SIZE);
  83. // WARNING: Memory out-of-bounds on PLATFORM_WEB
  84. Wave wav = LoadWave("resources/country.mp3");
  85. WaveFormat(&wav, SAMPLE_RATE, PER_SAMPLE_BIT_DEPTH, MONO);
  86. AudioStream audioStream = LoadAudioStream(SAMPLE_RATE, PER_SAMPLE_BIT_DEPTH, MONO);
  87. PlayAudioStream(audioStream);
  88. int fftHistoryLen = (int)ceilf(FFT_HISTORICAL_SMOOTHING_DUR/WINDOW_TIME) + 1;
  89. FFTData fft = {
  90. .spectrum = RL_CALLOC(sizeof(FFTComplex), FFT_WINDOW_SIZE),
  91. .workBuffer = RL_CALLOC(sizeof(FFTComplex), FFT_WINDOW_SIZE),
  92. .prevMagnitudes = RL_CALLOC(BUFFER_SIZE, sizeof(float)),
  93. .fftHistory = RL_CALLOC(fftHistoryLen, sizeof(float[BUFFER_SIZE])),
  94. .fftHistoryLen = fftHistoryLen,
  95. .historyPos = 0,
  96. .lastFftTime = 0.0,
  97. .tapbackPos = 0.01f
  98. };
  99. size_t wavCursor = 0;
  100. const short *wavPCM16 = wav.data;
  101. short chunkSamples[AUDIO_STREAM_RING_BUFFER_SIZE] = { 0 };
  102. float audioSamples[FFT_WINDOW_SIZE] = { 0 };
  103. SetTargetFPS(60);
  104. //----------------------------------------------------------------------------------
  105. // Main game loop
  106. while (!WindowShouldClose()) // Detect window close button or ESC key
  107. {
  108. // Update
  109. //----------------------------------------------------------------------------------
  110. while (IsAudioStreamProcessed(audioStream))
  111. {
  112. for (int i = 0; i < AUDIO_STREAM_RING_BUFFER_SIZE; i++)
  113. {
  114. int left = (wav.channels == 2)? wavPCM16[wavCursor*2 + 0] : wavPCM16[wavCursor];
  115. int right = (wav.channels == 2)? wavPCM16[wavCursor*2 + 1] : left;
  116. chunkSamples[i] = (short)((left + right)/2);
  117. if (++wavCursor >= wav.frameCount) wavCursor = 0;
  118. }
  119. UpdateAudioStream(audioStream, chunkSamples, AUDIO_STREAM_RING_BUFFER_SIZE);
  120. for (int i = 0; i < FFT_WINDOW_SIZE; i++) audioSamples[i] = (chunkSamples[i*2] + chunkSamples[i*2 + 1])*0.5f/32767.0f;
  121. }
  122. CaptureFrame(&fft, audioSamples);
  123. RenderFrame(&fft, &fftImage);
  124. UpdateTexture(fftTexture, fftImage.data);
  125. //----------------------------------------------------------------------------------
  126. // Draw
  127. //----------------------------------------------------------------------------------
  128. BeginDrawing();
  129. ClearBackground(RAYWHITE);
  130. BeginShaderMode(shader);
  131. SetShaderValueTexture(shader, iChannel0Location, fftTexture);
  132. DrawTextureRec(bufferA.texture,
  133. (Rectangle){ 0, 0, (float)screenWidth, (float)-screenHeight },
  134. (Vector2){ 0, 0 }, WHITE);
  135. EndShaderMode();
  136. EndDrawing();
  137. //------------------------------------------------------------------------------
  138. }
  139. // De-Initialization
  140. //--------------------------------------------------------------------------------------
  141. UnloadShader(shader);
  142. UnloadRenderTexture(bufferA);
  143. UnloadTexture(fftTexture);
  144. UnloadImage(fftImage);
  145. UnloadAudioStream(audioStream);
  146. UnloadWave(wav);
  147. CloseAudioDevice();
  148. RL_FREE(fft.spectrum);
  149. RL_FREE(fft.workBuffer);
  150. RL_FREE(fft.prevMagnitudes);
  151. RL_FREE(fft.fftHistory);
  152. CloseWindow(); // Close window and OpenGL context
  153. //----------------------------------------------------------------------------------
  154. return 0;
  155. }
  156. // Cooley–Tukey FFT https://en.wikipedia.org/wiki/Cooley%E2%80%93Tukey_FFT_algorithm#Data_reordering,_bit_reversal,_and_in-place_algorithms
  157. static void CooleyTukeyFFTSlow(FFTComplex *spectrum, int n)
  158. {
  159. int j = 0;
  160. for (int i = 1; i < n - 1; i++)
  161. {
  162. int bit = n >> 1;
  163. while (j >= bit)
  164. {
  165. j -= bit;
  166. bit >>= 1;
  167. }
  168. j += bit;
  169. if (i < j)
  170. {
  171. FFTComplex temp = spectrum[i];
  172. spectrum[i] = spectrum[j];
  173. spectrum[j] = temp;
  174. }
  175. }
  176. for (int len = 2; len <= n; len <<= 1)
  177. {
  178. float angle = -2.0f*PI/len;
  179. FFTComplex twiddleUnit = { cosf(angle), sinf(angle) };
  180. for (int i = 0; i < n; i += len)
  181. {
  182. FFTComplex twiddleCurrent = { 1.0f, 0.0f };
  183. for (int j = 0; j < len/2; j++)
  184. {
  185. FFTComplex even = spectrum[i + j];
  186. FFTComplex odd = spectrum[i + j + len/2];
  187. FFTComplex twiddledOdd = {
  188. odd.real*twiddleCurrent.real - odd.imaginary*twiddleCurrent.imaginary,
  189. odd.real*twiddleCurrent.imaginary + odd.imaginary*twiddleCurrent.real
  190. };
  191. spectrum[i + j].real = even.real + twiddledOdd.real;
  192. spectrum[i + j].imaginary = even.imaginary + twiddledOdd.imaginary;
  193. spectrum[i + j + len/2].real = even.real - twiddledOdd.real;
  194. spectrum[i + j + len/2].imaginary = even.imaginary - twiddledOdd.imaginary;
  195. float twiddleRealNext = twiddleCurrent.real*twiddleUnit.real - twiddleCurrent.imaginary*twiddleUnit.imaginary;
  196. twiddleCurrent.imaginary = twiddleCurrent.real*twiddleUnit.imaginary + twiddleCurrent.imaginary*twiddleUnit.real;
  197. twiddleCurrent.real = twiddleRealNext;
  198. }
  199. }
  200. }
  201. }
  202. static void CaptureFrame(FFTData *fftData, const float *audioSamples)
  203. {
  204. for (int i = 0; i < FFT_WINDOW_SIZE; i++)
  205. {
  206. float x = (2.0f*PI*i)/(FFT_WINDOW_SIZE - 1.0f);
  207. float blackmanWeight = 0.42f - 0.5f*cosf(x) + 0.08f*cosf(2.0f*x); // https://en.wikipedia.org/wiki/Window_function#Blackman_window
  208. fftData->workBuffer[i].real = audioSamples[i]*blackmanWeight;
  209. fftData->workBuffer[i].imaginary = 0.0f;
  210. }
  211. CooleyTukeyFFTSlow(fftData->workBuffer, FFT_WINDOW_SIZE);
  212. memcpy(fftData->spectrum, fftData->workBuffer, sizeof(FFTComplex)*FFT_WINDOW_SIZE);
  213. float smoothedSpectrum[BUFFER_SIZE];
  214. for (int bin = 0; bin < BUFFER_SIZE; bin++)
  215. {
  216. float re = fftData->workBuffer[bin].real;
  217. float im = fftData->workBuffer[bin].imaginary;
  218. float linearMagnitude = sqrtf(re*re + im*im)/FFT_WINDOW_SIZE;
  219. float smoothedMagnitude = SMOOTHING_TIME_CONSTANT*fftData->prevMagnitudes[bin] + (1.0f - SMOOTHING_TIME_CONSTANT)*linearMagnitude;
  220. fftData->prevMagnitudes[bin] = smoothedMagnitude;
  221. float db = logf(fmaxf(smoothedMagnitude, 1e-40f))*DB_TO_LINEAR_SCALE;
  222. float normalized = (db - MIN_DECIBELS)*INVERSE_DECIBEL_RANGE;
  223. smoothedSpectrum[bin] = Clamp(normalized, 0.0f, 1.0f);
  224. }
  225. fftData->lastFftTime = GetTime();
  226. memcpy(fftData->fftHistory[fftData->historyPos], smoothedSpectrum, sizeof(smoothedSpectrum));
  227. fftData->historyPos = (fftData->historyPos + 1)%fftData->fftHistoryLen;
  228. }
  229. static void RenderFrame(const FFTData *fftData, Image *fftImage)
  230. {
  231. double framesSinceTapback = floor(fftData->tapbackPos/WINDOW_TIME);
  232. framesSinceTapback = Clamp(framesSinceTapback, 0.0, fftData->fftHistoryLen - 1);
  233. int historyPosition = (fftData->historyPos - 1 - (int)framesSinceTapback)%fftData->fftHistoryLen;
  234. if (historyPosition < 0) historyPosition += fftData->fftHistoryLen;
  235. const float *amplitude = fftData->fftHistory[historyPosition];
  236. for (int bin = 0; bin < BUFFER_SIZE; bin++) ImageDrawPixel(fftImage, bin, FFT_ROW, ColorFromNormalized((Vector4){ amplitude[bin], UNUSED_CHANNEL, UNUSED_CHANNEL, UNUSED_CHANNEL }));
  237. }