refactor: split spectrogram.c into per-concern modules
spectrogram.c was ~2950 lines holding everything. Break it into cohesive translation units; spectrogram.c keeps only globals + the main frame loop. New modules: - spectrogram_types.h shared types, constants, extern globals, inline math - fft.c/.h FFT, bit-reverse, twiddle (standalone, no app deps) - stft.c/.h STFT compute, adaptive resolution, FFT-size LRU cache - audio.c/.h WAV/ffmpeg load, FreeSignal, bandpass, playback - render.c/.h UI scaling, colormaps, texture gen, on-screen drawing - ui.c/.h file browser, sidebar, sliders, PNG export Also: - utils.c now includes utils.h instead of re-typedef'ing AudioSignal/ SignalStats (they had to be hand-synced before). - Remove dead code: ApplyHannWindow and ComputeSTFTHighResRange were never called (the live high-res path is ComputeNextHighResChunk). - Delete the unused raylib-template main.c. - rspektrum.make: build the new units. premake5.lua: glob src/**.c so a future regen stays correct. Pure code movement otherwise; no behavior change. Builds clean (-Wshadow). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,546 @@
|
||||
// ui.c - file browser, sidebar controls, sliders, and PNG export
|
||||
#include "ui.h"
|
||||
#include "render.h"
|
||||
#include "stft.h"
|
||||
#include "audio.h"
|
||||
#include "platform.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <math.h>
|
||||
|
||||
// Internal sidebar widgets (defined after DrawSidebar, which uses them)
|
||||
static void DrawSlider(Rectangle bounds, float value);
|
||||
static bool UpdateSlider(Rectangle bounds, float* value);
|
||||
|
||||
// ===== File browser =====
|
||||
void FreeBrowserFiles(void)
|
||||
{
|
||||
if (app.browserFiles) {
|
||||
for (int i = 0; i < app.browserFileCount; i++) free(app.browserFiles[i]);
|
||||
free(app.browserFiles);
|
||||
free(app.browserIsDir);
|
||||
app.browserFiles = NULL;
|
||||
app.browserIsDir = NULL;
|
||||
}
|
||||
app.browserFileCount = 0;
|
||||
}
|
||||
|
||||
void ScanDirectory(const char* path)
|
||||
{
|
||||
FreeBrowserFiles();
|
||||
strncpy(app.browserPath, path, sizeof(app.browserPath) - 1);
|
||||
FilePathList files = LoadDirectoryFiles(path);
|
||||
|
||||
int dirCount = 0, wavCount = 0;
|
||||
for (int i = 0; i < files.count; i++) {
|
||||
const char* name = GetFileName(files.paths[i]);
|
||||
if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
|
||||
if (DirectoryExists(files.paths[i])) dirCount++;
|
||||
else {
|
||||
const char* ext = GetFileExtension(files.paths[i]);
|
||||
if (ext && (strcmp(ext, ".wav") == 0 || strcmp(ext, ".WAV") == 0 ||
|
||||
strcmp(ext, ".Wave") == 0 || strcmp(ext, ".Wav") == 0)) wavCount++;
|
||||
}
|
||||
}
|
||||
|
||||
int totalCount = dirCount + wavCount;
|
||||
if (totalCount > 0) {
|
||||
app.browserFiles = (char**)malloc(totalCount * sizeof(char*));
|
||||
app.browserIsDir = (bool*)malloc(totalCount * sizeof(bool));
|
||||
app.browserFileCount = 0;
|
||||
|
||||
for (int i = 0; i < files.count; i++) {
|
||||
const char* name = GetFileName(files.paths[i]);
|
||||
if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
|
||||
if (DirectoryExists(files.paths[i])) {
|
||||
size_t len = strlen(name) + 1;
|
||||
app.browserFiles[app.browserFileCount] = (char*)malloc(len);
|
||||
if (app.browserFiles[app.browserFileCount]) {
|
||||
memcpy(app.browserFiles[app.browserFileCount], name, len);
|
||||
app.browserIsDir[app.browserFileCount] = true;
|
||||
app.browserFileCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < files.count; i++) {
|
||||
const char* name = GetFileName(files.paths[i]);
|
||||
if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
|
||||
if (!DirectoryExists(files.paths[i])) {
|
||||
const char* ext = GetFileExtension(files.paths[i]);
|
||||
if (ext && (strcmp(ext, ".wav") == 0 || strcmp(ext, ".WAV") == 0 ||
|
||||
strcmp(ext, ".Wave") == 0 || strcmp(ext, ".Wav") == 0)) {
|
||||
size_t len = strlen(name) + 1;
|
||||
app.browserFiles[app.browserFileCount] = (char*)malloc(len);
|
||||
if (app.browserFiles[app.browserFileCount]) {
|
||||
memcpy(app.browserFiles[app.browserFileCount], name, len);
|
||||
app.browserIsDir[app.browserFileCount] = false;
|
||||
app.browserFileCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
UnloadDirectoryFiles(files);
|
||||
app.browserScroll = 0;
|
||||
app.browserSelected = -1;
|
||||
}
|
||||
|
||||
static void NavigateToParentDirectory(void)
|
||||
{
|
||||
const char* parent = GetPrevDirectoryPath(app.browserPath);
|
||||
if (parent && strlen(parent) > 0) ScanDirectory(parent);
|
||||
}
|
||||
|
||||
static void NavigateToDirectory(const char* dirName)
|
||||
{
|
||||
char newPath[512];
|
||||
int written = snprintf(newPath, sizeof(newPath), "%s/%s", app.browserPath, dirName);
|
||||
if (written < 0 || written >= (int)sizeof(newPath)) return; // Path too long
|
||||
if (DirectoryExists(newPath)) ScanDirectory(newPath);
|
||||
}
|
||||
|
||||
static void LoadSelectedFile(void)
|
||||
{
|
||||
if (app.browserSelected < 0 || app.browserSelected >= app.browserFileCount) return;
|
||||
|
||||
char filePath[512];
|
||||
int written = snprintf(filePath, sizeof(filePath), "%s/%s", app.browserPath, app.browserFiles[app.browserSelected]);
|
||||
if (written < 0 || written >= (int)sizeof(filePath)) return; // Path too long
|
||||
|
||||
if (app.browserIsDir[app.browserSelected]) {
|
||||
NavigateToDirectory(app.browserFiles[app.browserSelected]);
|
||||
} else if (FileExists(filePath) && LoadWavFile(filePath, &app.signal)) {
|
||||
app.loaded = true;
|
||||
app.stftComputed = false;
|
||||
app.loadingPhase = 0;
|
||||
app.loadingProgress = 0.0f;
|
||||
app.currentSTFTSegment = 0;
|
||||
app.skipFactor = 1;
|
||||
app.highResFinished = false;
|
||||
app.bgHighResSeg = 0;
|
||||
app.bgFinished = false;
|
||||
app.isBgProcessing = false;
|
||||
// Signal changed — free cache (results are tied to signal data)
|
||||
FreeAllCacheEntries(&app.fftCache);
|
||||
app.timeSelectionStart = app.viewStart = 0.0f;
|
||||
app.timeSelectionEnd = app.viewEnd = 1.0f;
|
||||
app.freqSelectionStart = 0.0f;
|
||||
app.freqSelectionEnd = 1.0f;
|
||||
app.showFileBrowser = false;
|
||||
ComputeSTFTInit(&app.signal, &app.stft, app.fftSize);
|
||||
// Invalidate visible texture cache
|
||||
if (app.visibleTexture.id != 0) UnloadTexture(app.visibleTexture);
|
||||
app.visibleTexture = (Texture2D){ 0 };
|
||||
app.visibleTextureValid = false;
|
||||
TraceLog(LOG_INFO, "Loaded: %s", filePath);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawFileBrowser(void)
|
||||
{
|
||||
// Draw semi-transparent overlay first
|
||||
DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Fade(BLACK, 0.85f));
|
||||
|
||||
float scale = GetUIScale();
|
||||
float bw = 900.0f * scale, bh = 700.0f * scale;
|
||||
float bx = (GetScreenWidth() - bw) / 2, by = (GetScreenHeight() - bh) / 2;
|
||||
|
||||
DrawRectangle(bx, by, bw, bh, (Color){ 45, 45, 55, 255 });
|
||||
DrawRectangleLinesEx((Rectangle){ bx, by, bw, bh }, (int)(2 * scale), GRAY);
|
||||
DrawRectangle(bx, by, bw, (int)(40 * scale), (Color){ 60, 60, 75, 255 });
|
||||
DrawTextScaled("File Browser - Select WAV File", bx + (int)(15 * scale), by + (int)(8 * scale), (int)(20 * scale), WHITE);
|
||||
|
||||
// Path bar
|
||||
float pathBarY = by + (int)(46 * scale);
|
||||
DrawRectangle(bx + (int)(15 * scale), pathBarY, bw - (int)(110 * scale), (int)(30 * scale), (Color){ 30, 30, 40, 255 });
|
||||
DrawRectangleLinesEx((Rectangle){ bx + (int)(15 * scale), pathBarY, bw - (int)(110 * scale), (int)(30 * scale) }, (int)(1 * scale), GRAY);
|
||||
|
||||
char displayPath[300];
|
||||
strncpy(displayPath, app.browserPath, sizeof(displayPath) - 1);
|
||||
displayPath[sizeof(displayPath) - 1] = '\0';
|
||||
if (strlen(displayPath) > 60) sprintf(displayPath, "...%s", app.browserPath + strlen(app.browserPath) - 57);
|
||||
DrawTextScaled(displayPath, bx + (int)(22 * scale), pathBarY + (int)(5 * scale), (int)(14 * scale), LIGHTGRAY);
|
||||
|
||||
// Up button
|
||||
Rectangle upBtn = { bx + bw - (int)(90 * scale), pathBarY, (int)(75 * scale), (int)(30 * scale) };
|
||||
if (CheckCollisionPointRec(GetMousePosition(), upBtn)) DrawRectangleRec(upBtn, (Color){ 80, 80, 90, 255 });
|
||||
DrawTextScaled("UP (..)", upBtn.x + (int)(10 * scale), upBtn.y + (int)(7 * scale), (int)(14 * scale), WHITE);
|
||||
|
||||
// File list
|
||||
float lx = bx + (int)(15 * scale), ly = pathBarY + (int)(40 * scale);
|
||||
float lw = bw - (int)(30 * scale), lh = bh - (int)(195 * scale);
|
||||
DrawRectangle(lx, ly, lw, lh, (Color){ 25, 25, 35, 255 });
|
||||
DrawRectangleLinesEx((Rectangle){ lx, ly, lw, lh }, (int)(1 * scale), GRAY);
|
||||
|
||||
// Line height: base 36px scaled (enough for icon + filename without overlap)
|
||||
float lineH = 36 * scale;
|
||||
|
||||
// Handle empty directory
|
||||
int visibleItems = (int)(lh / lineH);
|
||||
if (visibleItems < 1) visibleItems = 1;
|
||||
|
||||
if (app.browserFileCount <= 0 || !app.browserFiles) {
|
||||
DrawTextScaled("(No WAV files in directory)", lx + (int)(20 * scale), ly + (int)(lh / 2 - 12 * scale), (int)(14 * scale), GRAY);
|
||||
} else {
|
||||
if (app.browserFileCount > visibleItems) {
|
||||
float sh = (float)visibleItems / app.browserFileCount * lh;
|
||||
if (sh < (int)(10 * scale)) sh = (int)(10 * scale);
|
||||
float sy = ly + (float)app.browserScroll / (app.browserFileCount - visibleItems) * (lh - sh);
|
||||
DrawRectangle(lx + lw - (int)(10 * scale), sy, (int)(8 * scale), sh, GRAY);
|
||||
}
|
||||
|
||||
int startItem = app.browserScroll;
|
||||
int endItem = startItem + visibleItems + 1;
|
||||
if (endItem > app.browserFileCount) endItem = app.browserFileCount;
|
||||
|
||||
float iconW = (int)(45 * scale); // space for icon column
|
||||
for (int i = startItem; i < endItem; i++) {
|
||||
if (i < 0 || i >= app.browserFileCount || !app.browserFiles[i] || !app.browserIsDir) continue;
|
||||
|
||||
float iy = ly + (i - startItem) * lineH + (int)(2 * scale);
|
||||
bool hovered = CheckCollisionPointRec((Vector2){ GetMouseX(), GetMouseY() }, (Rectangle){ lx + (int)(2 * scale), iy, lw - (int)(14 * scale), lineH - (int)(4 * scale) });
|
||||
|
||||
if (i == app.browserSelected) DrawRectangle(lx + (int)(2 * scale), iy, lw - (int)(14 * scale), (int)((lineH - 4) * scale), (Color){ 50, 70, 120, 180 });
|
||||
else if (hovered) DrawRectangle(lx + (int)(2 * scale), iy, lw - (int)(14 * scale), (int)((lineH - 4) * scale), (Color){ 60, 60, 80, 100 });
|
||||
|
||||
const char* icon = app.browserIsDir[i] ? "[DIR]" : "[WAV]";
|
||||
Color iconCol = app.browserIsDir[i] ? (Color){ 255, 220, 80, 255 } : (Color){ 80, 200, 120, 255 };
|
||||
DrawTextScaled(icon, lx + (int)(8 * scale), iy + (int)(4 * scale), (int)(13 * scale), iconCol);
|
||||
DrawTextScaled(app.browserFiles[i], lx + iconW + (int)(10 * scale), iy + (int)(4 * scale), (int)(14 * scale), WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll with mouse wheel
|
||||
if (CheckCollisionPointRec(GetMousePosition(), (Rectangle){ lx, ly, lw - (int)(10 * scale), lh }) && app.browserFileCount > 0) {
|
||||
int wheel = GetMouseWheelMove();
|
||||
if (wheel > 0) app.browserScroll--;
|
||||
if (wheel < 0) app.browserScroll++;
|
||||
if (app.browserScroll < 0) app.browserScroll = 0;
|
||||
int maxScroll = app.browserFileCount - visibleItems;
|
||||
if (maxScroll < 0) maxScroll = 0;
|
||||
if (app.browserScroll > maxScroll) app.browserScroll = maxScroll;
|
||||
}
|
||||
|
||||
// Handle clicks
|
||||
if (CheckCollisionPointRec(GetMousePosition(), upBtn) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) NavigateToParentDirectory();
|
||||
|
||||
if (CheckCollisionPointRec(GetMousePosition(), (Rectangle){ lx, ly, lw - (int)(10 * scale), lh }) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && app.browserFileCount > 0) {
|
||||
int clicked = app.browserScroll + (int)((GetMouseY() - ly) / lineH);
|
||||
if (clicked >= 0 && clicked < app.browserFileCount) {
|
||||
app.browserSelected = clicked;
|
||||
static double lastClick = 0;
|
||||
if (GetTime() - lastClick < 0.3) LoadSelectedFile();
|
||||
lastClick = GetTime();
|
||||
}
|
||||
}
|
||||
|
||||
// Buttons
|
||||
float btnY = by + bh - (int)(55 * scale);
|
||||
Rectangle openBtn = { bx + bw - (int)(170 * scale), btnY, (int)(150 * scale), (int)(40 * scale) };
|
||||
Rectangle cancelBtn = { bx + (int)(15 * scale), btnY, (int)(120 * scale), (int)(40 * scale) };
|
||||
|
||||
bool openHovered = CheckCollisionPointRec(GetMousePosition(), openBtn);
|
||||
bool openClicked = openHovered && IsMouseButtonPressed(MOUSE_LEFT_BUTTON);
|
||||
if (openHovered) DrawRectangleRec(openBtn, (Color){ 100, 100, 120, 255 });
|
||||
else DrawRectangleRec(openBtn, (Color){ 80, 80, 90, 255 });
|
||||
DrawRectangleLinesEx(openBtn, (int)(1 * scale), WHITE);
|
||||
DrawTextScaled("OPEN (Enter)", openBtn.x + (int)(25 * scale), openBtn.y + (int)(12 * scale), (int)(16 * scale), WHITE);
|
||||
|
||||
DrawRectangleRec(cancelBtn, (Color){ 100, 40, 40, 255 });
|
||||
DrawTextScaled("ESC Cancel", cancelBtn.x + (int)(18 * scale), cancelBtn.y + (int)(12 * scale), (int)(16 * scale), WHITE);
|
||||
|
||||
if ((IsKeyPressed(KEY_ENTER) || openClicked) && app.browserSelected >= 0 && app.browserFileCount > 0) LoadSelectedFile();
|
||||
if (IsKeyPressed(KEY_ESCAPE)) app.showFileBrowser = false;
|
||||
if (IsKeyPressed(KEY_UP) && app.browserSelected > 0 && app.browserFileCount > 0) {
|
||||
app.browserSelected--;
|
||||
if (app.browserSelected < app.browserScroll) app.browserScroll = app.browserSelected;
|
||||
}
|
||||
if (IsKeyPressed(KEY_DOWN) && app.browserSelected < app.browserFileCount - 1 && app.browserFileCount > 0) {
|
||||
app.browserSelected++;
|
||||
if (app.browserSelected >= app.browserScroll + visibleItems) app.browserScroll = app.browserSelected - visibleItems + 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ===== PNG export =====
|
||||
void ExportPNG(const SpectrogramApp* spa, const char* dirPath)
|
||||
{
|
||||
if (!spa->stftComputed || !spa->spectrogramImage.data) return;
|
||||
|
||||
int imgW = spa->spectrogramImage.width;
|
||||
int imgH = spa->spectrogramImage.height;
|
||||
|
||||
// Selection region in image-pixel coordinates
|
||||
int selX0 = (int)(spa->timeSelectionStart * imgW);
|
||||
int selX1 = (int)(spa->timeSelectionEnd * imgW);
|
||||
int selY0 = (int)((1.0f - spa->freqSelectionEnd) * imgH);
|
||||
int selY1 = (int)((1.0f - spa->freqSelectionStart) * imgH);
|
||||
|
||||
// Clamp to image bounds
|
||||
selX0 = Clamp(selX0, 0, imgW);
|
||||
selX1 = Clamp(selX1, 0, imgW);
|
||||
selY0 = Clamp(selY0, 0, imgH);
|
||||
selY1 = Clamp(selY1, 0, imgH);
|
||||
|
||||
int regionW = selX1 - selX0;
|
||||
int regionH = selY1 - selY0;
|
||||
if (regionW <= 0 || regionH <= 0) return;
|
||||
|
||||
// Extract selected region by copying pixel rows
|
||||
Image region = { 0 };
|
||||
region.width = regionW;
|
||||
region.height = regionH;
|
||||
region.mipmaps = 1;
|
||||
region.format = spa->spectrogramImage.format;
|
||||
region.data = RL_MALLOC(regionW * regionH * 4);
|
||||
if (!region.data) return;
|
||||
|
||||
// Raylib stores images as tightly packed RGBA rows - memcpy each row at a time
|
||||
Color* src = (Color*)spa->spectrogramImage.data;
|
||||
Color* dst = (Color*)region.data;
|
||||
for (int y = 0; y < regionH; y++) {
|
||||
memcpy(dst + y * regionW,
|
||||
src + (selY0 + y) * imgW + selX0,
|
||||
regionW * 4);
|
||||
}
|
||||
|
||||
// Scale if a non-zero exportScale is set (and region isn't too large)
|
||||
if (spa->exportScale > 0.0f && regionW < 4096) {
|
||||
int outW = regionW * (int)spa->exportScale;
|
||||
int outH = regionH * (int)spa->exportScale;
|
||||
if (outW > 0 && outH > 0) {
|
||||
ImageResize(®ion, outW, outH);
|
||||
}
|
||||
}
|
||||
|
||||
// Export as PNG
|
||||
char path[4096];
|
||||
if (spa->exportScale <= 0.0f && regionW == imgW) {
|
||||
// Full width export without scaling
|
||||
snprintf(path, sizeof(path), "%s/spectrogram_full.png", dirPath);
|
||||
} else {
|
||||
snprintf(path, sizeof(path), "%s/spectrogram_export.png", dirPath);
|
||||
}
|
||||
|
||||
if (ExportImage(region, path)) {
|
||||
snprintf((char*)spa->exportMessage, sizeof(spa->exportMessage),
|
||||
"Exported: %dx%d %.40s", region.width, region.height, path);
|
||||
} else {
|
||||
snprintf((char*)spa->exportMessage, sizeof(spa->exportMessage), "Export failed");
|
||||
}
|
||||
|
||||
RL_FREE(region.data);
|
||||
}
|
||||
|
||||
// ===== Sidebar =====
|
||||
void DrawSidebar(void)
|
||||
{
|
||||
float scale = GetUIScale();
|
||||
float sidebarWidth = 300 * scale;
|
||||
float x = 10 * scale;
|
||||
float y = 10 * scale;
|
||||
int fontSize = (int)(12 * scale);
|
||||
bool needsRegen = false;
|
||||
|
||||
// Dark sidebar background
|
||||
DrawRectangle(0, 0, (int)(sidebarWidth + 20 * scale), GetScreenHeight(), (Color){ 35, 35, 40, 255 });
|
||||
DrawLine((int)(sidebarWidth + 10 * scale), 0, (int)(sidebarWidth + 10 * scale), GetScreenHeight(), GRAY);
|
||||
|
||||
// Title
|
||||
DrawTextScaled("Spectrogram Controls", x, y, 16, WHITE); y += 28 * scale;
|
||||
|
||||
// FFT Size clicker
|
||||
DrawTextScaled(TextFormat("FFT: %d (%.1f Hz/bin)", app.fftSize, (float)app.signal.sampleRate / app.fftSize), x, y, 14, LIGHTGRAY); y += 20 * scale;
|
||||
Rectangle fftMinus = { x, y, 30 * scale, 25 * scale };
|
||||
Rectangle fftPlus = { x + sidebarWidth - 40 * scale, y, 30 * scale, 25 * scale };
|
||||
if (CheckCollisionPointRec(GetMousePosition(), fftMinus) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
|
||||
int newFFT = app.fftSize / 2;
|
||||
if (newFFT >= FFT_SIZE_MIN) {
|
||||
ChangeFFTSize(newFFT);
|
||||
}
|
||||
}
|
||||
if (CheckCollisionPointRec(GetMousePosition(), fftPlus) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
|
||||
int newFFT = app.fftSize * 2;
|
||||
if (newFFT <= FFT_SIZE_MAX) {
|
||||
ChangeFFTSize(newFFT);
|
||||
}
|
||||
}
|
||||
DrawRectangleRec(fftMinus, (Color){ 50, 50, 60, 255 });
|
||||
DrawRectangleLinesEx(fftMinus, 1, GRAY);
|
||||
DrawTextScaled("-", fftMinus.x + 12 * scale, fftMinus.y + 5 * scale, 18, WHITE);
|
||||
DrawRectangleRec(fftPlus, (Color){ 50, 50, 60, 255 });
|
||||
DrawRectangleLinesEx(fftPlus, 1, GRAY);
|
||||
DrawTextScaled("+", fftPlus.x + 10 * scale, fftPlus.y + 5 * scale, 18, WHITE);
|
||||
y += 32 * scale;
|
||||
|
||||
// dB Floor slider
|
||||
DrawTextScaled(TextFormat("dB Floor: %.1f", app.amplitudeFloorDb), x, y, 14, LIGHTGRAY); y += 20 * scale;
|
||||
Rectangle dbSlider = { x, y, sidebarWidth - 10 * scale, 20 * scale };
|
||||
float dbValue = (app.amplitudeFloorDb + 100.0f) / 80.0f;
|
||||
DrawSlider(dbSlider, dbValue);
|
||||
if (UpdateSlider(dbSlider, &dbValue)) {
|
||||
app.amplitudeFloorDb = -100.0f + dbValue * 80.0f;
|
||||
needsRegen = true;
|
||||
}
|
||||
y += 28 * scale;
|
||||
|
||||
// Colormap dropdown
|
||||
DrawTextScaled("Colormap:", x, y, 14, LIGHTGRAY); y += 20 * scale;
|
||||
const char* colormapNames[] = { "Grays", "Inferno", "Viridis", "Plasma", "Hot", "Cool" };
|
||||
Rectangle cmapButton = { x, y, sidebarWidth - 10 * scale, 25 * scale };
|
||||
if (CheckCollisionPointRec(GetMousePosition(), cmapButton) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
|
||||
app.colormap = (ColormapType)((app.colormap + 1) % COLORMAP_COUNT);
|
||||
GenerateColormapTexture();
|
||||
needsRegen = true;
|
||||
}
|
||||
DrawRectangleRec(cmapButton, (Color){ 50, 50, 60, 255 });
|
||||
DrawRectangleLinesEx(cmapButton, 1, GRAY);
|
||||
DrawTextScaled(colormapNames[app.colormap], cmapButton.x + 10 * scale, cmapButton.y + 6 * scale, 14, WHITE);
|
||||
DrawTexturePro(colormapTexture, (Rectangle){ 0, 0, 256, 1 },
|
||||
(Rectangle){ cmapButton.x + cmapButton.width - 60 * scale, cmapButton.y + 5 * scale, 50 * scale, 15 * scale },
|
||||
(Vector2){ 0, 0 }, 0.0f, WHITE);
|
||||
y += 32 * scale;
|
||||
|
||||
// Grid toggle
|
||||
Rectangle gridCheck = { x, y, 18 * scale, 18 * scale };
|
||||
if (CheckCollisionPointRec(GetMousePosition(), gridCheck) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
|
||||
app.showGrid = !app.showGrid;
|
||||
}
|
||||
DrawRectangleRec(gridCheck, app.showGrid ? BLUE : DARKGRAY);
|
||||
DrawRectangleLinesEx(gridCheck, 1, WHITE);
|
||||
DrawTextScaled("Show Grid", x + 25 * scale, y + 2 * scale, 14, LIGHTGRAY); y += 28 * scale;
|
||||
|
||||
// File loading
|
||||
DrawTextScaled("File:", x, y, 14, LIGHTGRAY); y += 20 * scale;
|
||||
Rectangle fileButton = { x, y, sidebarWidth - 10 * scale, 25 * scale };
|
||||
if (CheckCollisionPointRec(GetMousePosition(), fileButton) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
|
||||
app.showFileBrowser = true;
|
||||
ScanDirectory(GetWorkingDirectory());
|
||||
}
|
||||
DrawRectangleRec(fileButton, (Color){ 50, 50, 60, 255 });
|
||||
DrawRectangleLinesEx(fileButton, 1, GRAY);
|
||||
DrawTextScaled("Open File Browser (O)", fileButton.x + 10 * scale, fileButton.y + 6 * scale, 14, WHITE);
|
||||
y += 38 * scale;
|
||||
|
||||
// Playback
|
||||
Rectangle playButton = { x, y, sidebarWidth - 10 * scale, 35 * scale };
|
||||
if (CheckCollisionPointRec(GetMousePosition(), playButton) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
|
||||
if (app.isPlaying && AudioPlaybackSound.frameCount > 0) {
|
||||
StopSound(AudioPlaybackSound);
|
||||
app.isPlaying = false;
|
||||
app.playheadElapsed = 0;
|
||||
app.playheadT = 0;
|
||||
} else {
|
||||
PlaySelectedRegion();
|
||||
app.isPlaying = true;
|
||||
}
|
||||
}
|
||||
const char* playText = app.isPlaying ? "STOP (SPACE)" : "PLAY (SPACE)";
|
||||
DrawRectangleRec(playButton, app.isPlaying ? (Color){ 120, 40, 40, 255 } : (Color){ 40, 100, 40, 255 });
|
||||
DrawRectangleLinesEx(playButton, 1, app.isPlaying ? RED : GREEN);
|
||||
DrawTextScaled(playText, playButton.x + 10 * scale, playButton.y + 12 * scale, 14, WHITE);
|
||||
y += 48 * scale;
|
||||
|
||||
// Fullscreen toggle
|
||||
Rectangle fsButton = { x, y, sidebarWidth - 10 * scale, 25 * scale };
|
||||
bool isFullscreen = IsWindowFullscreen();
|
||||
if (CheckCollisionPointRec(GetMousePosition(), fsButton) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
|
||||
ToggleFullscreen();
|
||||
}
|
||||
DrawRectangleRec(fsButton, isFullscreen ? (Color){ 40, 80, 120, 255 } : (Color){ 50, 50, 60, 255 });
|
||||
DrawRectangleLinesEx(fsButton, 1, GRAY);
|
||||
DrawTextScaled(isFullscreen ? "Exit Fullscreen (F11)" : "Fullscreen (F11)", fsButton.x + 10 * scale, fsButton.y + 6 * scale, 14, WHITE);
|
||||
y += 38 * scale;
|
||||
|
||||
// Reset/Clear buttons
|
||||
Rectangle resetButton = { x, y, (sidebarWidth - 15 * scale) / 2, 25 * scale };
|
||||
if (CheckCollisionPointRec(GetMousePosition(), resetButton) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
|
||||
app.timeSelectionStart = app.viewStart;
|
||||
app.timeSelectionEnd = app.viewEnd;
|
||||
app.freqSelectionStart = 0.0f;
|
||||
app.freqSelectionEnd = 1.0f;
|
||||
}
|
||||
DrawRectangleRec(resetButton, (Color){ 80, 50, 50, 255 });
|
||||
DrawRectangleLinesEx(resetButton, 1, RED);
|
||||
DrawTextScaled("Reset Sel (R)", resetButton.x + 10 * scale, resetButton.y + 6 * scale, 14, WHITE);
|
||||
|
||||
Rectangle clearButton = { x + resetButton.width + 5 * scale, y, (sidebarWidth - 15 * scale) / 2, 25 * scale };
|
||||
if (CheckCollisionPointRec(GetMousePosition(), clearButton) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
|
||||
app.timeSelectionStart = 0.0f;
|
||||
app.timeSelectionEnd = 1.0f;
|
||||
app.freqSelectionStart = 0.0f;
|
||||
app.freqSelectionEnd = 1.0f;
|
||||
}
|
||||
DrawRectangleRec(clearButton, (Color){ 80, 50, 50, 255 });
|
||||
DrawRectangleLinesEx(clearButton, 1, RED);
|
||||
DrawTextScaled("Clear (ESC)", clearButton.x + 10 * scale, clearButton.y + 6 * scale, 14, WHITE);
|
||||
y += 38 * scale;
|
||||
|
||||
// Export PNG
|
||||
{
|
||||
Rectangle exportButton = { x, y, sidebarWidth - 10 * scale, 30 * scale };
|
||||
if (CheckCollisionPointRec(GetMousePosition(), exportButton) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
|
||||
ExportPNG(&app, app.exportDir);
|
||||
}
|
||||
DrawRectangleRec(exportButton, (Color){ 40, 60, 80, 255 });
|
||||
DrawRectangleLinesEx(exportButton, 1, CYAN);
|
||||
DrawTextScaled("Export PNG (E)", exportButton.x + 10 * scale, exportButton.y + 7 * scale, 14, WHITE);
|
||||
y += 35 * scale;
|
||||
|
||||
// Export scale slider
|
||||
DrawTextScaled(TextFormat("Export Scale: %.1fx", app.exportScale), x, y, 12, LIGHTGRAY); y += 18 * scale;
|
||||
Rectangle scaleSlider = { x, y, sidebarWidth - 10 * scale, 14 * scale };
|
||||
DrawSlider(scaleSlider, app.exportScale / 10.0f);
|
||||
float scaleVal = 0.0f;
|
||||
if (UpdateSlider(scaleSlider, &scaleVal)) {
|
||||
app.exportScale = scaleVal * 10.0f;
|
||||
}
|
||||
y += 24 * scale;
|
||||
}
|
||||
|
||||
// Signal info
|
||||
DrawTextScaled("Signal Info:", x, y, 14, LIGHTGRAY); y += 20 * scale;
|
||||
if (app.loaded) {
|
||||
DrawTextScaled(TextFormat("Sample Rate: %d Hz", app.signal.sampleRate), x, y, 14, GRAY); y += 18 * scale;
|
||||
DrawTextScaled(TextFormat("Duration: %.2f sec", app.signal.duration), x, y, 14, GRAY); y += 18 * scale;
|
||||
DrawTextScaled(TextFormat("Max Freq: %.1f kHz", (float)app.signal.sampleRate / 2000.0f), x, y, 14, GRAY); y += 18 * scale;
|
||||
} else {
|
||||
DrawTextScaled("No file loaded", x, y, 14, GRAY); y += 18 * scale;
|
||||
}
|
||||
|
||||
if (needsRegen && app.stftComputed) {
|
||||
GenerateSpectrogramTexture(&app.stft, &app.spectrogramImage, &app.spectrogramTexture);
|
||||
app.visibleTextureValid = false; // Force cache invalidation
|
||||
}
|
||||
}
|
||||
|
||||
static void DrawSlider(Rectangle bounds, float value)
|
||||
{
|
||||
// Background
|
||||
DrawRectangleRec(bounds, DARKGRAY);
|
||||
DrawRectangleLinesEx(bounds, 1, GRAY);
|
||||
|
||||
// Fill
|
||||
float fillWidth = (bounds.width - 4) * value;
|
||||
DrawRectangle(bounds.x + 2, bounds.y + 2, fillWidth, bounds.height - 4, BLUE);
|
||||
|
||||
// Handle
|
||||
float handleX = bounds.x + 2 + fillWidth;
|
||||
DrawRectangle(handleX - 3, bounds.y + 1, 6, bounds.height - 2, WHITE);
|
||||
}
|
||||
|
||||
static bool UpdateSlider(Rectangle bounds, float* value)
|
||||
{
|
||||
bool changed = false;
|
||||
if (CheckCollisionPointRec(GetMousePosition(), bounds) && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) {
|
||||
float newValue = (GetMousePosition().x - bounds.x - 2) / (bounds.width - 4);
|
||||
newValue = Clamp(newValue, 0.0f, 1.0f);
|
||||
if (fabsf(newValue - *value) > 0.001f) {
|
||||
*value = newValue;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
Reference in New Issue
Block a user