542126261e
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>
292 lines
10 KiB
C
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
|