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:
2026-05-25 01:26:51 -07:00
parent 3a8f20b783
commit ddbbe2734c
6 changed files with 94 additions and 70 deletions
+37 -15
View File
@@ -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 =====