From bca898516b50d6c80d8d1875b63b1482fd88ddf5 Mon Sep 17 00:00:00 2001 From: Tyler Date: Fri, 27 Mar 2026 23:16:49 -0700 Subject: [PATCH] Add configurable FFT size (512-8192), 75% overlap for smoother display Co-authored-by: Qwen-Coder --- src/spectrogram.c | 60 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/src/spectrogram.c b/src/spectrogram.c index 2b87c04..07fb924 100644 --- a/src/spectrogram.c +++ b/src/spectrogram.c @@ -27,8 +27,10 @@ // Configuration // ============================================================================ -#define FFT_SIZE 2048 -#define HOP_SIZE (FFT_SIZE / 2) +#define FFT_SIZE_DEFAULT 2048 +#define FFT_SIZE_MAX 8192 +#define FFT_SIZE_MIN 512 +#define HOP_RATIO 4 // FFT_SIZE / HOP_SIZE = 4 means 75% overlap #define MAX_SAMPLE_RATE 48000 #define LOUDNESS_FLOOR_DB -80.0f @@ -113,6 +115,7 @@ typedef struct { float amplitudeCeilingDb; ColormapType colormap; bool showGrid; + int fftSize; // Current FFT size (512-8192) // File browser state bool showFileBrowser; @@ -259,10 +262,10 @@ static void ApplyHannWindow(float* samples, int n) // STFT Implementation // ============================================================================ -static void ComputeSTFT(AudioSignal* signal, StftResult* result) +static void ComputeSTFT(AudioSignal* signal, StftResult* result, int fftSize) { - int windowSize = FFT_SIZE, hopSize = HOP_SIZE; - int numSegments = (signal->numSamples - windowSize) / hopSize + 1; + int hopSize = fftSize / HOP_RATIO; // 75% overlap + int numSegments = (signal->numSamples - fftSize) / hopSize + 1; if (numSegments <= 0) numSegments = 1; result->numSegments = numSegments; @@ -271,30 +274,30 @@ static void ComputeSTFT(AudioSignal* signal, StftResult* result) result->totalSamples = signal->numSamples; result->useHannWindow = true; - int numBins = windowSize / 2 + 1; - float* windowedSamples = (float*)malloc(windowSize * sizeof(float)); - float complex *complexInput = (float complex*)malloc(windowSize * sizeof(float complex)); - float complex* fftOutput = (float complex*)malloc(windowSize * sizeof(float complex)); + int numBins = fftSize / 2 + 1; + float* windowedSamples = (float*)malloc(fftSize * sizeof(float)); + float complex *complexInput = (float complex*)malloc(fftSize * sizeof(float complex)); + float complex* fftOutput = (float complex*)malloc(fftSize * sizeof(float complex)); for (int seg = 0; seg < numSegments; seg++) { int offset = seg * hopSize; - int samplesToCopy = windowSize; + int samplesToCopy = fftSize; if (offset + samplesToCopy > signal->numSamples) { samplesToCopy = signal->numSamples - offset; - memset(windowedSamples, 0, windowSize * sizeof(float)); + memset(windowedSamples, 0, fftSize * sizeof(float)); } memcpy(windowedSamples, signal->samples + offset, samplesToCopy * sizeof(float)); - ApplyHannWindow(windowedSamples, windowSize); - for (int i = 0; i < windowSize; i++) complexInput[i] = windowedSamples[i] + 0.0f * I; - FFT(complexInput, fftOutput, windowSize, false); + ApplyHannWindow(windowedSamples, fftSize); + for (int i = 0; i < fftSize; i++) complexInput[i] = windowedSamples[i] + 0.0f * I; + FFT(complexInput, fftOutput, fftSize, false); result->segments[seg].numBins = numBins; result->segments[seg].sampleOffset = offset; result->segments[seg].sampleCount = samplesToCopy; result->segments[seg].spectrum = (FrequencyData*)malloc(numBins * sizeof(FrequencyData)); for (int bin = 0; bin < numBins; bin++) { - result->segments[seg].spectrum[bin].frequency = (float)bin * signal->sampleRate / windowSize; - result->segments[seg].spectrum[bin].amplitude = (bin == 0) ? cabsf(fftOutput[bin]) / windowSize : 2.0f * cabsf(fftOutput[bin]) / windowSize; + result->segments[seg].spectrum[bin].frequency = (float)bin * signal->sampleRate / fftSize; + result->segments[seg].spectrum[bin].amplitude = (bin == 0) ? cabsf(fftOutput[bin]) / fftSize : 2.0f * cabsf(fftOutput[bin]) / fftSize; result->segments[seg].spectrum[bin].phase = cargf(fftOutput[bin]); } } @@ -801,6 +804,7 @@ static void DrawInfo(Rectangle bounds) DrawText(TextFormat("Sample Rate: %d Hz", app.signal.sampleRate), 10, y, fontSize, LIGHTGRAY); y += 20; DrawText(TextFormat("Duration: %.2f sec", app.signal.duration), 10, y, fontSize, LIGHTGRAY); y += 20; DrawText(TextFormat("View: %.1f%%-%.1f%% (%.2f sec)", app.viewStart*100, app.viewEnd*100, (app.viewEnd-app.viewStart)*app.signal.duration), 10, y, fontSize, LIGHTGRAY); y += 20; + DrawText(TextFormat("FFT: %d (%.1f Hz/bin, 75%% overlap)", app.fftSize, (float)app.signal.sampleRate / app.fftSize), 10, y, fontSize, LIGHTGRAY); y += 20; DrawText(TextFormat("Max Freq: %.1f kHz", (float)app.signal.sampleRate / 2000.0f), 10, y, fontSize, LIGHTGRAY); y += 20; y += 10; @@ -813,7 +817,7 @@ static void DrawInfo(Rectangle bounds) DrawText("No audio file loaded", 10, y, fontSize, RED); } - y = GetScreenHeight() - 260; + y = GetScreenHeight() - 280; DrawText("Controls:", 10, y, fontSize, LIGHTGRAY); y += 20; DrawText(" O - Open file browser", 10, y, fontSize, GRAY); y += 18; DrawText(" Drag & drop WAV file", 10, y, fontSize, GRAY); y += 18; @@ -822,6 +826,7 @@ static void DrawInfo(Rectangle bounds) DrawText(" LMB Drag - Select time region", 10, y, fontSize, GRAY); y += 18; DrawText(" Shift+LMB - Select freq range", 10, y, fontSize, GRAY); y += 18; DrawText(" SPACE - Play selection", 10, y, fontSize, GRAY); y += 18; + DrawText(" 1/2 - FFT size (resolution)", 10, y, fontSize, GRAY); y += 18; DrawText(" M - Cycle colormap", 10, y, fontSize, GRAY); y += 18; DrawText(" W/S - Adjust dB floor", 10, y, fontSize, GRAY); y += 18; DrawText(" G - Toggle grid", 10, y, fontSize, GRAY); y += 18; @@ -862,6 +867,7 @@ int main(int argc, char* argv[]) app.cachedVisibleStart = -1; app.cachedVisibleEnd = -1; app.visibleTextureValid = false; + app.fftSize = FFT_SIZE_DEFAULT; GenerateColormapTexture(); ScanDirectory(GetWorkingDirectory()); @@ -1014,6 +1020,24 @@ int main(int argc, char* argv[]) } if (IsKeyPressed(KEY_SPACE) && !app.showFileBrowser) PlaySelectedRegion(); + // FFT size control (1/2 keys) + if (IsKeyPressed(KEY_ONE) && !app.showFileBrowser) { + app.fftSize /= 2; + if (app.fftSize < FFT_SIZE_MIN) app.fftSize = FFT_SIZE_MIN; + else { + app.stftComputed = false; + TraceLog(LOG_INFO, "FFT size: %d", app.fftSize); + } + } + if (IsKeyPressed(KEY_TWO) && !app.showFileBrowser) { + app.fftSize *= 2; + if (app.fftSize > FFT_SIZE_MAX) app.fftSize = FFT_SIZE_MAX; + else { + app.stftComputed = false; + TraceLog(LOG_INFO, "FFT size: %d", app.fftSize); + } + } + if (IsKeyPressed(KEY_ESCAPE)) { if (app.showFileBrowser) app.showFileBrowser = false; else { @@ -1076,7 +1100,7 @@ int main(int argc, char* argv[]) if (app.loaded && !app.stftComputed) { TraceLog(LOG_INFO, "Computing STFT..."); double startTime = GetTime(); - ComputeSTFT(&app.signal, &app.stft); + ComputeSTFT(&app.signal, &app.stft, app.fftSize); TraceLog(LOG_INFO, "STFT computed in %.2f sec (%d segments)", GetTime() - startTime, app.stft.numSegments); GenerateSpectrogramTexture(&app.stft, &app.spectrogramImage, &app.spectrogramTexture); app.stftComputed = true;