|
|
|
@ -0,0 +1,279 @@ |
|
|
|
/******************************************************************************************* |
|
|
|
* |
|
|
|
* raylib [audio] example - fft spectrum visualizer |
|
|
|
* |
|
|
|
* Example complexity rating: [★★★☆] 3/4 |
|
|
|
* |
|
|
|
* Example originally created with raylib 6.0 |
|
|
|
* |
|
|
|
* 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> |
|
|
|
|
|
|
|
#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(NULL, "resources/fft.glsl"); |
|
|
|
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); |
|
|
|
|
|
|
|
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 = malloc(sizeof(FFTComplex)*FFT_WINDOW_SIZE), |
|
|
|
.workBuffer = malloc(sizeof(FFTComplex)*FFT_WINDOW_SIZE), |
|
|
|
.prevMagnitudes = calloc(BUFFER_SIZE, sizeof(float)), |
|
|
|
.fftHistory = 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(BLACK); |
|
|
|
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(); |
|
|
|
|
|
|
|
free(fft.spectrum); |
|
|
|
free(fft.workBuffer); |
|
|
|
free(fft.prevMagnitudes); |
|
|
|
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 })); |
|
|
|
} |
|
|
|
} |