Files
rspektrum/src/spectrogram_types.h
T
tyler 542126261e feat: export selection to WAV (band-limited, time-cropped)
New "Export WAV (W)" button + W key saves the current selection as a mono
WAV — the same band-limited, time-cropped audio you'd hear on playback, so
"what you hear is what you save". Self-documenting filename encodes the
time span and frequency band (rspektrum_sel_<t0>-<t1>s_<f0>-<f1>Hz.wav).

Refactor the shared filtered-region builder out of PlaySelectedRegion
(which also fixes a leak: it never freed regionSamples after
LoadSoundFromWave copies it). Make the export confirmation message persist
~3s instead of flashing for a single frame (affected PNG export too).

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

292 lines
10 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;
// 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;
// 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