The selection readout was time-domain only — Energy/Peak/RMS/PAPR computed
from the whole-bandwidth waveform in the time span, ignoring the box's
frequency bounds entirely. The 2D box only measured one axis.
Add ComputeSpectralStats (stft.c): measures the boxed band from the STFT
magnitude (not the synchrosqueezed display buffer, which relocates energy)
and reports peak frequency, power-weighted centroid ("power center"),
occupied bandwidth (in-band span >3 dB over a median noise floor — robust
for both tones and noise-like bursts), and in-band SNR.
Also fold the two near-identical stats-panel blocks in render.c into one
DrawStatPanel + BuildSelectionStatLines helper so the live-drag and
committed-selection readouts can't drift.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ComputeSTFTIncremental (overview, skipFactor-strided) and
ComputeNextHighResChunk (high-res fill) had ~50 lines of identical
per-segment code (windowing, FFT, bin fill for both the normal and
derivative spectra). Extract it into ComputeSegment() with a reusable
SegScratch buffer set (allocated once per pass, no per-segment malloc).
Each caller keeps only its own skip logic. Behavior-preserving — verified
the rendered spectrogram is unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
- 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>
spectrogram.c was ~2950 lines holding everything. Break it into cohesive
translation units; spectrogram.c keeps only globals + the main frame loop.
New modules:
- spectrogram_types.h shared types, constants, extern globals, inline math
- fft.c/.h FFT, bit-reverse, twiddle (standalone, no app deps)
- stft.c/.h STFT compute, adaptive resolution, FFT-size LRU cache
- audio.c/.h WAV/ffmpeg load, FreeSignal, bandpass, playback
- render.c/.h UI scaling, colormaps, texture gen, on-screen drawing
- ui.c/.h file browser, sidebar, sliders, PNG export
Also:
- utils.c now includes utils.h instead of re-typedef'ing AudioSignal/
SignalStats (they had to be hand-synced before).
- Remove dead code: ApplyHannWindow and ComputeSTFTHighResRange were never
called (the live high-res path is ComputeNextHighResChunk).
- Delete the unused raylib-template main.c.
- rspektrum.make: build the new units. premake5.lua: glob src/**.c so a
future regen stays correct.
Pure code movement otherwise; no behavior change. Builds clean (-Wshadow).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>