diff --git a/src/render.c b/src/render.c index 1d1baf2..aab2fe5 100644 --- a/src/render.c +++ b/src/render.c @@ -96,7 +96,12 @@ void GenerateColormapTexture(void) } // ===== Spectrogram texture ===== -void GenerateSpectrogramTexture(StftResult* stft, Image* image, Texture2D* texture) +// ===== SYNCHROSQUEEZING (energy reassignment) ===== +// The expensive part: reassign energy to true frequencies using the derivative +// STFT. Depends only on the STFT data, so the result is cached in +// app.reassignBuffer and reused by ColorizeSpectrogram across dB-floor / +// colormap changes (which don't need to recompute any of this). +static void ComputeSpectrogramReassignment(StftResult* stft) { if (stft->numSegments == 0) return; int width = stft->numSegments; @@ -104,9 +109,12 @@ void GenerateSpectrogramTexture(StftResult* stft, Image* image, Texture2D* textu int fftSize = (height - 1) * 2; float freqPerBin = (float)stft->sampleRate / fftSize; - UnloadImage(*image); // release previous image (NULL-safe on first call) - *image = GenImageColor(width, height, BLACK); - Color* pixels = (Color*)image->data; + // (Re)allocate the cached accumulation buffer for reassigned energy. + free(app.reassignBuffer); + app.reassignBuffer = (float*)calloc(width * height, sizeof(float)); + app.reassignWidth = width; + app.reassignHeight = height; + float* accumBuffer = app.reassignBuffer; // Find max amplitude for normalization (skip NULL segments) float maxAmplitude = 0.0001f; @@ -117,15 +125,9 @@ void GenerateSpectrogramTexture(StftResult* stft, Image* image, Texture2D* textu maxAmplitude = stft->segments[seg].spectrum[bin].amplitude; } - // ===== SYNCHROSQUEEZING ===== - // Reassign energy to true frequencies using derivative STFT - - // Accumulation buffer for reassigned energy - float* accumBuffer = (float*)calloc(width * height, sizeof(float)); - // Noise threshold: only reassign bins with significant energy float noiseThreshold = maxAmplitude * 0.01f; // 1% of max amplitude - + for (int seg = 0; seg < width; seg++) { // Skip segments that haven't been computed yet (overview/high-res transition) if (stft->segments[seg].spectrum == NULL) continue; @@ -182,8 +184,22 @@ void GenerateSpectrogramTexture(StftResult* stft, Image* image, Texture2D* textu accumBuffer[idx1] += amplitude * frac; } } - - // Convert accumulation buffer to colors +} + +// Map the cached reassignment buffer to colors using the current dB floor/ +// ceiling and colormap. Cheap — safe to call every frame the dB slider moves +// or when the colormap changes (no synchrosqueezing recompute). +void ColorizeSpectrogram(Image* image, Texture2D* texture) +{ + if (app.reassignBuffer == NULL) return; + int width = app.reassignWidth; + int height = app.reassignHeight; + float* accumBuffer = app.reassignBuffer; + + UnloadImage(*image); // release previous image (NULL-safe on first call) + *image = GenImageColor(width, height, BLACK); + Color* pixels = (Color*)image->data; + for (int i = 0; i < width * height; i++) { if (accumBuffer[i] > 0.0001f) { float db = AmplitudeToDecibels(accumBuffer[i]); @@ -192,14 +208,20 @@ void GenerateSpectrogramTexture(StftResult* stft, Image* image, Texture2D* textu pixels[i] = GetColormapColor(normalized, app.colormap); } } - - free(accumBuffer); if (texture->id != 0) UnloadTexture(*texture); *texture = LoadTextureFromImage(*image); SetTextureFilter(*texture, TEXTURE_FILTER_BILINEAR); } +// Recompute the reassignment (STFT changed) and rebuild the texture. +void GenerateSpectrogramTexture(StftResult* stft, Image* image, Texture2D* texture) +{ + if (stft->numSegments == 0) return; + ComputeSpectrogramReassignment(stft); + ColorizeSpectrogram(image, texture); +} + // Compute auto-adjusted amplitude floor/ceiling from STFT data // ===== Grid, labels, selection, playhead ===== diff --git a/src/render.h b/src/render.h index 45a3c37..6e061b2 100644 --- a/src/render.h +++ b/src/render.h @@ -13,7 +13,11 @@ float MeasureTextScaled(const char* text, float baseSize); void GenerateColormapTexture(void); // --- Spectrogram texture --- +// GenerateSpectrogramTexture recomputes the synchrosqueezed reassignment (use +// when the STFT changes). ColorizeSpectrogram only re-maps the cached +// reassignment to colors (use for dB-floor / colormap changes). void GenerateSpectrogramTexture(StftResult* stft, Image* image, Texture2D* texture); +void ColorizeSpectrogram(Image* image, Texture2D* texture); // --- On-screen drawing (operate on the global app state) --- void DrawSpectrogramGrid(Rectangle bounds, int numCellsX, int numCellsY, Color color); diff --git a/src/spectrogram.c b/src/spectrogram.c index fde9336..4fe94cb 100644 --- a/src/spectrogram.c +++ b/src/spectrogram.c @@ -53,6 +53,37 @@ static bool IsUserInteracting(void) return false; } +// Reset all per-signal state after a new signal has been loaded into app.signal. +// Drops the cached STFT/FFT-size cache and the on-screen textures so the main +// loop recomputes from scratch (loadingPhase 0 handles the STFT (re)alloc). +void ResetForNewSignal(void) +{ + app.loaded = true; + app.stftComputed = false; + app.loadingPhase = 0; + app.loadingProgress = 0.0f; + app.currentSTFTSegment = 0; + app.skipFactor = 1; + app.highResFinished = false; + app.bgHighResSeg = 0; + app.bgFinished = false; + app.isBgProcessing = false; + app.amplitudeUserSet = false; // re-enable auto-scaling for the new file + + // Cached STFT results are tied to the old signal data. + FreeAllCacheEntries(&app.fftCache); + + // Reset view + selection to full range. + app.viewStart = 0.0f; app.viewEnd = 1.0f; + app.timeSelectionStart = 0.0f; app.timeSelectionEnd = 1.0f; + app.freqSelectionStart = 0.0f; app.freqSelectionEnd = 1.0f; + + // Invalidate the cached visible texture. + if (app.visibleTexture.id != 0) UnloadTexture(app.visibleTexture); + app.visibleTexture = (Texture2D){ 0 }; + app.visibleTextureValid = false; +} + // ============================================================================ // Main Application // ============================================================================ @@ -108,7 +139,6 @@ int main(int argc, char* argv[]) app.highResFinished = false; app.bgHighResSeg = 0; app.bgFinished = false; - app.lastInteractedFrame = 0; app.isBgProcessing = false; // Initialize FFT cache app.fftCache.count = 0; @@ -151,19 +181,7 @@ int main(int argc, char* argv[]) if (FileExists(pathToLoad) && LoadWavFile(pathToLoad, &app.signal)) { fileLoaded = true; - app.loaded = true; - app.stftComputed = false; - app.loadingPhase = 0; - app.loadingProgress = 0.0f; - app.currentSTFTSegment = 0; - app.skipFactor = 1; - app.highResFinished = false; - app.bgHighResSeg = 0; - app.bgFinished = false; - app.isBgProcessing = false; - // Signal changed — free cache (results are tied to signal data) - FreeAllCacheEntries(&app.fftCache); - ComputeSTFTInit(&app.signal, &app.stft, app.fftSize); + ResetForNewSignal(); TraceLog(LOG_INFO, "File loaded successfully"); } } @@ -180,24 +198,7 @@ int main(int argc, char* argv[]) bool isWav = ext && (strcmp(ext, ".wav") == 0 || strcmp(ext, ".WAV") == 0 || strcmp(ext, ".Wave") == 0 || strcmp(ext, ".Wav") == 0); if (isWav && FileExists(dropped.paths[0])) { if (LoadWavFile(dropped.paths[0], &app.signal)) { - app.loaded = true; - app.stftComputed = false; - app.loadingPhase = 0; - app.loadingProgress = 0.0f; - app.currentSTFTSegment = 0; - app.skipFactor = 1; - app.highResFinished = false; - app.bgHighResSeg = 0; - app.bgFinished = false; - app.isBgProcessing = false; - // Signal changed — free cache (results are tied to signal data) - FreeAllCacheEntries(&app.fftCache); - app.viewStart = 0.0f; app.viewEnd = 1.0f; - ComputeSTFTInit(&app.signal, &app.stft, app.fftSize); - // Invalidate visible texture cache - if (app.visibleTexture.id != 0) UnloadTexture(app.visibleTexture); - app.visibleTexture = (Texture2D){ 0 }; - app.visibleTextureValid = false; + ResetForNewSignal(); } } } @@ -986,6 +987,7 @@ int main(int argc, char* argv[]) UnloadTexture(colormapTexture); FreeBrowserFiles(); FreeAllCacheEntries(&app.fftCache); + free(app.reassignBuffer); FreeSignal(&app.signal); CloseAudioDevice(); CloseWindow(); diff --git a/src/spectrogram_types.h b/src/spectrogram_types.h index 7d10f39..5ac7652 100644 --- a/src/spectrogram_types.h +++ b/src/spectrogram_types.h @@ -139,10 +139,17 @@ typedef struct { // Display settings float amplitudeFloorDb; float amplitudeCeilingDb; + bool amplitudeUserSet; // true once the user adjusts the dB floor; suppresses auto-scale ColormapType colormap; bool showGrid; int fftSize; // Current FFT size (128-2048) + // Cached synchrosqueezed energy (the expensive reassignment result). + // Reused across dB-floor / colormap changes — only re-colorized, not recomputed. + float* reassignBuffer; + int reassignWidth; + int reassignHeight; + // File browser state bool showFileBrowser; char browserPath[512]; @@ -174,7 +181,6 @@ typedef struct { // filled in at full resolution in the background while the user is idle. int bgHighResSeg; // next segment index to compute at high-res bool bgFinished; // true when all segments are computed at high-res - int lastInteractedFrame; // frame counter when last user interaction occurred bool isBgProcessing; // true while background task is actively computing // FFT size cache — LRU cache of previously computed STFT results. @@ -202,6 +208,10 @@ extern Sound AudioPlaybackSound; extern Texture2D colormapTexture; extern Font mainFont; +// Reset all per-signal state after a new signal is loaded into app.signal +// (defined in spectrogram.c; used by every load path). +void ResetForNewSignal(void); + // ============================================================================ // Small math helpers (header-inline so every module can use them) // ============================================================================ diff --git a/src/stft.c b/src/stft.c index 9542bc5..e8c16de 100644 --- a/src/stft.c +++ b/src/stft.c @@ -371,6 +371,10 @@ int ComputeSkipFactor(float signalDurationSec) // ===== Amplitude auto-scaling ===== void AutoScaleAmplitude(StftResult* stft) { + // Don't clobber a dB floor the user has set by hand. Reset on new file load + // re-enables auto-scaling (see ResetForNewSignal). + if (app.amplitudeUserSet) return; + float maxDb = -999.0f; float minDb = 0.0f; for (int seg = 0; seg < stft->numSegments; seg++) { diff --git a/src/ui.c b/src/ui.c index 3b21e8d..8de738b 100644 --- a/src/ui.c +++ b/src/ui.c @@ -112,28 +112,8 @@ static void LoadSelectedFile(void) if (app.browserIsDir[app.browserSelected]) { NavigateToDirectory(app.browserFiles[app.browserSelected]); } else if (FileExists(filePath) && LoadWavFile(filePath, &app.signal)) { - app.loaded = true; - app.stftComputed = false; - app.loadingPhase = 0; - app.loadingProgress = 0.0f; - app.currentSTFTSegment = 0; - app.skipFactor = 1; - app.highResFinished = false; - app.bgHighResSeg = 0; - app.bgFinished = false; - app.isBgProcessing = false; - // Signal changed — free cache (results are tied to signal data) - FreeAllCacheEntries(&app.fftCache); - app.timeSelectionStart = app.viewStart = 0.0f; - app.timeSelectionEnd = app.viewEnd = 1.0f; - app.freqSelectionStart = 0.0f; - app.freqSelectionEnd = 1.0f; + ResetForNewSignal(); app.showFileBrowser = false; - ComputeSTFTInit(&app.signal, &app.stft, app.fftSize); - // Invalidate visible texture cache - if (app.visibleTexture.id != 0) UnloadTexture(app.visibleTexture); - app.visibleTexture = (Texture2D){ 0 }; - app.visibleTextureValid = false; TraceLog(LOG_INFO, "Loaded: %s", filePath); } } @@ -382,6 +362,7 @@ void DrawSidebar(void) DrawSlider(dbSlider, dbValue); if (UpdateSlider(dbSlider, &dbValue)) { app.amplitudeFloorDb = -100.0f + dbValue * 80.0f; + app.amplitudeUserSet = true; // stop auto-scale from overwriting this needsRegen = true; } y += 28 * scale; @@ -511,7 +492,8 @@ void DrawSidebar(void) } if (needsRegen && app.stftComputed) { - GenerateSpectrogramTexture(&app.stft, &app.spectrogramImage, &app.spectrogramTexture); + // dB floor / colormap only — re-map the cached reassignment, don't recompute it. + ColorizeSpectrogram(&app.spectrogramImage, &app.spectrogramTexture); app.visibleTextureValid = false; // Force cache invalidation } }