audio_fft_spectrum_visualizer.c 11 KB

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