Add sidebar UI with sliders, buttons, checkboxes. ESC clears selections instead of exiting

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-03-30 20:05:57 -07:00
parent bbd476c389
commit 87183cbe7d
+170 -98
View File
@@ -28,8 +28,8 @@
// ============================================================================
#define FFT_SIZE_DEFAULT 2048
#define FFT_SIZE_MAX 8192
#define FFT_SIZE_MIN 512
#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
@@ -786,74 +786,187 @@ static void DrawLabels(Rectangle bounds)
DrawText("Freq", bounds.x - 35, bounds.y + bounds.height / 2, fontSize, GRAY);
}
static void DrawSlider(Rectangle bounds, float value);
static bool UpdateSlider(Rectangle bounds, float* value);
static void DrawSidebar(void);
static void DrawSelection(Rectangle bounds)
{
Color overlayColor = Fade(BLACK, 0.5f);
float selStartX = bounds.x + app.timeSelectionStart * bounds.width;
float selEndX = bounds.x + app.timeSelectionEnd * bounds.width;
if (selStartX > bounds.x) DrawRectangle(bounds.x, bounds.y, selStartX - bounds.x, bounds.height, overlayColor);
if (selEndX < bounds.x + bounds.width) DrawRectangle(selEndX, bounds.y, bounds.x + bounds.width - selEndX, bounds.height, overlayColor);
float selStartY = bounds.y + bounds.height - app.freqSelectionEnd * bounds.height;
float selEndY = bounds.y + bounds.height - app.freqSelectionStart * bounds.height;
if (selStartY > bounds.y) DrawRectangle(selStartX, bounds.y, selEndX - selStartX, selStartY - bounds.y, overlayColor);
if (selEndY < bounds.y + bounds.height) DrawRectangle(selStartX, selEndY, selEndX - selStartX, bounds.y + bounds.height - selEndY, overlayColor);
DrawLineV((Vector2){ selStartX, bounds.y }, (Vector2){ selStartX, bounds.y + bounds.height }, YELLOW);
DrawLineV((Vector2){ selEndX, bounds.y }, (Vector2){ selEndX, bounds.y + bounds.height }, YELLOW);
if (app.freqSelectionStart > 0.0f || app.freqSelectionEnd < 1.0f) {
DrawLineV((Vector2){ selStartX, selStartY }, (Vector2){ selEndX, selStartY }, CYAN);
DrawLineV((Vector2){ selStartX, selEndY }, (Vector2){ selEndX, selEndY }, CYAN);
}
}
static void DrawInfo(Rectangle bounds)
static void DrawSidebar(void)
{
int fontSize = 12, y = 10;
float sidebarWidth = 280;
float x = 10;
float y = 10;
int fontSize = 12;
bool needsRegen = false;
// Title
DrawText("Spectrogram Controls", x, y, 14, WHITE); y += 25;
// FFT Size slider
DrawText(TextFormat("FFT Size: %d (%.1f Hz/bin)", app.fftSize, (float)app.signal.sampleRate / app.fftSize), x, y, fontSize, LIGHTGRAY); y += 18;
Rectangle fftSlider = { x, y, sidebarWidth - 10, 20 };
float fftValue = (float)(app.fftSize - FFT_SIZE_MIN) / (FFT_SIZE_MAX - FFT_SIZE_MIN);
DrawSlider(fftSlider, fftValue);
if (UpdateSlider(fftSlider, &fftValue)) {
int newFFT = FFT_SIZE_MIN + (int)(fftValue * (FFT_SIZE_MAX - FFT_SIZE_MIN));
// Round to power of 2
while ((newFFT & (newFFT - 1)) != 0) newFFT++;
if (newFFT != app.fftSize && newFFT >= FFT_SIZE_MIN && newFFT <= FFT_SIZE_MAX) {
app.fftSize = newFFT;
app.stftComputed = false;
needsRegen = true;
}
}
y += 25;
// dB Floor slider
DrawText(TextFormat("dB Floor: %.1f", app.amplitudeFloorDb), x, y, fontSize, LIGHTGRAY); y += 18;
Rectangle dbSlider = { x, y, sidebarWidth - 10, 20 };
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 += 25;
// Colormap dropdown
DrawText("Colormap:", x, y, fontSize, LIGHTGRAY); y += 18;
const char* colormapNames[] = { "Grays", "Inferno", "Viridis", "Plasma", "Hot", "Cool" };
Rectangle cmapButton = { x, y, sidebarWidth - 10, 25 };
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);
DrawText(colormapNames[app.colormap], cmapButton.x + 10, cmapButton.y + 6, fontSize, WHITE);
DrawTexturePro(colormapTexture, (Rectangle){ 0, 0, 256, 1 },
(Rectangle){ cmapButton.x + cmapButton.width - 60, cmapButton.y + 5, 50, 15 },
(Vector2){ 0, 0 }, 0.0f, WHITE);
y += 30;
// Grid toggle
Rectangle gridCheck = { x, y, 18, 18 };
if (CheckCollisionPointRec(GetMousePosition(), gridCheck) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
app.showGrid = !app.showGrid;
}
DrawRectangleRec(gridCheck, app.showGrid ? BLUE : DARKGRAY);
DrawRectangleLinesEx(gridCheck, 1, WHITE);
DrawText("Show Grid", x + 25, y + 2, fontSize, LIGHTGRAY); y += 25;
// File loading
DrawText("File:", x, y, fontSize, LIGHTGRAY); y += 18;
Rectangle fileButton = { x, y, sidebarWidth - 10, 25 };
if (CheckCollisionPointRec(GetMousePosition(), fileButton) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
app.showFileBrowser = true;
ScanDirectory(GetWorkingDirectory());
}
DrawRectangleRec(fileButton, (Color){ 50, 50, 60, 255 });
DrawRectangleLinesEx(fileButton, 1, GRAY);
DrawText("Open File Browser (O)", fileButton.x + 10, fileButton.y + 6, fontSize, WHITE);
y += 35;
// Playback
Rectangle playButton = { x, y, sidebarWidth - 10, 35 };
if (CheckCollisionPointRec(GetMousePosition(), playButton) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
PlaySelectedRegion();
}
DrawRectangleRec(playButton, (Color){ 40, 100, 40, 255 });
DrawRectangleLinesEx(playButton, 1, GREEN);
DrawText("PLAY Selection (SPACE)", playButton.x + 10, playButton.y + 12, fontSize, WHITE);
y += 45;
// Reset/Clear buttons
Rectangle resetButton = { x, y, (sidebarWidth - 15) / 2, 25 };
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);
DrawText("Reset Sel (R)", resetButton.x + 10, resetButton.y + 6, fontSize, WHITE);
Rectangle clearButton = { x + resetButton.width + 5, y, (sidebarWidth - 15) / 2, 25 };
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);
DrawText("Clear (ESC)", clearButton.x + 10, clearButton.y + 6, fontSize, WHITE);
y += 35;
// Signal info
DrawText("Signal Info:", x, y, fontSize, LIGHTGRAY); y += 18;
if (app.loaded) {
DrawText(TextFormat("Sample Rate: %d Hz", app.signal.sampleRate), 10, y, fontSize, LIGHTGRAY); y += 20;
DrawText(TextFormat("Duration: %.2f sec", app.signal.duration), 10, y, fontSize, LIGHTGRAY); y += 20;
DrawText(TextFormat("View: %.1f%%-%.1f%% (%.2f sec)", app.viewStart*100, app.viewEnd*100, (app.viewEnd-app.viewStart)*app.signal.duration), 10, y, fontSize, LIGHTGRAY); y += 20;
DrawText(TextFormat("FFT: %d (%.1f Hz/bin)", app.fftSize, (float)app.signal.sampleRate / app.fftSize), 10, y, fontSize, LIGHTGRAY); y += 20;
DrawText(TextFormat("Max Freq: %.1f kHz", (float)app.signal.sampleRate / 2000.0f), 10, y, fontSize, LIGHTGRAY); y += 20;
y += 10;
float selDuration = (app.timeSelectionEnd - app.timeSelectionStart) * app.signal.duration;
DrawText(TextFormat("Time Sel: %.2f-%.2f sec (%.3f sec)", app.timeSelectionStart*app.signal.duration, app.timeSelectionEnd*app.signal.duration, selDuration), 10, y, fontSize, YELLOW); y += 20;
float maxFreq = (float)app.signal.sampleRate / 2.0f;
DrawText(TextFormat("Freq Sel: %.0f-%.0f Hz", app.freqSelectionStart*maxFreq, app.freqSelectionEnd*maxFreq), 10, y, fontSize, CYAN);
DrawText(TextFormat("Sample Rate: %d Hz", app.signal.sampleRate), x, y, fontSize, GRAY); y += 16;
DrawText(TextFormat("Duration: %.2f sec", app.signal.duration), x, y, fontSize, GRAY); y += 16;
DrawText(TextFormat("Max Freq: %.1f kHz", (float)app.signal.sampleRate / 2000.0f), x, y, fontSize, GRAY); y += 16;
} else {
DrawText("No audio file loaded", 10, y, fontSize, RED);
DrawText("No file loaded", x, y, fontSize, GRAY); y += 16;
}
y = GetScreenHeight() - 280;
DrawText("Controls:", 10, y, fontSize, LIGHTGRAY); y += 20;
DrawText(" O - Open file browser", 10, y, fontSize, GRAY); y += 18;
DrawText(" Drag & drop WAV file", 10, y, fontSize, GRAY); y += 18;
DrawText(" Mouse Wheel - Zoom to cursor", 10, y, fontSize, GRAY); y += 18;
DrawText(" Alt/Middle+Drag - Pan view", 10, y, fontSize, GRAY); y += 18;
DrawText(" LMB Drag - Select time region", 10, y, fontSize, GRAY); y += 18;
DrawText(" Shift+LMB - Select freq range", 10, y, fontSize, GRAY); y += 18;
DrawText(" SPACE - Play selection", 10, y, fontSize, GRAY); y += 18;
DrawText(" 1/2 - FFT size (resolution)", 10, y, fontSize, GRAY); y += 18;
DrawText(" M - Cycle colormap", 10, y, fontSize, GRAY); y += 18;
DrawText(" W/S - Adjust dB floor", 10, y, fontSize, GRAY); y += 18;
DrawText(" G - Toggle grid", 10, y, fontSize, GRAY); y += 18;
DrawText(" R - Reset selections", 10, y, fontSize, GRAY); y += 18;
DrawText(" Home - Full view, End - Zoom in", 10, y, fontSize, GRAY); y += 18;
DrawText(" ESC - Clear selections", 10, y, fontSize, GRAY);
if (needsRegen && app.stftComputed) {
GenerateSpectrogramTexture(&app.stft, &app.spectrogramImage, &app.spectrogramTexture);
}
}
static void DrawSlider(Rectangle bounds, float value)
{
// Background
DrawRectangleRec(bounds, DARKGRAY);
DrawRectangleLinesEx(bounds, 1, GRAY);
y = GetScreenHeight() - 60;
DrawText("Colormap:", GetScreenWidth() - 200, y, fontSize, LIGHTGRAY);
const char* colormapNames[] = { "Grays", "Inferno", "Viridis", "Plasma", "Hot", "Cool" };
DrawText(colormapNames[app.colormap], GetScreenWidth() - 200, y + 18, fontSize, WHITE);
DrawTexturePro(colormapTexture, (Rectangle){ 0, 0, 256, 1 }, (Rectangle){ GetScreenWidth() - 100, y + 15, 80, 15 }, (Vector2){ 0, 0 }, 0.0f, WHITE);
DrawText(TextFormat("dB Floor: %.1f", app.amplitudeFloorDb), GetScreenWidth() - 200, y + 35, fontSize, LIGHTGRAY);
// 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;
}
// ============================================================================
@@ -1004,59 +1117,16 @@ int main(int argc, char* argv[])
}
}
// Other controls
if (IsKeyPressed(KEY_G) && !app.showFileBrowser) {
app.showGrid = !app.showGrid;
}
if (IsKeyPressed(KEY_R) && !app.showFileBrowser) {
app.timeSelectionStart = app.viewStart;
app.timeSelectionEnd = app.viewEnd;
app.freqSelectionStart = 0.0f;
app.freqSelectionEnd = 1.0f;
}
if (IsKeyPressed(KEY_M) && !app.showFileBrowser) {
app.colormap = (ColormapType)((app.colormap + 1) % COLORMAP_COUNT);
GenerateColormapTexture();
if (app.stftComputed) GenerateSpectrogramTexture(&app.stft, &app.spectrogramImage, &app.spectrogramTexture);
app.visibleTextureValid = false;
}
if (IsKeyPressed(KEY_W) && !app.showFileBrowser) {
app.amplitudeFloorDb += 2.0f;
if (app.amplitudeFloorDb > app.amplitudeCeilingDb - 5) app.amplitudeFloorDb = app.amplitudeCeilingDb - 5;
if (app.stftComputed) GenerateSpectrogramTexture(&app.stft, &app.spectrogramImage, &app.spectrogramTexture);
app.visibleTextureValid = false;
}
if (IsKeyPressed(KEY_S) && !app.showFileBrowser) {
app.amplitudeFloorDb -= 2.0f;
if (app.amplitudeFloorDb < -100) app.amplitudeFloorDb = -100;
if (app.stftComputed) GenerateSpectrogramTexture(&app.stft, &app.spectrogramImage, &app.spectrogramTexture);
app.visibleTextureValid = false;
}
// Keyboard shortcuts (SPACE for play, ESC for clear)
if (IsKeyPressed(KEY_SPACE) && !app.showFileBrowser) PlaySelectedRegion();
// FFT size control (1/2 keys)
if (IsKeyPressed(KEY_ONE) && !app.showFileBrowser) {
app.fftSize /= 2;
if (app.fftSize < FFT_SIZE_MIN) app.fftSize = FFT_SIZE_MIN;
else {
app.stftComputed = false;
TraceLog(LOG_INFO, "FFT size: %d", app.fftSize);
}
}
if (IsKeyPressed(KEY_TWO) && !app.showFileBrowser) {
app.fftSize *= 2;
if (app.fftSize > FFT_SIZE_MAX) app.fftSize = FFT_SIZE_MAX;
else {
app.stftComputed = false;
TraceLog(LOG_INFO, "FFT size: %d", app.fftSize);
}
}
if (IsKeyPressed(KEY_ESCAPE)) {
if (app.showFileBrowser) app.showFileBrowser = false;
else {
app.timeSelectionStart = app.viewStart;
app.timeSelectionEnd = app.viewEnd;
if (app.showFileBrowser) {
app.showFileBrowser = false;
} else {
// Clear selections instead of exiting
app.timeSelectionStart = 0.0f;
app.timeSelectionEnd = 1.0f;
app.freqSelectionStart = 0.0f;
app.freqSelectionEnd = 1.0f;
}
@@ -1188,9 +1258,11 @@ int main(int argc, char* argv[])
// Draw file browser on top (if active)
if (app.showFileBrowser) DrawFileBrowser();
// Draw UI info and FPS (always on top)
DrawInfo(viewBounds);
DrawFPS(GetScreenWidth() - 100, 10);
// Draw sidebar controls
DrawSidebar();
// Draw FPS
DrawFPS(GetScreenWidth() - 80, 10);
EndDrawing();
}