feat: dB scale modes (relative/absolute dBFS), about dialog, fix white-out

dB floor handling:
- Fix the white-out/inversion when the floor was cranked up: guard the
  (ceiling - floor) range in ColorizeSpectrogram so floor >= ceiling
  degrades to a hard threshold instead of dividing by ~0 (white) or a
  negative (inverted colors).
- Add two amplitude scale modes, toggled in the sidebar:
  * Relative: ceiling tracks the signal peak, floor sits N dB below it
    (slider = dynamic range, 10..100 dB). Floor can't cross the ceiling.
  * Absolute: fixed dBFS scale (0 dBFS = full scale), slider sets an
    absolute floor in dBFS. Brightness reflects real level.
  Mode + both slider values persist across loads. This replaces the
  amplitudeUserSet flag — storing the intent (range / absolute floor)
  preserves it across re-scales structurally.

About / Help dialog (no help menu existed):
- F1 or sidebar button; documents the scale modes and the caveats
  (dBFS not dBm — a WAV has no power reference; ~6 dB Hann window
  offset; pixels are reassigned energy, not raw bins) plus key list.
- Modal: underlying spectrogram input is gated while open; open/close
  handled in the main loop so the opening click can't self-close it and
  a dismissing click can't fall through into a selection.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 01:43:02 -07:00
parent ddbbe2734c
commit 3863ab8620
6 changed files with 174 additions and 30 deletions
+28 -9
View File
@@ -68,7 +68,6 @@ void ResetForNewSignal(void)
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);
@@ -124,6 +123,9 @@ int main(int argc, char* argv[])
app.freqViewStart = 0.0f; app.freqViewEnd = 1.0f;
app.showGrid = true;
app.colormap = COLORMAP_INFERNO;
app.amplitudeMode = SCALE_RELATIVE;
app.dynRangeDb = 40.0f; // relative: show 40 dB below the peak
app.absoluteFloorDb = -60.0f; // absolute: -60 dBFS floor
app.amplitudeFloorDb = -60.0f;
app.amplitudeCeilingDb = 0.0f;
app.showFileBrowser = false;
@@ -206,7 +208,7 @@ int main(int argc, char* argv[])
}
// File browser toggle
if (IsKeyPressed(KEY_O) && !app.showFileBrowser) {
if (IsKeyPressed(KEY_O) && !app.showFileBrowser && !app.showAbout) {
app.showFileBrowser = true;
ScanDirectory(GetWorkingDirectory());
}
@@ -215,7 +217,12 @@ int main(int argc, char* argv[])
if (IsKeyPressed(KEY_P)) {
app.showScope = !app.showScope;
}
// About / help dialog (F1)
if (IsKeyPressed(KEY_F1)) {
app.showAbout = !app.showAbout;
}
// File browser update
if (app.showFileBrowser) {
// Browser handles its own input
@@ -242,7 +249,7 @@ int main(int argc, char* argv[])
}
// View controls
if (app.loaded && !app.showFileBrowser) {
if (app.loaded && !app.showFileBrowser && !app.showAbout) {
// Spectrogram area fills remaining window space (scaled)
float viewScale = GetUIScale();
float sidebarWidth = 320 * viewScale;
@@ -422,7 +429,7 @@ int main(int argc, char* argv[])
}
// Keyboard shortcuts (SPACE for play/stop toggle, ESC for clear)
if (IsKeyPressed(KEY_SPACE) && !app.showFileBrowser) {
if (IsKeyPressed(KEY_SPACE) && !app.showFileBrowser && !app.showAbout) {
if (app.isPlaying && AudioPlaybackSound.frameCount > 0) {
// Currently playing - stop it
StopSound(AudioPlaybackSound);
@@ -443,12 +450,14 @@ int main(int argc, char* argv[])
}
// Export PNG
if (IsKeyPressed(KEY_E) && !app.showFileBrowser && app.stftComputed) {
if (IsKeyPressed(KEY_E) && !app.showFileBrowser && !app.showAbout && app.stftComputed) {
ExportPNG(&app, app.exportDir);
}
if (IsKeyPressed(KEY_ESCAPE)) {
if (app.showFileBrowser) {
if (app.showAbout) {
app.showAbout = false;
} else if (app.showFileBrowser) {
app.showFileBrowser = false;
} else {
// Clear selections instead of exiting
@@ -522,7 +531,7 @@ int main(int argc, char* argv[])
}
// LMB drag = box select (time + frequency) OR drag existing selection
if (app.loaded && !app.showFileBrowser && CheckCollisionPointRec(mousePos, selBounds)) {
if (app.loaded && !app.showFileBrowser && !app.showAbout && CheckCollisionPointRec(mousePos, selBounds)) {
// Set cursor to resize all when near divider
if (mouseNearDivider && !app.isDividing) {
SetMouseCursor(MOUSE_CURSOR_RESIZE_ALL);
@@ -620,7 +629,7 @@ int main(int argc, char* argv[])
}
// Handle divider drag
if (app.loaded && app.showScope && !app.showFileBrowser) {
if (app.loaded && app.showScope && !app.showFileBrowser && !app.showAbout) {
// Calculate divider position (using same calculation as selBounds)
float viewScale = GetUIScale();
float topMargin = 50 * viewScale;
@@ -736,6 +745,13 @@ int main(int argc, char* argv[])
}
}
// Dismiss the About dialog with a click. Handled here, after the
// spectrogram input above (which is gated off while it's open), so the
// dismissing click can't fall through and start a selection/pan.
if (app.showAbout && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
app.showAbout = false;
}
// Rendering
BeginDrawing();
ClearBackground((Color){ 30, 30, 30, 255 });
@@ -959,6 +975,9 @@ int main(int argc, char* argv[])
// Draw file browser on top (if active)
if (app.showFileBrowser) DrawFileBrowser();
// About / help dialog (topmost)
DrawAboutDialog();
// Export message notification
if (app.exportMessage[0] != '\0') {
int msgW = MeasureText(app.exportMessage, 20);