e8ed19d338
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>
309 lines
11 KiB
C
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
|