Add frequency zoom, scrollbars, dynamic layout that fills window

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-04-11 21:49:26 -07:00
parent c0044c37da
commit b4c23d61a8
+204 -54
View File
@@ -27,7 +27,9 @@
// Configuration // Configuration
// ============================================================================ // ============================================================================
#define FFT_SIZE 128 #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 HOP_RATIO 4 // FFT_SIZE / HOP_SIZE = 4 means 75% overlap
#define MAX_SAMPLE_RATE 48000 #define MAX_SAMPLE_RATE 48000
#define LOUDNESS_FLOOR_DB -80.0f #define LOUDNESS_FLOOR_DB -80.0f
@@ -96,17 +98,23 @@ typedef struct {
bool isFreqSelecting; bool isFreqSelecting;
// Viewport/zoom controls // Viewport/zoom controls
float viewStart; // 0-1, start of visible region float viewStart; // 0-1, start of visible time region
float viewEnd; // 0-1, end of visible region float viewEnd; // 0-1, end of visible time region
float freqViewStart; // 0-1, start of visible frequency region (0 = 0Hz)
float freqViewEnd; // 0-1, end of visible frequency region (1 = Nyquist)
bool isPanning; bool isPanning;
float panStartViewStart; float panStartViewStart;
float panStartViewEnd; float panStartViewEnd;
float panStartFreqViewStart;
float panStartFreqViewEnd;
Vector2 panStartPos; Vector2 panStartPos;
// Cached visible texture // Cached visible texture
Texture2D visibleTexture; Texture2D visibleTexture;
int cachedVisibleStart; int cachedVisibleStart;
int cachedVisibleEnd; int cachedVisibleEnd;
int cachedVisibleStartY;
int cachedVisibleEndY;
bool visibleTextureValid; bool visibleTextureValid;
// Display settings // Display settings
@@ -114,6 +122,7 @@ typedef struct {
float amplitudeCeilingDb; float amplitudeCeilingDb;
ColormapType colormap; ColormapType colormap;
bool showGrid; bool showGrid;
int fftSize; // Current FFT size (128-2048)
// File browser state // File browser state
bool showFileBrowser; bool showFileBrowser;
@@ -969,7 +978,27 @@ static void DrawSidebar(void)
// Title // Title
DrawText("Spectrogram Controls", x, y, 14, WHITE); y += 25; DrawText("Spectrogram Controls", x, y, 14, WHITE); y += 25;
// FFT Size clicker
DrawText(TextFormat("FFT Size: %d (%.1f Hz/bin)", app.fftSize, (float)app.signal.sampleRate / app.fftSize), x, y, fontSize, LIGHTGRAY); y += 18;
Rectangle fftMinus = { x, y, 30, 25 };
Rectangle fftPlus = { x + sidebarWidth - 40, y, 30, 25 };
if (CheckCollisionPointRec(GetMousePosition(), fftMinus) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
int newFFT = app.fftSize / 2;
if (newFFT >= FFT_SIZE_MIN) { app.fftSize = newFFT; app.stftComputed = false; needsRegen = true; }
}
if (CheckCollisionPointRec(GetMousePosition(), fftPlus) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
int newFFT = app.fftSize * 2;
if (newFFT <= FFT_SIZE_MAX) { app.fftSize = newFFT; app.stftComputed = false; needsRegen = true; }
}
DrawRectangleRec(fftMinus, (Color){ 50, 50, 60, 255 });
DrawRectangleLinesEx(fftMinus, 1, GRAY);
DrawText("-", fftMinus.x + 12, fftMinus.y + 5, 16, WHITE);
DrawRectangleRec(fftPlus, (Color){ 50, 50, 60, 255 });
DrawRectangleLinesEx(fftPlus, 1, GRAY);
DrawText("+", fftPlus.x + 10, fftPlus.y + 5, 16, WHITE);
y += 30;
// dB Floor slider // dB Floor slider
DrawText(TextFormat("dB Floor: %.1f", app.amplitudeFloorDb), x, y, fontSize, LIGHTGRAY); y += 18; DrawText(TextFormat("dB Floor: %.1f", app.amplitudeFloorDb), x, y, fontSize, LIGHTGRAY); y += 18;
Rectangle dbSlider = { x, y, sidebarWidth - 10, 20 }; Rectangle dbSlider = { x, y, sidebarWidth - 10, 20 };
@@ -980,7 +1009,7 @@ static void DrawSidebar(void)
needsRegen = true; // Trigger immediate redraw needsRegen = true; // Trigger immediate redraw
} }
y += 25; y += 25;
// Colormap dropdown // Colormap dropdown
DrawText("Colormap:", x, y, fontSize, LIGHTGRAY); y += 18; DrawText("Colormap:", x, y, fontSize, LIGHTGRAY); y += 18;
const char* colormapNames[] = { "Grays", "Inferno", "Viridis", "Plasma", "Hot", "Cool" }; const char* colormapNames[] = { "Grays", "Inferno", "Viridis", "Plasma", "Hot", "Cool" };
@@ -993,11 +1022,11 @@ static void DrawSidebar(void)
DrawRectangleRec(cmapButton, (Color){ 50, 50, 60, 255 }); DrawRectangleRec(cmapButton, (Color){ 50, 50, 60, 255 });
DrawRectangleLinesEx(cmapButton, 1, GRAY); DrawRectangleLinesEx(cmapButton, 1, GRAY);
DrawText(colormapNames[app.colormap], cmapButton.x + 10, cmapButton.y + 6, fontSize, WHITE); DrawText(colormapNames[app.colormap], cmapButton.x + 10, cmapButton.y + 6, fontSize, WHITE);
DrawTexturePro(colormapTexture, (Rectangle){ 0, 0, 256, 1 }, DrawTexturePro(colormapTexture, (Rectangle){ 0, 0, 256, 1 },
(Rectangle){ cmapButton.x + cmapButton.width - 60, cmapButton.y + 5, 50, 15 }, (Rectangle){ cmapButton.x + cmapButton.width - 60, cmapButton.y + 5, 50, 15 },
(Vector2){ 0, 0 }, 0.0f, WHITE); (Vector2){ 0, 0 }, 0.0f, WHITE);
y += 30; y += 30;
// Grid toggle // Grid toggle
Rectangle gridCheck = { x, y, 18, 18 }; Rectangle gridCheck = { x, y, 18, 18 };
if (CheckCollisionPointRec(GetMousePosition(), gridCheck) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { if (CheckCollisionPointRec(GetMousePosition(), gridCheck) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
@@ -1065,7 +1094,7 @@ static void DrawSidebar(void)
if (app.loaded) { if (app.loaded) {
DrawText(TextFormat("Sample Rate: %d Hz", app.signal.sampleRate), x, y, fontSize, GRAY); y += 16; 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("Duration: %.2f sec", app.signal.duration), x, y, fontSize, GRAY); y += 16;
DrawText(TextFormat("FFT: %d, %.1f Hz/bin (synchrosqueezed)", FFT_SIZE, (float)app.signal.sampleRate / FFT_SIZE), 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 { } else {
DrawText("No file loaded", x, y, fontSize, GRAY); y += 16; DrawText("No file loaded", x, y, fontSize, GRAY); y += 16;
} }
@@ -1121,6 +1150,7 @@ int main(int argc, char* argv[])
app.timeSelectionStart = 0.0f; app.timeSelectionEnd = 1.0f; app.timeSelectionStart = 0.0f; app.timeSelectionEnd = 1.0f;
app.freqSelectionStart = 0.0f; app.freqSelectionEnd = 1.0f; app.freqSelectionStart = 0.0f; app.freqSelectionEnd = 1.0f;
app.viewStart = 0.0f; app.viewEnd = 1.0f; app.viewStart = 0.0f; app.viewEnd = 1.0f;
app.freqViewStart = 0.0f; app.freqViewEnd = 1.0f;
app.showGrid = true; app.showGrid = true;
app.colormap = COLORMAP_INFERNO; app.colormap = COLORMAP_INFERNO;
app.amplitudeFloorDb = -60.0f; app.amplitudeFloorDb = -60.0f;
@@ -1130,7 +1160,10 @@ int main(int argc, char* argv[])
app.visibleTexture = (Texture2D){ 0 }; app.visibleTexture = (Texture2D){ 0 };
app.cachedVisibleStart = -1; app.cachedVisibleStart = -1;
app.cachedVisibleEnd = -1; app.cachedVisibleEnd = -1;
app.cachedVisibleStartY = -1;
app.cachedVisibleEndY = -1;
app.visibleTextureValid = false; app.visibleTextureValid = false;
app.fftSize = FFT_SIZE_DEFAULT;
app.isPlaying = false; app.isPlaying = false;
app.playbackFinished = false; app.playbackFinished = false;
@@ -1197,64 +1230,91 @@ int main(int argc, char* argv[])
// View controls // View controls
if (app.loaded && !app.showFileBrowser) { if (app.loaded && !app.showFileBrowser) {
// Spectrogram area is to the right of sidebar (320px) // Spectrogram area fills remaining window space
Rectangle viewBounds = { 330, 50, GetScreenWidth() - 380, GetScreenHeight() - 150 }; // Leave room for scrollbars (20px) and axis labels
float sidebarWidth = 320;
float scrollbarSize = 18;
float labelMarginLeft = 65;
float labelMarginBottom = 35;
float topMargin = 50;
Rectangle viewBounds = {
sidebarWidth + labelMarginLeft,
topMargin,
GetScreenWidth() - sidebarWidth - labelMarginLeft - scrollbarSize - 10,
GetScreenHeight() - topMargin - labelMarginBottom - scrollbarSize - 10
};
// Zoom with mouse wheel (zooms to cursor position) // Zoom with mouse wheel (zooms both time and frequency to maintain aspect ratio)
if (GetMousePosition().x > 320 && CheckCollisionPointRec(GetMousePosition(), viewBounds)) { if (GetMousePosition().x > 380 && CheckCollisionPointRec(GetMousePosition(), viewBounds)) {
int wheel = GetMouseWheelMove(); int wheel = GetMouseWheelMove();
if (wheel != 0) { if (wheel != 0) {
// Calculate mouse position as normalized time (0-1)
float mouseT = (GetMousePosition().x - viewBounds.x) / viewBounds.width;
mouseT = Clamp(mouseT + app.viewStart, 0.0f, 1.0f);
float viewWidth = app.viewEnd - app.viewStart;
float zoomFactor = (wheel > 0) ? 0.8f : 1.2f; float zoomFactor = (wheel > 0) ? 0.8f : 1.2f;
// --- Time axis zoom (around cursor X) ---
float mouseT = (GetMousePosition().x - viewBounds.x) / viewBounds.width;
mouseT = app.viewStart + mouseT * (app.viewEnd - app.viewStart);
float viewWidth = app.viewEnd - app.viewStart;
float newWidth = viewWidth * zoomFactor; float newWidth = viewWidth * zoomFactor;
if (newWidth < 0.02f) newWidth = 0.02f; if (newWidth < 0.02f) newWidth = 0.02f;
if (newWidth > 1.0f) newWidth = 1.0f; if (newWidth > 1.0f) newWidth = 1.0f;
// Zoom around cursor position
float leftOfMouse = mouseT - app.viewStart; float leftOfMouse = mouseT - app.viewStart;
float rightOfMouse = app.viewEnd - mouseT; float rightOfMouse = app.viewEnd - mouseT;
app.viewStart = mouseT - leftOfMouse * (newWidth / viewWidth);
float newLeftOfMouse = leftOfMouse * (newWidth / viewWidth); app.viewEnd = mouseT + rightOfMouse * (newWidth / viewWidth);
float newRightOfMouse = rightOfMouse * (newWidth / viewWidth);
app.viewStart = mouseT - newLeftOfMouse;
app.viewEnd = mouseT + newRightOfMouse;
// Clamp to valid range
if (app.viewStart < 0) { app.viewStart = 0; app.viewEnd = newWidth; } if (app.viewStart < 0) { app.viewStart = 0; app.viewEnd = newWidth; }
if (app.viewEnd > 1) { app.viewEnd = 1; app.viewStart = 1 - newWidth; } if (app.viewEnd > 1) { app.viewEnd = 1; app.viewStart = 1 - newWidth; }
// --- Frequency axis zoom (around cursor Y) ---
float mouseF = 1.0f - (GetMousePosition().y - viewBounds.y) / viewBounds.height;
mouseF = app.freqViewStart + mouseF * (app.freqViewEnd - app.freqViewStart);
float freqWidth = app.freqViewEnd - app.freqViewStart;
float newFreqWidth = freqWidth * zoomFactor;
if (newFreqWidth < 0.02f) newFreqWidth = 0.02f;
if (newFreqWidth > 1.0f) newFreqWidth = 1.0f;
float belowMouse = mouseF - app.freqViewStart;
float aboveMouse = app.freqViewEnd - mouseF;
app.freqViewStart = mouseF - belowMouse * (newFreqWidth / freqWidth);
app.freqViewEnd = mouseF + aboveMouse * (newFreqWidth / freqWidth);
if (app.freqViewStart < 0) { app.freqViewStart = 0; app.freqViewEnd = newFreqWidth; }
if (app.freqViewEnd > 1) { app.freqViewEnd = 1; app.freqViewStart = 1 - newFreqWidth; }
// Invalidate texture cache // Invalidate texture cache
app.visibleTextureValid = false; app.visibleTextureValid = false;
} }
} }
// Pan with Alt+drag or middle mouse button // Pan with Alt+drag or middle mouse button (pans both axes)
bool canPan = IsKeyDown(KEY_LEFT_ALT) || IsKeyDown(KEY_RIGHT_ALT) || IsMouseButtonDown(MOUSE_BUTTON_MIDDLE); bool canPan = IsKeyDown(KEY_LEFT_ALT) || IsKeyDown(KEY_RIGHT_ALT) || IsMouseButtonDown(MOUSE_BUTTON_MIDDLE);
if (canPan && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { if (canPan && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
app.isPanning = true; app.isPanning = true;
app.panStartPos = GetMousePosition(); app.panStartPos = GetMousePosition();
app.panStartViewStart = app.viewStart; app.panStartViewStart = app.viewStart;
app.panStartViewEnd = app.viewEnd; app.panStartViewEnd = app.viewEnd;
app.panStartFreqViewStart = app.freqViewStart;
app.panStartFreqViewEnd = app.freqViewEnd;
} }
if (app.isPanning && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) { if (app.isPanning && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) {
float dx = (GetMousePosition().x - app.panStartPos.x) / viewBounds.width; float dx = (GetMousePosition().x - app.panStartPos.x) / viewBounds.width;
float dy = (GetMousePosition().y - app.panStartPos.y) / viewBounds.height;
float viewWidth = app.panStartViewEnd - app.panStartViewStart; float viewWidth = app.panStartViewEnd - app.panStartViewStart;
float freqWidth = app.panStartFreqViewEnd - app.panStartFreqViewStart;
app.viewStart = app.panStartViewStart - dx * viewWidth; app.viewStart = app.panStartViewStart - dx * viewWidth;
app.viewEnd = app.panStartViewEnd - dx * viewWidth; app.viewEnd = app.panStartViewEnd - dx * viewWidth;
if (app.viewStart < 0) { app.viewStart = 0; app.viewEnd = viewWidth; } if (app.viewStart < 0) { app.viewStart = 0; app.viewEnd = viewWidth; }
if (app.viewEnd > 1) { app.viewEnd = 1; app.viewStart = 1 - viewWidth; } if (app.viewEnd > 1) { app.viewEnd = 1; app.viewStart = 1 - viewWidth; }
app.freqViewStart = app.panStartFreqViewStart + dy * freqWidth;
app.freqViewEnd = app.panStartFreqViewEnd + dy * freqWidth;
if (app.freqViewStart < 0) { app.freqViewStart = 0; app.freqViewEnd = freqWidth; }
if (app.freqViewEnd > 1) { app.freqViewEnd = 1; app.freqViewStart = 1 - freqWidth; }
app.visibleTextureValid = false; app.visibleTextureValid = false;
} }
if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) app.isPanning = false; if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) app.isPanning = false;
// Home/End keys // Home/End keys
if (IsKeyPressed(KEY_HOME)) { if (IsKeyPressed(KEY_HOME)) {
app.viewStart = 0.0f; app.viewEnd = 1.0f; app.viewStart = 0.0f; app.viewEnd = 1.0f;
app.freqViewStart = 0.0f; app.freqViewEnd = 1.0f;
app.visibleTextureValid = false; app.visibleTextureValid = false;
} }
if (IsKeyPressed(KEY_END)) { if (IsKeyPressed(KEY_END)) {
@@ -1297,10 +1357,21 @@ int main(int argc, char* argv[])
} }
// Selection (only when mouse is in spectrogram area, not sidebar) // Selection (only when mouse is in spectrogram area, not sidebar)
Rectangle selBounds = { 330, 50, GetScreenWidth() - 380, GetScreenHeight() - 150 }; float selSidebarWidth = 320;
float selScrollbarSize = 18;
float selLabelMarginLeft = 65;
float selLabelMarginBottom = 35;
float selTopMargin = 50;
Rectangle selBounds = {
selSidebarWidth + selLabelMarginLeft,
selTopMargin,
GetScreenWidth() - selSidebarWidth - selLabelMarginLeft - selScrollbarSize - 10,
GetScreenHeight() - selTopMargin - selLabelMarginBottom - selScrollbarSize - 10
};
Vector2 mousePos = GetMousePosition(); Vector2 mousePos = GetMousePosition();
if (app.loaded && !app.showFileBrowser && mousePos.x > 320 && CheckCollisionPointRec(mousePos, selBounds)) { if (app.loaded && !app.showFileBrowser && mousePos.x > 380 && CheckCollisionPointRec(mousePos, selBounds)) {
bool shiftHeld = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT); bool shiftHeld = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT);
if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && !app.isPanning) { if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && !app.isPanning) {
@@ -1348,7 +1419,7 @@ int main(int argc, char* argv[])
if (app.loaded && !app.stftComputed) { if (app.loaded && !app.stftComputed) {
TraceLog(LOG_INFO, "Computing STFT..."); TraceLog(LOG_INFO, "Computing STFT...");
double startTime = GetTime(); double startTime = GetTime();
ComputeSTFT(&app.signal, &app.stft, FFT_SIZE); ComputeSTFT(&app.signal, &app.stft, app.fftSize);
TraceLog(LOG_INFO, "STFT computed in %.2f sec (%d segments)", GetTime() - startTime, app.stft.numSegments); TraceLog(LOG_INFO, "STFT computed in %.2f sec (%d segments)", GetTime() - startTime, app.stft.numSegments);
GenerateSpectrogramTexture(&app.stft, &app.spectrogramImage, &app.spectrogramTexture); GenerateSpectrogramTexture(&app.stft, &app.spectrogramImage, &app.spectrogramTexture);
app.stftComputed = true; app.stftComputed = true;
@@ -1358,61 +1429,140 @@ int main(int argc, char* argv[])
BeginDrawing(); BeginDrawing();
ClearBackground((Color){ 30, 30, 30, 255 }); ClearBackground((Color){ 30, 30, 30, 255 });
// Spectrogram area is to the right of sidebar (320px) // Spectrogram area fills remaining window space
Rectangle viewBounds = { 330, 50, GetScreenWidth() - 380, GetScreenHeight() - 150 }; // Sidebar: 320px wide, spectrogram takes the rest
// Leave room for scrollbars (20px) and axis labels
float sidebarWidth = 320;
float scrollbarSize = 18;
float labelMarginLeft = 65;
float labelMarginBottom = 35;
float topMargin = 50;
Rectangle viewBounds = {
sidebarWidth + labelMarginLeft,
topMargin,
GetScreenWidth() - sidebarWidth - labelMarginLeft - scrollbarSize - 10,
GetScreenHeight() - topMargin - labelMarginBottom - scrollbarSize - 10
};
Rectangle hScrollbar = { viewBounds.x, viewBounds.y + viewBounds.height + 10, viewBounds.width, scrollbarSize };
Rectangle vScrollbar = { viewBounds.x + viewBounds.width + 10, viewBounds.y, scrollbarSize, viewBounds.height };
// Draw sidebar first (on top left) // Draw sidebar first (on top left)
DrawSidebar(); DrawSidebar();
// Draw spectrogram (background, in its own area) // Draw spectrogram (background, in its own area)
if (app.loaded && app.stftComputed) { if (app.loaded && app.stftComputed) {
int imgWidth = app.spectrogramImage.width;
int imgHeight = app.spectrogramImage.height;
// Calculate visible region (time and frequency)
int visibleStartX = (int)(app.viewStart * imgWidth);
int visibleEndX = (int)(app.viewEnd * imgWidth);
int visibleWidth = visibleEndX - visibleStartX;
// Calculate visible region // Frequency: 0 = bottom of image (bin 0), 1 = top of image (bin max)
int visibleStart = (int)(app.viewStart * app.spectrogramImage.width); int visibleStartY = (int)((1.0f - app.freqViewEnd) * imgHeight);
int visibleEnd = (int)(app.viewEnd * app.spectrogramImage.width); int visibleEndY = (int)((1.0f - app.freqViewStart) * imgHeight);
int visibleWidth = visibleEnd - visibleStart; int visibleHeight = visibleEndY - visibleStartY;
// Invalidate cache if view changed or texture not valid // Invalidate cache if view changed or texture not valid
bool cacheInvalid = !app.visibleTextureValid || bool cacheInvalid = !app.visibleTextureValid ||
visibleStart != app.cachedVisibleStart || visibleStartX != app.cachedVisibleStart ||
visibleEnd != app.cachedVisibleEnd || visibleEndX != app.cachedVisibleEnd ||
visibleWidth <= 0; visibleStartY != app.cachedVisibleStartY ||
visibleEndY != app.cachedVisibleEndY ||
if (cacheInvalid && visibleWidth > 0 && visibleStart >= 0) { visibleWidth <= 0 || visibleHeight <= 0;
if (cacheInvalid && visibleWidth > 0 && visibleStartX >= 0 && visibleHeight > 0 && visibleStartY >= 0) {
// Free old texture if exists // Free old texture if exists
if (app.visibleTexture.id != 0) UnloadTexture(app.visibleTexture); if (app.visibleTexture.id != 0) UnloadTexture(app.visibleTexture);
// Create a sub-image for the visible region // Create a sub-image for the visible region
Image visibleImage = GenImageColor(visibleWidth, app.spectrogramImage.height, BLACK); Image visibleImage = GenImageColor(visibleWidth, visibleHeight, BLACK);
Color* srcPixels = (Color*)app.spectrogramImage.data; Color* srcPixels = (Color*)app.spectrogramImage.data;
Color* dstPixels = (Color*)visibleImage.data; Color* dstPixels = (Color*)visibleImage.data;
for (int y = 0; y < app.spectrogramImage.height; y++) { for (int y = 0; y < visibleHeight; y++) {
for (int x = 0; x < visibleWidth; x++) { for (int x = 0; x < visibleWidth; x++) {
dstPixels[y * visibleWidth + x] = srcPixels[y * app.spectrogramImage.width + visibleStart + x]; dstPixels[y * visibleWidth + x] = srcPixels[(visibleStartY + y) * imgWidth + visibleStartX + x];
} }
} }
app.visibleTexture = LoadTextureFromImage(visibleImage); app.visibleTexture = LoadTextureFromImage(visibleImage);
UnloadImage(visibleImage); UnloadImage(visibleImage);
app.cachedVisibleStart = visibleStart; app.cachedVisibleStart = visibleStartX;
app.cachedVisibleEnd = visibleEnd; app.cachedVisibleEnd = visibleEndX;
app.cachedVisibleStartY = visibleStartY;
app.cachedVisibleEndY = visibleEndY;
app.visibleTextureValid = true; app.visibleTextureValid = true;
} }
// Draw cached texture // Draw cached texture
if (app.visibleTextureValid && app.visibleTexture.id != 0) { if (app.visibleTextureValid && app.visibleTexture.id != 0) {
DrawTexturePro(app.visibleTexture, DrawTexturePro(app.visibleTexture,
(Rectangle){ 0, 0, visibleWidth, app.spectrogramImage.height }, (Rectangle){ 0, 0, visibleWidth, visibleHeight },
viewBounds, (Vector2){ 0, 0 }, 0.0f, WHITE); viewBounds, (Vector2){ 0, 0 }, 0.0f, WHITE);
} }
// Draw scrollbars
// Horizontal scrollbar (time)
DrawRectangleRec(hScrollbar, DARKGRAY);
float hThumbWidth = (app.viewEnd - app.viewStart) * hScrollbar.width;
float hThumbX = hScrollbar.x + app.viewStart * hScrollbar.width;
if (hThumbWidth < 10) hThumbWidth = 10;
DrawRectangle(hThumbX, hScrollbar.y, hThumbWidth, hScrollbar.height, GRAY);
// Vertical scrollbar (frequency)
DrawRectangleRec(vScrollbar, DARKGRAY);
float vThumbHeight = (app.freqViewEnd - app.freqViewStart) * vScrollbar.height;
float vThumbY = vScrollbar.y + (1.0f - app.freqViewEnd) * vScrollbar.height;
if (vThumbHeight < 10) vThumbHeight = 10;
DrawRectangle(vScrollbar.x, vThumbY, vScrollbar.width, vThumbHeight, GRAY);
// Handle scrollbar dragging
static bool draggingH = false, draggingV = false;
static Vector2 dragStartPos;
static float dragStartViewStart, dragStartFreqViewStart;
if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && CheckCollisionPointRec(GetMousePosition(), hScrollbar)) {
draggingH = true;
dragStartPos = GetMousePosition();
dragStartViewStart = app.viewStart;
}
if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && CheckCollisionPointRec(GetMousePosition(), vScrollbar)) {
draggingV = true;
dragStartPos = GetMousePosition();
dragStartFreqViewStart = app.freqViewStart;
}
if (draggingH && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) {
float dx = (GetMousePosition().x - dragStartPos.x) / hScrollbar.width;
float viewWidth = app.viewEnd - app.viewStart;
app.viewStart = dragStartViewStart + dx;
app.viewEnd = app.viewStart + viewWidth;
if (app.viewStart < 0) { app.viewStart = 0; app.viewEnd = viewWidth; }
if (app.viewEnd > 1) { app.viewEnd = 1; app.viewStart = 1 - viewWidth; }
app.visibleTextureValid = false;
}
if (draggingV && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) {
float dy = (GetMousePosition().y - dragStartPos.y) / vScrollbar.height;
float freqWidth = app.freqViewEnd - app.freqViewStart;
app.freqViewStart = dragStartFreqViewStart - dy;
app.freqViewEnd = app.freqViewStart + freqWidth;
if (app.freqViewStart < 0) { app.freqViewStart = 0; app.freqViewEnd = freqWidth; }
if (app.freqViewEnd > 1) { app.freqViewEnd = 1; app.freqViewStart = 1 - freqWidth; }
app.visibleTextureValid = false;
}
if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { draggingH = false; draggingV = false; }
if (app.showGrid) DrawSpectrogramGrid(viewBounds, 10, 8, Fade(GRAY, 0.3f)); if (app.showGrid) DrawSpectrogramGrid(viewBounds, 10, 8, Fade(GRAY, 0.3f));
DrawSelection(viewBounds); DrawSelection(viewBounds);
DrawLabels(viewBounds); DrawLabels(viewBounds);
DrawText("Spectrogram", viewBounds.x, viewBounds.y - 30, 20, LIGHTGRAY); DrawText("Spectrogram", viewBounds.x, viewBounds.y - 30, 20, LIGHTGRAY);
DrawText(TextFormat("Frequency (Hz) - Max: %d", app.signal.sampleRate / 2), float maxFreq = (float)app.signal.sampleRate / 2.0f;
float freqMin = app.freqViewStart * maxFreq;
float freqMax = app.freqViewEnd * maxFreq;
DrawText(TextFormat("Freq: %.0f-%.0f Hz", freqMin, freqMax),
viewBounds.x - 50, viewBounds.y + viewBounds.height / 2 - 10, 14, Fade(LIGHTGRAY, 0.7f)); viewBounds.x - 50, viewBounds.y + viewBounds.height / 2 - 10, 14, Fade(LIGHTGRAY, 0.7f));
} else if (!app.showFileBrowser) { } else if (!app.showFileBrowser) {
const char* msg1 = "Press 'O' or click 'Open File Browser' to load a WAV"; const char* msg1 = "Press 'O' or click 'Open File Browser' to load a WAV";