perf+ux: cache reassignment, keep manual dB floor, dedupe load paths
- Split GenerateSpectrogramTexture into ComputeSpectrogramReassignment (the expensive synchrosqueezing, cached in app.reassignBuffer) and ColorizeSpectrogram (cheap). dB-floor and colormap changes now only re-colorize instead of recomputing the whole reassignment every frame — the dB slider and colormap switching are smooth on large files. - AutoScaleAmplitude no longer overwrites a dB floor the user set by hand (amplitudeUserSet flag, reset per file load). - Extract ResetForNewSignal() used by all three load paths; removes the duplicated reset blocks and the double ComputeSTFTInit per load. Drag-drop now resets the selection like the browser already did. - Remove the dead lastInteractedFrame field. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+37
-15
@@ -96,7 +96,12 @@ void GenerateColormapTexture(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== Spectrogram texture =====
|
// ===== 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;
|
if (stft->numSegments == 0) return;
|
||||||
int width = stft->numSegments;
|
int width = stft->numSegments;
|
||||||
@@ -104,9 +109,12 @@ void GenerateSpectrogramTexture(StftResult* stft, Image* image, Texture2D* textu
|
|||||||
int fftSize = (height - 1) * 2;
|
int fftSize = (height - 1) * 2;
|
||||||
float freqPerBin = (float)stft->sampleRate / fftSize;
|
float freqPerBin = (float)stft->sampleRate / fftSize;
|
||||||
|
|
||||||
UnloadImage(*image); // release previous image (NULL-safe on first call)
|
// (Re)allocate the cached accumulation buffer for reassigned energy.
|
||||||
*image = GenImageColor(width, height, BLACK);
|
free(app.reassignBuffer);
|
||||||
Color* pixels = (Color*)image->data;
|
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)
|
// Find max amplitude for normalization (skip NULL segments)
|
||||||
float maxAmplitude = 0.0001f;
|
float maxAmplitude = 0.0001f;
|
||||||
@@ -117,15 +125,9 @@ void GenerateSpectrogramTexture(StftResult* stft, Image* image, Texture2D* textu
|
|||||||
maxAmplitude = stft->segments[seg].spectrum[bin].amplitude;
|
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
|
// Noise threshold: only reassign bins with significant energy
|
||||||
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++) {
|
||||||
// Skip segments that haven't been computed yet (overview/high-res transition)
|
// Skip segments that haven't been computed yet (overview/high-res transition)
|
||||||
if (stft->segments[seg].spectrum == NULL) continue;
|
if (stft->segments[seg].spectrum == NULL) continue;
|
||||||
@@ -182,8 +184,22 @@ void GenerateSpectrogramTexture(StftResult* stft, Image* image, Texture2D* textu
|
|||||||
accumBuffer[idx1] += amplitude * frac;
|
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++) {
|
for (int i = 0; i < width * height; i++) {
|
||||||
if (accumBuffer[i] > 0.0001f) {
|
if (accumBuffer[i] > 0.0001f) {
|
||||||
float db = AmplitudeToDecibels(accumBuffer[i]);
|
float db = AmplitudeToDecibels(accumBuffer[i]);
|
||||||
@@ -192,14 +208,20 @@ void GenerateSpectrogramTexture(StftResult* stft, Image* image, Texture2D* textu
|
|||||||
pixels[i] = GetColormapColor(normalized, app.colormap);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Compute auto-adjusted amplitude floor/ceiling from STFT data
|
||||||
|
|
||||||
// ===== Grid, labels, selection, playhead =====
|
// ===== Grid, labels, selection, playhead =====
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ float MeasureTextScaled(const char* text, float baseSize);
|
|||||||
void GenerateColormapTexture(void);
|
void GenerateColormapTexture(void);
|
||||||
|
|
||||||
// --- Spectrogram texture ---
|
// --- 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 GenerateSpectrogramTexture(StftResult* stft, Image* image, Texture2D* texture);
|
||||||
|
void ColorizeSpectrogram(Image* image, Texture2D* texture);
|
||||||
|
|
||||||
// --- On-screen drawing (operate on the global app state) ---
|
// --- On-screen drawing (operate on the global app state) ---
|
||||||
void DrawSpectrogramGrid(Rectangle bounds, int numCellsX, int numCellsY, Color color);
|
void DrawSpectrogramGrid(Rectangle bounds, int numCellsX, int numCellsY, Color color);
|
||||||
|
|||||||
+34
-32
@@ -53,6 +53,37 @@ static bool IsUserInteracting(void)
|
|||||||
return false;
|
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
|
// Main Application
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -108,7 +139,6 @@ int main(int argc, char* argv[])
|
|||||||
app.highResFinished = false;
|
app.highResFinished = false;
|
||||||
app.bgHighResSeg = 0;
|
app.bgHighResSeg = 0;
|
||||||
app.bgFinished = false;
|
app.bgFinished = false;
|
||||||
app.lastInteractedFrame = 0;
|
|
||||||
app.isBgProcessing = false;
|
app.isBgProcessing = false;
|
||||||
// Initialize FFT cache
|
// Initialize FFT cache
|
||||||
app.fftCache.count = 0;
|
app.fftCache.count = 0;
|
||||||
@@ -151,19 +181,7 @@ int main(int argc, char* argv[])
|
|||||||
|
|
||||||
if (FileExists(pathToLoad) && LoadWavFile(pathToLoad, &app.signal)) {
|
if (FileExists(pathToLoad) && LoadWavFile(pathToLoad, &app.signal)) {
|
||||||
fileLoaded = true;
|
fileLoaded = true;
|
||||||
app.loaded = true;
|
ResetForNewSignal();
|
||||||
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);
|
|
||||||
TraceLog(LOG_INFO, "File loaded successfully");
|
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);
|
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 (isWav && FileExists(dropped.paths[0])) {
|
||||||
if (LoadWavFile(dropped.paths[0], &app.signal)) {
|
if (LoadWavFile(dropped.paths[0], &app.signal)) {
|
||||||
app.loaded = true;
|
ResetForNewSignal();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -986,6 +987,7 @@ int main(int argc, char* argv[])
|
|||||||
UnloadTexture(colormapTexture);
|
UnloadTexture(colormapTexture);
|
||||||
FreeBrowserFiles();
|
FreeBrowserFiles();
|
||||||
FreeAllCacheEntries(&app.fftCache);
|
FreeAllCacheEntries(&app.fftCache);
|
||||||
|
free(app.reassignBuffer);
|
||||||
FreeSignal(&app.signal);
|
FreeSignal(&app.signal);
|
||||||
CloseAudioDevice();
|
CloseAudioDevice();
|
||||||
CloseWindow();
|
CloseWindow();
|
||||||
|
|||||||
+11
-1
@@ -139,10 +139,17 @@ typedef struct {
|
|||||||
// Display settings
|
// Display settings
|
||||||
float amplitudeFloorDb;
|
float amplitudeFloorDb;
|
||||||
float amplitudeCeilingDb;
|
float amplitudeCeilingDb;
|
||||||
|
bool amplitudeUserSet; // true once the user adjusts the dB floor; suppresses auto-scale
|
||||||
ColormapType colormap;
|
ColormapType colormap;
|
||||||
bool showGrid;
|
bool showGrid;
|
||||||
int fftSize; // Current FFT size (128-2048)
|
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
|
// File browser state
|
||||||
bool showFileBrowser;
|
bool showFileBrowser;
|
||||||
char browserPath[512];
|
char browserPath[512];
|
||||||
@@ -174,7 +181,6 @@ typedef struct {
|
|||||||
// filled in at full resolution in the background while the user is idle.
|
// filled in at full resolution in the background while the user is idle.
|
||||||
int bgHighResSeg; // next segment index to compute at high-res
|
int bgHighResSeg; // next segment index to compute at high-res
|
||||||
bool bgFinished; // true when all segments are computed 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
|
bool isBgProcessing; // true while background task is actively computing
|
||||||
|
|
||||||
// FFT size cache — LRU cache of previously computed STFT results.
|
// FFT size cache — LRU cache of previously computed STFT results.
|
||||||
@@ -202,6 +208,10 @@ extern Sound AudioPlaybackSound;
|
|||||||
extern Texture2D colormapTexture;
|
extern Texture2D colormapTexture;
|
||||||
extern Font mainFont;
|
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)
|
// Small math helpers (header-inline so every module can use them)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -371,6 +371,10 @@ int ComputeSkipFactor(float signalDurationSec)
|
|||||||
// ===== Amplitude auto-scaling =====
|
// ===== Amplitude auto-scaling =====
|
||||||
void AutoScaleAmplitude(StftResult* stft)
|
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 maxDb = -999.0f;
|
||||||
float minDb = 0.0f;
|
float minDb = 0.0f;
|
||||||
for (int seg = 0; seg < stft->numSegments; seg++) {
|
for (int seg = 0; seg < stft->numSegments; seg++) {
|
||||||
|
|||||||
@@ -112,28 +112,8 @@ static void LoadSelectedFile(void)
|
|||||||
if (app.browserIsDir[app.browserSelected]) {
|
if (app.browserIsDir[app.browserSelected]) {
|
||||||
NavigateToDirectory(app.browserFiles[app.browserSelected]);
|
NavigateToDirectory(app.browserFiles[app.browserSelected]);
|
||||||
} else if (FileExists(filePath) && LoadWavFile(filePath, &app.signal)) {
|
} else if (FileExists(filePath) && LoadWavFile(filePath, &app.signal)) {
|
||||||
app.loaded = true;
|
ResetForNewSignal();
|
||||||
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;
|
|
||||||
app.showFileBrowser = false;
|
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);
|
TraceLog(LOG_INFO, "Loaded: %s", filePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -382,6 +362,7 @@ void DrawSidebar(void)
|
|||||||
DrawSlider(dbSlider, dbValue);
|
DrawSlider(dbSlider, dbValue);
|
||||||
if (UpdateSlider(dbSlider, &dbValue)) {
|
if (UpdateSlider(dbSlider, &dbValue)) {
|
||||||
app.amplitudeFloorDb = -100.0f + dbValue * 80.0f;
|
app.amplitudeFloorDb = -100.0f + dbValue * 80.0f;
|
||||||
|
app.amplitudeUserSet = true; // stop auto-scale from overwriting this
|
||||||
needsRegen = true;
|
needsRegen = true;
|
||||||
}
|
}
|
||||||
y += 28 * scale;
|
y += 28 * scale;
|
||||||
@@ -511,7 +492,8 @@ void DrawSidebar(void)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (needsRegen && app.stftComputed) {
|
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
|
app.visibleTextureValid = false; // Force cache invalidation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user