Add configurable FFT size (512-8192), 75% overlap for smoother display

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-03-27 23:16:49 -07:00
parent 10e7a68baa
commit bca898516b
+42 -18
View File
@@ -27,8 +27,10 @@
// Configuration // Configuration
// ============================================================================ // ============================================================================
#define FFT_SIZE 2048 #define FFT_SIZE_DEFAULT 2048
#define HOP_SIZE (FFT_SIZE / 2) #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 MAX_SAMPLE_RATE 48000
#define LOUDNESS_FLOOR_DB -80.0f #define LOUDNESS_FLOOR_DB -80.0f
@@ -113,6 +115,7 @@ typedef struct {
float amplitudeCeilingDb; float amplitudeCeilingDb;
ColormapType colormap; ColormapType colormap;
bool showGrid; bool showGrid;
int fftSize; // Current FFT size (512-8192)
// File browser state // File browser state
bool showFileBrowser; bool showFileBrowser;
@@ -259,10 +262,10 @@ static void ApplyHannWindow(float* samples, int n)
// STFT Implementation // 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 hopSize = fftSize / HOP_RATIO; // 75% overlap
int numSegments = (signal->numSamples - windowSize) / hopSize + 1; int numSegments = (signal->numSamples - fftSize) / hopSize + 1;
if (numSegments <= 0) numSegments = 1; if (numSegments <= 0) numSegments = 1;
result->numSegments = numSegments; result->numSegments = numSegments;
@@ -271,30 +274,30 @@ static void ComputeSTFT(AudioSignal* signal, StftResult* result)
result->totalSamples = signal->numSamples; result->totalSamples = signal->numSamples;
result->useHannWindow = true; result->useHannWindow = true;
int numBins = windowSize / 2 + 1; int numBins = fftSize / 2 + 1;
float* windowedSamples = (float*)malloc(windowSize * sizeof(float)); float* windowedSamples = (float*)malloc(fftSize * sizeof(float));
float complex *complexInput = (float complex*)malloc(windowSize * sizeof(float complex)); float complex *complexInput = (float complex*)malloc(fftSize * sizeof(float complex));
float complex* fftOutput = (float complex*)malloc(windowSize * sizeof(float complex)); float complex* fftOutput = (float complex*)malloc(fftSize * sizeof(float complex));
for (int seg = 0; seg < numSegments; seg++) { for (int seg = 0; seg < numSegments; seg++) {
int offset = seg * hopSize; int offset = seg * hopSize;
int samplesToCopy = windowSize; int samplesToCopy = fftSize;
if (offset + samplesToCopy > signal->numSamples) { if (offset + samplesToCopy > signal->numSamples) {
samplesToCopy = signal->numSamples - offset; samplesToCopy = signal->numSamples - offset;
memset(windowedSamples, 0, windowSize * sizeof(float)); memset(windowedSamples, 0, fftSize * sizeof(float));
} }
memcpy(windowedSamples, signal->samples + offset, samplesToCopy * sizeof(float)); memcpy(windowedSamples, signal->samples + offset, samplesToCopy * sizeof(float));
ApplyHannWindow(windowedSamples, windowSize); ApplyHannWindow(windowedSamples, fftSize);
for (int i = 0; i < windowSize; i++) complexInput[i] = windowedSamples[i] + 0.0f * I; for (int i = 0; i < fftSize; i++) complexInput[i] = windowedSamples[i] + 0.0f * I;
FFT(complexInput, fftOutput, windowSize, false); FFT(complexInput, fftOutput, fftSize, false);
result->segments[seg].numBins = numBins; result->segments[seg].numBins = numBins;
result->segments[seg].sampleOffset = offset; result->segments[seg].sampleOffset = offset;
result->segments[seg].sampleCount = samplesToCopy; result->segments[seg].sampleCount = samplesToCopy;
result->segments[seg].spectrum = (FrequencyData*)malloc(numBins * sizeof(FrequencyData)); result->segments[seg].spectrum = (FrequencyData*)malloc(numBins * sizeof(FrequencyData));
for (int bin = 0; bin < numBins; bin++) { for (int bin = 0; bin < numBins; bin++) {
result->segments[seg].spectrum[bin].frequency = (float)bin * signal->sampleRate / windowSize; result->segments[seg].spectrum[bin].frequency = (float)bin * signal->sampleRate / fftSize;
result->segments[seg].spectrum[bin].amplitude = (bin == 0) ? cabsf(fftOutput[bin]) / windowSize : 2.0f * cabsf(fftOutput[bin]) / windowSize; 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]); 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("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("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("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; DrawText(TextFormat("Max Freq: %.1f kHz", (float)app.signal.sampleRate / 2000.0f), 10, y, fontSize, LIGHTGRAY); y += 20;
y += 10; y += 10;
@@ -813,7 +817,7 @@ static void DrawInfo(Rectangle bounds)
DrawText("No audio file loaded", 10, y, fontSize, RED); DrawText("No audio file loaded", 10, y, fontSize, RED);
} }
y = GetScreenHeight() - 260; y = GetScreenHeight() - 280;
DrawText("Controls:", 10, y, fontSize, LIGHTGRAY); y += 20; DrawText("Controls:", 10, y, fontSize, LIGHTGRAY); y += 20;
DrawText(" O - Open file browser", 10, y, fontSize, GRAY); y += 18; DrawText(" O - Open file browser", 10, y, fontSize, GRAY); y += 18;
DrawText(" Drag & drop WAV file", 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(" LMB Drag - Select time region", 10, y, fontSize, GRAY); y += 18;
DrawText(" Shift+LMB - Select freq range", 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(" 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(" M - Cycle colormap", 10, y, fontSize, GRAY); y += 18;
DrawText(" W/S - Adjust dB floor", 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; DrawText(" G - Toggle grid", 10, y, fontSize, GRAY); y += 18;
@@ -862,6 +867,7 @@ int main(int argc, char* argv[])
app.cachedVisibleStart = -1; app.cachedVisibleStart = -1;
app.cachedVisibleEnd = -1; app.cachedVisibleEnd = -1;
app.visibleTextureValid = false; app.visibleTextureValid = false;
app.fftSize = FFT_SIZE_DEFAULT;
GenerateColormapTexture(); GenerateColormapTexture();
ScanDirectory(GetWorkingDirectory()); ScanDirectory(GetWorkingDirectory());
@@ -1014,6 +1020,24 @@ int main(int argc, char* argv[])
} }
if (IsKeyPressed(KEY_SPACE) && !app.showFileBrowser) PlaySelectedRegion(); 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 (IsKeyPressed(KEY_ESCAPE)) {
if (app.showFileBrowser) app.showFileBrowser = false; if (app.showFileBrowser) app.showFileBrowser = false;
else { else {
@@ -1076,7 +1100,7 @@ int main(int argc, char* argv[])
if (app.loaded && !app.stftComputed) { if (app.loaded && !app.stftComputed) {
TraceLog(LOG_INFO, "Computing STFT..."); TraceLog(LOG_INFO, "Computing STFT...");
double startTime = GetTime(); 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); TraceLog(LOG_INFO, "STFT computed in %.2f sec (%d segments)", GetTime() - startTime, app.stft.numSegments);
GenerateSpectrogramTexture(&app.stft, &app.spectrogramImage, &app.spectrogramTexture); GenerateSpectrogramTexture(&app.stft, &app.spectrogramImage, &app.spectrogramTexture);
app.stftComputed = true; app.stftComputed = true;