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:
@@ -355,17 +355,42 @@ void DrawSidebar(void)
|
||||
DrawTextScaled("+", fftPlus.x + 10 * scale, fftPlus.y + 5 * scale, 18, WHITE);
|
||||
y += 32 * scale;
|
||||
|
||||
// dB Floor slider
|
||||
DrawTextScaled(TextFormat("dB Floor: %.1f", app.amplitudeFloorDb), x, y, 14, LIGHTGRAY); y += 20 * scale;
|
||||
Rectangle dbSlider = { x, y, sidebarWidth - 10 * scale, 20 * scale };
|
||||
float dbValue = (app.amplitudeFloorDb + 100.0f) / 80.0f;
|
||||
DrawSlider(dbSlider, dbValue);
|
||||
if (UpdateSlider(dbSlider, &dbValue)) {
|
||||
app.amplitudeFloorDb = -100.0f + dbValue * 80.0f;
|
||||
app.amplitudeUserSet = true; // stop auto-scale from overwriting this
|
||||
// Amplitude scale mode toggle (Relative dynamic range vs Absolute dBFS)
|
||||
Rectangle scaleBtn = { x, y, sidebarWidth - 10 * scale, 22 * scale };
|
||||
if (CheckCollisionPointRec(GetMousePosition(), scaleBtn) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
|
||||
app.amplitudeMode = (app.amplitudeMode == SCALE_RELATIVE) ? SCALE_ABSOLUTE : SCALE_RELATIVE;
|
||||
AutoScaleAmplitude(&app.stft); // re-derive floor/ceiling for the new mode
|
||||
needsRegen = true;
|
||||
}
|
||||
y += 28 * scale;
|
||||
DrawRectangleRec(scaleBtn, (Color){ 50, 50, 60, 255 });
|
||||
DrawRectangleLinesEx(scaleBtn, 1, GRAY);
|
||||
DrawTextScaled(app.amplitudeMode == SCALE_ABSOLUTE ? "Scale: Absolute (dBFS)" : "Scale: Relative",
|
||||
scaleBtn.x + 8 * scale, scaleBtn.y + 4 * scale, 13, WHITE);
|
||||
y += 30 * scale;
|
||||
|
||||
// Floor control — meaning depends on the scale mode.
|
||||
Rectangle dbSlider = { x, y + 20 * scale, sidebarWidth - 10 * scale, 20 * scale };
|
||||
if (app.amplitudeMode == SCALE_ABSOLUTE) {
|
||||
DrawTextScaled(TextFormat("Floor: %.1f dBFS", app.absoluteFloorDb), x, y, 14, LIGHTGRAY);
|
||||
float dbValue = (app.absoluteFloorDb + 100.0f) / 100.0f; // -100..0 dBFS -> 0..1
|
||||
DrawSlider(dbSlider, dbValue);
|
||||
if (UpdateSlider(dbSlider, &dbValue)) {
|
||||
app.absoluteFloorDb = -100.0f + dbValue * 100.0f;
|
||||
app.amplitudeCeilingDb = 0.0f;
|
||||
app.amplitudeFloorDb = app.absoluteFloorDb;
|
||||
needsRegen = true;
|
||||
}
|
||||
} else {
|
||||
DrawTextScaled(TextFormat("Dyn Range: %.0f dB", app.dynRangeDb), x, y, 14, LIGHTGRAY);
|
||||
float dbValue = (100.0f - app.dynRangeDb) / 90.0f; // 100..10 dB -> 0..1 (right = more contrast)
|
||||
DrawSlider(dbSlider, dbValue);
|
||||
if (UpdateSlider(dbSlider, &dbValue)) {
|
||||
app.dynRangeDb = 100.0f - dbValue * 90.0f;
|
||||
app.amplitudeFloorDb = app.amplitudeCeilingDb - app.dynRangeDb;
|
||||
needsRegen = true;
|
||||
}
|
||||
}
|
||||
y += 48 * scale;
|
||||
|
||||
// Colormap dropdown
|
||||
DrawTextScaled("Colormap:", x, y, 14, LIGHTGRAY); y += 20 * scale;
|
||||
@@ -490,7 +515,18 @@ void DrawSidebar(void)
|
||||
} else {
|
||||
DrawTextScaled("No file loaded", x, y, 14, GRAY); y += 18 * scale;
|
||||
}
|
||||
|
||||
y += 10 * scale;
|
||||
|
||||
// About / Help button
|
||||
Rectangle aboutBtn = { x, y, sidebarWidth - 10 * scale, 24 * scale };
|
||||
if (CheckCollisionPointRec(GetMousePosition(), aboutBtn) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
|
||||
app.showAbout = true;
|
||||
}
|
||||
DrawRectangleRec(aboutBtn, (Color){ 50, 50, 60, 255 });
|
||||
DrawRectangleLinesEx(aboutBtn, 1, GRAY);
|
||||
DrawTextScaled("About / Help (F1)", aboutBtn.x + 8 * scale, aboutBtn.y + 5 * scale, 13, WHITE);
|
||||
y += 30 * scale;
|
||||
|
||||
if (needsRegen && app.stftComputed) {
|
||||
// dB floor / colormap only — re-map the cached reassignment, don't recompute it.
|
||||
ColorizeSpectrogram(&app.spectrogramImage, &app.spectrogramTexture);
|
||||
@@ -526,3 +562,64 @@ static bool UpdateSlider(Rectangle bounds, float* value)
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
// ===== About / Help dialog =====
|
||||
|
||||
void DrawAboutDialog(void)
|
||||
{
|
||||
if (!app.showAbout) return;
|
||||
|
||||
float scale = GetUIScale();
|
||||
int sw = GetScreenWidth();
|
||||
int sh = GetScreenHeight();
|
||||
|
||||
// Dim the background; a click outside the panel (or Esc/F1) closes it.
|
||||
DrawRectangle(0, 0, sw, sh, Fade(BLACK, 0.7f));
|
||||
|
||||
float pw = 560 * scale;
|
||||
float ph = 470 * scale;
|
||||
Rectangle panel = { (sw - pw) / 2, (sh - ph) / 2, pw, ph };
|
||||
DrawRectangleRec(panel, (Color){ 30, 30, 38, 255 });
|
||||
DrawRectangleLinesEx(panel, 2, (Color){ 90, 90, 110, 255 });
|
||||
|
||||
float px = panel.x + 24 * scale;
|
||||
float py = panel.y + 20 * scale;
|
||||
|
||||
DrawTextScaled("rspektrum — synchrosqueezed spectrogram viewer", px, py, 18, WHITE);
|
||||
py += 34 * scale;
|
||||
|
||||
// Body lines. NULL = blank spacer; headings start with no leading spaces.
|
||||
const char* lines[] = {
|
||||
"Amplitude scale",
|
||||
" Relative: ceiling tracks the signal peak; the slider sets how many",
|
||||
" dB of range are shown below it (good for seeing structure).",
|
||||
" Absolute: fixed dBFS scale, 0 dBFS = digital full scale (sample = 1.0).",
|
||||
" Brightness then reflects a real level, comparable across files.",
|
||||
"",
|
||||
"Why dBFS and not dBm",
|
||||
" A WAV carries no power reference (impedance / receiver calibration),",
|
||||
" so absolute levels are shown in dBFS, not dBm. 0 dBFS = full scale.",
|
||||
"",
|
||||
"Accuracy caveats",
|
||||
" - The Hann window adds a fixed ~6 dB coherent-gain offset to",
|
||||
" spectral magnitudes.",
|
||||
" - Pixels show synchrosqueezed (reassigned) energy, not raw FFT bins.",
|
||||
" Good to ~a dB for visualization; not lab-grade metrology.",
|
||||
"",
|
||||
"Keys",
|
||||
" O open file P toggle scope Home/End reset / fit view",
|
||||
" F1 / Esc close this dialog",
|
||||
};
|
||||
int n = sizeof(lines) / sizeof(lines[0]);
|
||||
for (int i = 0; i < n; i++) {
|
||||
const char* s = lines[i];
|
||||
bool heading = (s[0] != '\0' && s[0] != ' ');
|
||||
DrawTextScaled(s, px, py, heading ? 14 : 13, heading ? (Color){ 150, 200, 255, 255 } : LIGHTGRAY);
|
||||
py += (s[0] == '\0' ? 8 : 18) * scale;
|
||||
}
|
||||
|
||||
DrawTextScaled("F1 / Esc / click to close", panel.x + pw - 196 * scale,
|
||||
panel.y + ph - 26 * scale, 12, GRAY);
|
||||
// Note: opening/closing is handled in the main input loop (not here) so the
|
||||
// same click/keypress that opens the dialog can't immediately close it.
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user