// 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 "mlnl.h" // MlnlAnnotations #include #include #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 within the PLAYING region float playheadElapsed; // Elapsed seconds since play started // Snapshot of the region actually handed to the audio device, captured at // PlaySelectedRegion time. The playhead must be measured against this, not // against the live app.sel — the user can move or resize the selection while // audio is still playing, and the marker has to keep tracking the sound // that's really coming out. playDuration comes from the buffer's own sample // count / sampleRate, so it can't drift from app.signal.duration (which is // derived pre-mono-downmix and disagrees for stereo files). float playSelStart, playSelEnd; // sel.timeStart/End when playback began float playDuration; // true length of the playing buffer, seconds // 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 // Sidebar scroll offset (px), for when controls overflow a short window float sidebarScroll; // 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 // Display-side frequency crop. Caps the displayed frequency axis at this // Hz value — purely a visualization concern (signal data, STFT, audio // playback are unaffected). 0 = no crop, use full Nyquist. The crop is // automatically clamped to the current signal's Nyquist by the helper // below, so a 3 kHz crop is harmless when loading a 2 kHz-sample file. // Persists across loads so a user analyzing mLink (≤3 kHz) doesn't have // to re-set it after every file open. float displayMaxFreqHz; // True when a fresh signal has loaded and is waiting for ApplyAutoCrop to // run (after the STFT exists, so the energy fallback can compute). Set by // ResetForNewSignal; cleared by the loadingPhase=2 hook once autocrop has // run, so an FFT-size change (same loadingPhase path) won't retrigger. bool autocropPending; // Notice splash shown when ApplyAutoCrop actually shrank the view. Modal: // user dismisses with "OK" (keep crop) or "Uncrop" (restore full view). bool autocropNoticeActive; char autocropNoticeMsg[256]; // Optional mLnL annotations parsed from the loaded WAV (empty if the file // doesn't carry the chunk). The annotations overlay has two surfaces: // 1. A faint always-on draw on the spectrogram (alpha = opacityBase). // 2. A "timeline lane" above the spectrogram for browsing events; // hover/click in the lane bumps the matching spectrogram overlay to // opacityHover so the user can find/inspect specific events without // the overlay drowning the underlying signal. MlnlAnnotations annotations; int hoveredEvent; // spectrogram-cursor hit (-1 = none); used for tooltip bool showAnnotations; // master on/off bool annotationsExpanded; // sidebar dropdown open (per-kind checkboxes etc.) bool annotationKindEnabled[MLNL_KIND_MAX]; // per-kind visibility (filters both surfaces) float annotationOpacityBase; // 0..1 — quiet always-on alpha for spectrogram overlay float annotationOpacityHover; // 0..1 — alpha for hovered/selected events // Timeline lane state. The lane is rendered between the freq-range banner // and the spectrogram pixels. Collapsed = single-row sparkline; expanded = // one row per kind currently enabled in the file. bool timelineExpanded; int hoveredTimelineEvent; // -1 = none; event index hovered in the lane int selectedAnnotation; // -1 = none; persistent selection from a lane click } 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); // Auto-crop the display freq axis and time view to "frequencies/times of // interest" using whichever source has high confidence: // 1) mLnL annotations — max(f_hi)+headroom for freq, span(t_start..t_end)+pad for time // 2) STFT energy heuristic — cumulative-energy threshold for freq, activity envelope for time // A no-op when neither source meets the confidence test (e.g. signal genuinely // uses most of the band/timeline). Modifies displayMaxFreqHz, view.start/end, // and invalidates the texture cache. Called automatically after STFT init on // every file load; can also be re-run from the sidebar button. void ApplyAutoCrop(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 || app.autocropNoticeActive; } // 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; } // Effective top of the displayed frequency axis (Hz). Capped at the actual // signal Nyquist so the crop never tries to show frequencies that aren't in // the data. All DISPLAY-side code paths (labels, banner, annotation freq // mapping, texture sampling fraction) should reach the frequency axis through // this helper instead of computing sampleRate*0.5 directly. Data-side math // (STFT bin spacing, PSD, audio filtering) keeps using the true Nyquist. static inline float EffectiveMaxFreqHz(void) { if (app.signal.sampleRate <= 0) return 1.0f; float nyq = app.signal.sampleRate * 0.5f; if (app.displayMaxFreqHz > 0.0f && app.displayMaxFreqHz < nyq) return app.displayMaxFreqHz; return nyq; } // Fraction of the texture's full frequency axis that should be visible (0..1). // Used by the spectrogram's texture sub-image extraction: a value of 0.5 means // "the visible window's freqEnd=1.0 corresponds to the texture's mid-row". static inline float DisplayFreqFraction(void) { if (app.signal.sampleRate <= 0) return 1.0f; float nyq = app.signal.sampleRate * 0.5f; if (nyq <= 0.0f) return 1.0f; return EffectiveMaxFreqHz() / nyq; } // ============================================================================ // 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