Files
rspektrum/src/spectrogram_types.h
T
tyler e8ed19d338 feat: spectrum-slice (PSD) panel + two-point marker/ruler tool
Spectrum slice (S): floating panel plotting the time-averaged power
spectrum of the current selection (or the visible view when there's no
selection). Frequency on X over the region's band, auto-ranged dB on Y,
with a peak marker. Backed by ComputePowerSpectrum() in stft.c (mean
linear power per bin over the time span). The selection stat panel now
biases to the left when this panel is up so the two don't overlap.

Marker/ruler tool (M): press-drag-release drops point A and B; the
overlay shows crosshairs, a connecting line, and a readout of the
ham-useful deltas — Δt, Δf, tone spacing (1/Δt), and drift (Δf/Δt).
Marker mode swaps the LMB-drag gesture from box-select to marker drop
(Alt/middle still pan); RMB/Esc clear the measurement. Markers reset on
new-file load alongside the selection.

Both are gated toggles (off by default), wired into the keymap (so they
self-document in the About dialog) and the sidebar.

Verified headlessly: idle viewport pixel-identical to baseline (AE=0,
deterministic); both panels render correctly with sensible numbers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 10:53:57 -07:00

309 lines
11 KiB
C

// spectrogram_types.h - Shared types, constants, globals, and small math helpers.
// This is the "spine" header included by every module.
#ifndef SPECTROGRAM_TYPES_H
#define SPECTROGRAM_TYPES_H
#include "raylib.h"
#include "utils.h" // AudioSignal, SignalStats
#include "primitives.h" // ScopeView, WaveformData
#include <stdbool.h>
#include <math.h>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
#ifndef CYAN
#define CYAN (Color){ 0, 255, 255, 255 }
#endif
// ============================================================================
// Configuration
// ============================================================================
#define FFT_SIZE_DEFAULT 2048
#define FFT_SIZE_MAX 2048
#define FFT_SIZE_MIN 128
#define HOP_RATIO 4 // FFT_SIZE / HOP_SIZE = 4 means 75% overlap
#define MAX_SAMPLE_RATE 48000
#define LOUDNESS_FLOOR_DB -80.0f
// Base resolution for proportional UI scaling (see GetUIScale in render.c)
#define BASE_WIDTH 1280
#define BASE_HEIGHT 800
#define FFT_CACHE_SIZE 4
// ============================================================================
// Data Structures
// ============================================================================
typedef enum {
COLORMAP_GRAYS = 0,
COLORMAP_INFERNO,
COLORMAP_VIRIDIS,
COLORMAP_PLASMA,
COLORMAP_HOT,
COLORMAP_COOL,
COLORMAP_COUNT
} ColormapType;
// How the colorizer maps amplitude to brightness:
// - RELATIVE: ceiling tracks the signal peak; floor sits dynRangeDb below it.
// - ABSOLUTE: fixed dBFS scale (0 dBFS ceiling = full scale, absolute floor).
typedef enum {
SCALE_RELATIVE = 0,
SCALE_ABSOLUTE
} AmplitudeScaleMode;
typedef struct {
float frequency;
float amplitude;
float phase;
} FrequencyData;
typedef struct {
FrequencyData* spectrum;
FrequencyData* derivativeSpectrum; // STFT with derivative window (for synchrosqueezing)
int numBins;
int sampleOffset;
int sampleCount;
} StftSegment;
typedef struct {
StftSegment* segments;
int numSegments;
int sampleRate;
int totalSamples;
bool useHannWindow;
} StftResult;
typedef struct {
int fftSize;
StftResult result;
int accessOrder; // lower = more recently accessed
} FFTCacheEntry;
typedef struct {
FFTCacheEntry entries[FFT_CACHE_SIZE];
int count;
int nextOrder;
} FFTSizeCache;
// The time+frequency box selection and its drag/move interaction state.
// All coordinates are 0-1 normalized. A "box-select" drags out a new box;
// a "move" drags an existing box around.
typedef struct {
float timeStart, timeEnd; // selected time span
float freqStart, freqEnd; // selected frequency span
bool isTimeSelecting; // dragging out a new time span
bool isFreqSelecting; // dragging out a new frequency span
Vector2 selectStartPos; // mouse pos when a box-select began (min-drag check)
bool isDragging; // moving an existing selection box
Vector2 dragStartPos; // mouse pos when the move began
float dragTimeStart; // selection start time when the move began
float dragFreqStart; // selection freq start when the move began
} Selection;
// Two-point ruler for measuring deltas on the spectrogram (Δt, Δf, baud, drift).
// Both points are 0-1 normalized: t over the whole signal, f as a fraction of
// Nyquist. A press-drag-release drops A at press and B at release; the readout
// stays on screen until cleared, so you can re-drag to re-measure.
typedef struct {
bool active; // a measurement exists (drawn + read out)
bool dragging; // mid-drag, placing point B
float t0, f0; // point A
float t1, f1; // point B
} MarkerTool;
// The visible window into the spectrogram (time + frequency), all 0-1
// normalized, plus the range captured at the start of a pan drag.
typedef struct {
float start, end; // visible time range
float freqStart, freqEnd; // visible freq range (0 = 0 Hz, 1 = Nyquist)
bool isPanning;
float panStart, panEnd; // time range captured when the pan began
float panFreqStart, panFreqEnd; // freq range captured when the pan began
Vector2 panStartPos; // mouse pos when the pan began
} Viewport;
typedef struct {
AudioSignal signal;
StftResult stft;
Image spectrogramImage;
Texture2D spectrogramTexture;
bool loaded;
bool stftComputed;
// Playback state
float playheadT; // 0-1 normalized position in selection
float playheadElapsed; // Elapsed seconds since play started
// Time + frequency box selection and its drag/move interaction state.
Selection sel;
// Two-point ruler tool. markerMode swaps the LMB-drag gesture from
// box-select to dropping markers; showSpectrum toggles the PSD slice panel.
MarkerTool marker;
bool markerMode;
bool showSpectrum;
// Export settings
float exportScale;
char exportDir[4096];
char exportMessage[256];
float exportMessageTimer; // seconds the export message stays on screen
// Visible viewport (time + frequency) and in-progress pan state.
Viewport view;
// Cached visible texture
Texture2D visibleTexture;
int cachedVisibleStart;
int cachedVisibleEnd;
int cachedVisibleStartY;
int cachedVisibleEndY;
bool visibleTextureValid;
// Display settings. amplitudeFloorDb/CeilingDb are the values the colorizer
// actually uses; they're derived from the mode + the controls below.
float amplitudeFloorDb;
float amplitudeCeilingDb;
AmplitudeScaleMode amplitudeMode;
float dynRangeDb; // RELATIVE mode: dB of range shown below the peak
float absoluteFloorDb; // ABSOLUTE mode: floor in dBFS (ceiling pinned at 0)
ColormapType colormap;
bool showGrid;
int fftSize; // Current FFT size (128-2048)
// Cached synchrosqueezed energy (the expensive reassignment result).
// Reused across dB-floor / colormap changes — only re-colorized, not recomputed.
float* reassignBuffer;
int reassignWidth;
int reassignHeight;
// Overlays
bool showAbout; // About / help dialog
// File browser state
bool showFileBrowser;
char browserPath[512];
char** browserFiles;
bool* browserIsDir;
int browserFileCount;
int browserScroll;
int browserSelected;
bool isBrowsing;
// Playback state
bool isPlaying;
bool playbackFinished; // Track if playback completed naturally
// Loading/processing state
int loadingPhase; // 0 = computing STFT, 1 = generating texture
float loadingProgress; // 0.0 to 1.0 overall progress
int currentSTFTSegment; // Which segment we're on for incremental processing
// Adaptive resolution: skipFactor=1 means compute all segments, skipFactor=N
// means compute every Nth segment (faster initial load, overview-only).
// highResFinished tracks whether full-res segments have been computed for
// the current view range.
int skipFactor;
bool highResFinished;
// Background high-res computation state.
// After the overview (skipFactor-strided) loads, missing segments are
// filled in at full resolution in the background while the user is idle.
int bgHighResSeg; // next segment index to compute at high-res
bool bgFinished; // true when all segments are computed at high-res
bool isBgProcessing; // true while background task is actively computing
// FFT size cache — LRU cache of previously computed STFT results.
// When user switches FFT sizes, we check the cache first to avoid
// recomputing. When cache is full, we evict the least-recently-used entry.
FFTSizeCache fftCache;
// Waveform scope view (underneath spectrogram viewport)
ScopeView scopeView;
bool showScope; // Toggle to show/hide scope view
// Scope view divider
float dividerY; // Y position of divider between spectrogram and scope (0-1 normalized)
bool isDividing; // True while user is dragging the divider
Vector2 dividerStartPos; // Mouse position when started dividing
float dividerStartY; // Spectro height when started dividing
} SpectrogramApp;
// ============================================================================
// Global State (defined in spectrogram.c)
// ============================================================================
extern SpectrogramApp app;
extern Sound AudioPlaybackSound;
extern Texture2D colormapTexture;
extern Font mainFont;
// Reset all per-signal state after a new signal is loaded into app.signal
// (defined in spectrogram.c; used by every load path).
void ResetForNewSignal(void);
// True when a modal overlay owns input; normal spectrogram/keyboard interaction
// is gated off while this is the case. Add new overlays here in one place.
static inline bool UiModalOpen(void)
{
return app.showFileBrowser || app.showAbout;
}
// Reset the box selection to the full signal (the "no selection" state).
static inline void ClearSelection(void)
{
app.sel.timeStart = 0.0f; app.sel.timeEnd = 1.0f;
app.sel.freqStart = 0.0f; app.sel.freqEnd = 1.0f;
}
// ============================================================================
// Keymap — single source of truth for global key bindings.
// The dispatcher (DispatchKeymap in spectrogram.c) runs every entry whose
// `action` is non-NULL and whose gate passes; the About/Help overlay renders
// the whole table so the on-screen key list can never drift from the bindings.
// Order-sensitive keys (Space, Esc) carry action==NULL and are handled inline
// where their frame ordering matters; they appear here for documentation only.
// ============================================================================
typedef void (*KeyActionFn)(void);
#define KEYGATE_NONE 0u
#define KEYGATE_MODAL 1u // skip while a modal overlay is open (!UiModalOpen())
#define KEYGATE_LOADED 2u // require a loaded signal
#define KEYGATE_STFT 4u // require a computed STFT
typedef struct {
int key; // raylib key code
unsigned gate; // KEYGATE_* bitmask
KeyActionFn action; // NULL = handled inline / documentation-only
const char* label; // shown in the help overlay, e.g. "O", "Home"
const char* help; // description for the help overlay
} KeyBinding;
// Returns the keymap table and its entry count (defined in spectrogram.c).
const KeyBinding* GetKeymap(int* count);
// ============================================================================
// Small math helpers (header-inline so every module can use them)
// ============================================================================
static inline float AmplitudeToDecibels(float amplitude)
{
if (amplitude < 0.0001f) amplitude = 0.0001f;
return 20.0f * log10f(amplitude);
}
static inline float Clamp(float value, float min, float max)
{
if (value < min) return min;
if (value > max) return max;
return value;
}
#endif // SPECTROGRAM_TYPES_H