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
+13 -8
View File
@@ -369,23 +369,28 @@ int ComputeSkipFactor(float signalDurationSec)
// This replaces existing overview (skipFactor-strided) segments with
// ===== Amplitude auto-scaling =====
// Derive the colorizer's floor/ceiling from the current scale mode and controls.
// Called after the STFT changes (load, background completion, FFT-size change)
// and when the user toggles the mode. Deriving the floor from dynRangeDb /
// absoluteFloorDb here is what preserves the user's setting across re-scales.
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;
if (app.amplitudeMode == SCALE_ABSOLUTE) {
// Fixed dBFS scale: 0 dBFS (full-scale) ceiling, user-set absolute floor.
app.amplitudeCeilingDb = 0.0f;
app.amplitudeFloorDb = app.absoluteFloorDb;
return;
}
// Relative: ceiling tracks the signal peak; floor sits dynRangeDb below it.
float maxDb = -999.0f;
float minDb = 0.0f;
for (int seg = 0; seg < stft->numSegments; seg++) {
for (int bin = 0; bin < stft->segments[seg].numBins; bin++) {
float db = AmplitudeToDecibels(stft->segments[seg].spectrum[bin].amplitude);
if (db > maxDb) maxDb = db;
if (db < minDb) minDb = db;
}
}
// Set ceiling at the max, floor 40dB below — enough range to see structure
// but not so wide that the signal is drowned in black
if (maxDb < -998.0f) maxDb = 0.0f; // no data yet — sane default
app.amplitudeCeilingDb = maxDb;
app.amplitudeFloorDb = maxDb - 40.0f;
app.amplitudeFloorDb = maxDb - app.dynRangeDb;
}