Simplify UI: fixed FFT 128 with synchrosqueezing always on, removed FFT slider and SQ checkbox

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-04-11 00:09:54 -07:00
parent d11546d282
commit c0044c37da
+62 -110
View File
@@ -27,9 +27,7 @@
// Configuration // Configuration
// ============================================================================ // ============================================================================
#define FFT_SIZE_DEFAULT 2048 #define FFT_SIZE 128
#define FFT_SIZE_MAX 2048
#define FFT_SIZE_MIN 128
#define HOP_RATIO 4 // FFT_SIZE / HOP_SIZE = 4 means 75% overlap #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
@@ -116,8 +114,6 @@ typedef struct {
float amplitudeCeilingDb; float amplitudeCeilingDb;
ColormapType colormap; ColormapType colormap;
bool showGrid; bool showGrid;
int fftSize; // Current FFT size (128-2048)
bool useSynchrosqueezing; // Enable synchrosqueezing for sharper display
// File browser state // File browser state
bool showFileBrowser; bool showFileBrowser;
@@ -466,96 +462,81 @@ static void GenerateSpectrogramTexture(StftResult* stft, Image* image, Texture2D
if (stft->segments[seg].spectrum[bin].amplitude > maxAmplitude) if (stft->segments[seg].spectrum[bin].amplitude > maxAmplitude)
maxAmplitude = stft->segments[seg].spectrum[bin].amplitude; maxAmplitude = stft->segments[seg].spectrum[bin].amplitude;
if (app.useSynchrosqueezing) { // ===== SYNCHROSQUEEZING =====
// ===== SYNCHROSQUEEZING ===== // Reassign energy to true frequencies using derivative STFT
// Reassign energy to true frequencies using derivative STFT
// Accumulation buffer for reassigned energy // Accumulation buffer for reassigned energy
float* accumBuffer = (float*)calloc(width * height, sizeof(float)); float* accumBuffer = (float*)calloc(width * height, sizeof(float));
// Noise threshold: only reassign bins with significant energy // Noise threshold: only reassign bins with significant energy
// This prevents noise from being reassigned to random frequencies float noiseThreshold = maxAmplitude * 0.01f; // 1% of max amplitude
float noiseThreshold = maxAmplitude * 0.01f; // 1% of max amplitude
for (int seg = 0; seg < width; seg++) { for (int seg = 0; seg < width; seg++) {
for (int bin = 0; bin < height; bin++) { for (int bin = 0; bin < height; bin++) {
FrequencyData* V_f = &stft->segments[seg].spectrum[bin]; FrequencyData* V_f = &stft->segments[seg].spectrum[bin];
FrequencyData* V_fd = &stft->segments[seg].derivativeSpectrum[bin]; FrequencyData* V_fd = &stft->segments[seg].derivativeSpectrum[bin];
float amplitude = V_f->amplitude; float amplitude = V_f->amplitude;
// Skip noise bins // Skip noise bins
if (amplitude < noiseThreshold) continue; if (amplitude < noiseThreshold) continue;
// Compute instantaneous frequency using synchrosqueezing formula: // Compute instantaneous frequency using synchrosqueezing formula:
// ω̂ = bin_freq + Re[V_fd / (i * V_f)] // ω̂ = bin_freq + Re[V_fd / (i * V_f)]
// Complex division: (a+bi)/(c+di) = ((ac+bd) + (bc-ad)i) / (c²+d²) // Complex division: (a+bi)/(c+di) = ((ac+bd) + (bc-ad)i) / (c²+d²)
// We need Re[(a+bi) / (i*(c+di))] = Re[(a+bi) / (-d+ci)] = (ad+bc)/(c²+d²) // We need Re[(a+bi) / (i*(c+di))] = Re[(a+bi) / (-d+ci)] = (ad+bc)/(c²+d²)
float V_f_real = amplitude * cosf(V_f->phase); float V_f_real = amplitude * cosf(V_f->phase);
float V_f_imag = amplitude * sinf(V_f->phase); float V_f_imag = amplitude * sinf(V_f->phase);
float V_fd_real = V_fd->amplitude * cosf(V_fd->phase); float V_fd_real = V_fd->amplitude * cosf(V_fd->phase);
float V_fd_imag = V_fd->amplitude * sinf(V_fd->phase); float V_fd_imag = V_fd->amplitude * sinf(V_fd->phase);
float denom = V_f_real * V_f_real + V_f_imag * V_f_imag; float denom = V_f_real * V_f_real + V_f_imag * V_f_imag;
float trueFreq = V_f->frequency; // Default to bin frequency float trueFreq = V_f->frequency; // Default to bin frequency
if (denom > 1e-10f) { if (denom > 1e-10f) {
// Re[V_fd / (i * V_f)] = (-V_fd_real * V_f_imag + V_fd_imag * V_f_real) / denom // Re[V_fd / (i * V_f)] = (-V_fd_real * V_f_imag + V_fd_imag * V_f_real) / denom
// Note the MINUS sign on the first term // Note the MINUS sign on the first term
float correction = (-V_fd_real * V_f_imag + V_fd_imag * V_f_real) / denom; float correction = (-V_fd_real * V_f_imag + V_fd_imag * V_f_real) / denom;
trueFreq = V_f->frequency + correction; trueFreq = V_f->frequency + correction;
}
// Clamp to valid range
if (trueFreq < 0) trueFreq = 0;
if (trueFreq >= stft->sampleRate / 2.0f) trueFreq = stft->sampleRate / 2.0f - 1;
// Map to bin coordinate
float targetBinF = trueFreq / freqPerBin;
if (targetBinF < 0) targetBinF = 0;
if (targetBinF >= height) targetBinF = height - 0.001f;
// Bilinear splatting to neighboring bins
int bin0 = (int)targetBinF;
int bin1 = bin0 + 1;
if (bin1 >= height) bin1 = height - 1;
float frac = targetBinF - bin0;
int idx0 = (height - 1 - bin0) * width + seg;
int idx1 = (height - 1 - bin1) * width + seg;
accumBuffer[idx0] += amplitude * (1 - frac);
accumBuffer[idx1] += amplitude * frac;
} }
}
// Convert accumulation buffer to colors // Clamp to valid range
for (int i = 0; i < width * height; i++) { if (trueFreq < 0) trueFreq = 0;
if (accumBuffer[i] > 0.0001f) { if (trueFreq >= stft->sampleRate / 2.0f) trueFreq = stft->sampleRate / 2.0f - 1;
float db = AmplitudeToDecibels(accumBuffer[i]);
float normalized = (db - app.amplitudeFloorDb) / (app.amplitudeCeilingDb - app.amplitudeFloorDb);
normalized = Clamp(normalized, 0.0f, 1.0f);
pixels[i] = GetColormapColor(normalized, app.colormap);
}
}
free(accumBuffer); // Map to bin coordinate
} else { float targetBinF = trueFreq / freqPerBin;
// ===== STANDARD STFT (no synchrosqueezing) ===== if (targetBinF < 0) targetBinF = 0;
for (int seg = 0; seg < width; seg++) { if (targetBinF >= height) targetBinF = height - 0.001f;
for (int bin = 0; bin < height; bin++) {
float amplitude = stft->segments[seg].spectrum[bin].amplitude; // Bilinear splatting to neighboring bins
float db = AmplitudeToDecibels(amplitude); int bin0 = (int)targetBinF;
float normalized = (db - app.amplitudeFloorDb) / (app.amplitudeCeilingDb - app.amplitudeFloorDb); int bin1 = bin0 + 1;
normalized = Clamp(normalized, 0.0f, 1.0f); if (bin1 >= height) bin1 = height - 1;
int pixelIndex = (height - 1 - bin) * width + seg;
pixels[pixelIndex] = GetColormapColor(normalized, app.colormap); float frac = targetBinF - bin0;
} int idx0 = (height - 1 - bin0) * width + seg;
int idx1 = (height - 1 - bin1) * width + seg;
accumBuffer[idx0] += amplitude * (1 - frac);
accumBuffer[idx1] += amplitude * frac;
} }
} }
// Convert accumulation buffer to colors
for (int i = 0; i < width * height; i++) {
if (accumBuffer[i] > 0.0001f) {
float db = AmplitudeToDecibels(accumBuffer[i]);
float normalized = (db - app.amplitudeFloorDb) / (app.amplitudeCeilingDb - app.amplitudeFloorDb);
normalized = Clamp(normalized, 0.0f, 1.0f);
pixels[i] = GetColormapColor(normalized, app.colormap);
}
}
free(accumBuffer);
if (texture->id != 0) UnloadTexture(*texture); if (texture->id != 0) UnloadTexture(*texture);
*texture = LoadTextureFromImage(*image); *texture = LoadTextureFromImage(*image);
SetTextureFilter(*texture, TEXTURE_FILTER_BILINEAR); SetTextureFilter(*texture, TEXTURE_FILTER_BILINEAR);
@@ -989,23 +970,6 @@ static void DrawSidebar(void)
// Title // Title
DrawText("Spectrogram Controls", x, y, 14, WHITE); y += 25; DrawText("Spectrogram Controls", x, y, 14, WHITE); y += 25;
// FFT Size slider
DrawText(TextFormat("FFT Size: %d (%.1f Hz/bin)", app.fftSize, (float)app.signal.sampleRate / app.fftSize), x, y, fontSize, LIGHTGRAY); y += 18;
Rectangle fftSlider = { x, y, sidebarWidth - 10, 20 };
float fftValue = (float)(app.fftSize - FFT_SIZE_MIN) / (FFT_SIZE_MAX - FFT_SIZE_MIN);
DrawSlider(fftSlider, fftValue);
if (UpdateSlider(fftSlider, &fftValue)) {
int newFFT = FFT_SIZE_MIN + (int)(fftValue * (FFT_SIZE_MAX - FFT_SIZE_MIN));
// Round to power of 2
while ((newFFT & (newFFT - 1)) != 0) newFFT++;
if (newFFT != app.fftSize && newFFT >= FFT_SIZE_MIN && newFFT <= FFT_SIZE_MAX) {
app.fftSize = newFFT;
app.stftComputed = false;
needsRegen = true;
}
}
y += 25;
// dB Floor slider // dB Floor slider
DrawText(TextFormat("dB Floor: %.1f", app.amplitudeFloorDb), x, y, fontSize, LIGHTGRAY); y += 18; DrawText(TextFormat("dB Floor: %.1f", app.amplitudeFloorDb), x, y, fontSize, LIGHTGRAY); y += 18;
Rectangle dbSlider = { x, y, sidebarWidth - 10, 20 }; Rectangle dbSlider = { x, y, sidebarWidth - 10, 20 };
@@ -1043,16 +1007,6 @@ static void DrawSidebar(void)
DrawRectangleLinesEx(gridCheck, 1, WHITE); DrawRectangleLinesEx(gridCheck, 1, WHITE);
DrawText("Show Grid", x + 25, y + 2, fontSize, LIGHTGRAY); y += 25; DrawText("Show Grid", x + 25, y + 2, fontSize, LIGHTGRAY); y += 25;
// Synchrosqueezing toggle
Rectangle sqCheck = { x, y, 18, 18 };
if (CheckCollisionPointRec(GetMousePosition(), sqCheck) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
app.useSynchrosqueezing = !app.useSynchrosqueezing;
needsRegen = true;
}
DrawRectangleRec(sqCheck, app.useSynchrosqueezing ? BLUE : DARKGRAY);
DrawRectangleLinesEx(sqCheck, 1, WHITE);
DrawText("Synchrosqueezing (sharp)", x + 25, y + 2, fontSize, LIGHTGRAY); y += 25;
// File loading // File loading
DrawText("File:", x, y, fontSize, LIGHTGRAY); y += 18; DrawText("File:", x, y, fontSize, LIGHTGRAY); y += 18;
Rectangle fileButton = { x, y, sidebarWidth - 10, 25 }; Rectangle fileButton = { x, y, sidebarWidth - 10, 25 };
@@ -1111,7 +1065,7 @@ static void DrawSidebar(void)
if (app.loaded) { if (app.loaded) {
DrawText(TextFormat("Sample Rate: %d Hz", app.signal.sampleRate), x, y, fontSize, GRAY); y += 16; DrawText(TextFormat("Sample Rate: %d Hz", app.signal.sampleRate), x, y, fontSize, GRAY); y += 16;
DrawText(TextFormat("Duration: %.2f sec", app.signal.duration), x, y, fontSize, GRAY); y += 16; DrawText(TextFormat("Duration: %.2f sec", app.signal.duration), x, y, fontSize, GRAY); y += 16;
DrawText(TextFormat("Max Freq: %.1f kHz", (float)app.signal.sampleRate / 2000.0f), x, y, fontSize, GRAY); y += 16; DrawText(TextFormat("FFT: %d, %.1f Hz/bin (synchrosqueezed)", FFT_SIZE, (float)app.signal.sampleRate / FFT_SIZE), x, y, fontSize, GRAY); y += 16;
} else { } else {
DrawText("No file loaded", x, y, fontSize, GRAY); y += 16; DrawText("No file loaded", x, y, fontSize, GRAY); y += 16;
} }
@@ -1177,10 +1131,8 @@ 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;
app.isPlaying = false; app.isPlaying = false;
app.playbackFinished = false; app.playbackFinished = false;
app.useSynchrosqueezing = true; // Enabled by default
GenerateColormapTexture(); GenerateColormapTexture();
ScanDirectory(GetWorkingDirectory()); ScanDirectory(GetWorkingDirectory());
@@ -1396,7 +1348,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, app.fftSize); ComputeSTFT(&app.signal, &app.stft, FFT_SIZE);
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;