diff --git a/src/spectrogram.c b/src/spectrogram.c index 7a84bbe..c5ab3a0 100644 --- a/src/spectrogram.c +++ b/src/spectrogram.c @@ -101,6 +101,12 @@ typedef struct { float panStartViewStart; float panStartViewEnd; Vector2 panStartPos; + + // Cached visible texture + Texture2D visibleTexture; + int cachedVisibleStart; + int cachedVisibleEnd; + bool visibleTextureValid; // Display settings float amplitudeFloorDb; @@ -578,76 +584,96 @@ static void LoadSelectedFile(void) app.freqSelectionStart = 0.0f; app.freqSelectionEnd = 1.0f; app.showFileBrowser = false; + // 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); } } static void DrawFileBrowser(void) { + // Draw semi-transparent overlay first DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight(), Fade(BLACK, 0.85f)); - + float bw = 650, bh = 550; 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 }, 2, GRAY); DrawRectangle(bx, by, bw, 35, (Color){ 60, 60, 75, 255 }); DrawText("File Browser - Select WAV File", bx + 15, by + 10, 18, WHITE); - + // Path bar DrawRectangle(bx + 15, by + 45, bw - 110, 28, (Color){ 30, 30, 40, 255 }); DrawRectangleLinesEx((Rectangle){ bx + 15, by + 45, bw - 110, 28 }, 1, 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); DrawText(displayPath, bx + 20, by + 51, 14, LIGHTGRAY); - + // Up button Rectangle upBtn = { bx + bw - 85, by + 45, 70, 28 }; if (CheckCollisionPointRec(GetMousePosition(), upBtn)) DrawRectangleRec(upBtn, (Color){ 80, 80, 90, 255 }); DrawText("UP (..)", upBtn.x + 10, upBtn.y + 7, 14, WHITE); - + // File list float lx = bx + 15, ly = by + 82, lw = bw - 30, lh = bh - 150; DrawRectangle(lx, ly, lw, lh, (Color){ 25, 25, 35, 255 }); DrawRectangleLinesEx((Rectangle){ lx, ly, lw, lh }, 1, GRAY); - + + // Handle empty directory int visibleItems = (int)(lh / 26); - if (app.browserFileCount > visibleItems) { - float sh = (float)visibleItems / app.browserFileCount * lh; - float sy = ly + (float)app.browserScroll / (app.browserFileCount - visibleItems) * (lh - sh); - DrawRectangle(lx + lw - 10, sy, 8, sh, GRAY); + if (visibleItems < 1) visibleItems = 1; + + if (app.browserFileCount <= 0 || !app.browserFiles) { + DrawText("(No WAV files in directory)", lx + 20, ly + lh/2 - 10, 14, GRAY); + } else { + if (app.browserFileCount > visibleItems) { + float sh = (float)visibleItems / app.browserFileCount * lh; + if (sh < 10) sh = 10; + float sy = ly + (float)app.browserScroll / (app.browserFileCount - visibleItems) * (lh - sh); + DrawRectangle(lx + lw - 10, sy, 8, sh, GRAY); + } + + int startItem = app.browserScroll; + int endItem = startItem + visibleItems + 1; + if (endItem > app.browserFileCount) endItem = app.browserFileCount; + + for (int i = startItem; i < endItem; i++) { + if (i < 0 || i >= app.browserFileCount || !app.browserFiles[i] || !app.browserIsDir) continue; + + float iy = ly + (i - startItem) * 26 + 2; + bool hovered = CheckCollisionPointRec((Vector2){ GetMouseX(), GetMouseY() }, (Rectangle){ lx + 2, iy, lw - 14, 24 }); + + if (i == app.browserSelected) DrawRectangle(lx + 2, iy, lw - 14, 24, (Color){ 50, 70, 120, 180 }); + else if (hovered) DrawRectangle(lx + 2, iy, lw - 14, 24, (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 }; + DrawText(icon, lx + 8, iy + 5, 13, iconCol); + DrawText(app.browserFiles[i], lx + 55, iy + 5, 14, WHITE); + } } - - int startItem = app.browserScroll; - int endItem = startItem + visibleItems + 1; - if (endItem > app.browserFileCount) endItem = app.browserFileCount; - - for (int i = startItem; i < endItem; i++) { - float iy = ly + (i - startItem) * 26 + 2; - bool hovered = CheckCollisionPointRec((Vector2){ GetMouseX(), GetMouseY() }, (Rectangle){ lx + 2, iy, lw - 14, 24 }); - - if (i == app.browserSelected) DrawRectangle(lx + 2, iy, lw - 14, 24, (Color){ 50, 70, 120, 180 }); - else if (hovered) DrawRectangle(lx + 2, iy, lw - 14, 24, (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 }; - DrawText(icon, lx + 8, iy + 5, 13, iconCol); - DrawText(app.browserFiles[i], lx + 55, iy + 5, 14, WHITE); - } - - if (CheckCollisionPointRec(GetMousePosition(), (Rectangle){ lx, ly, lw - 10, lh })) { + + // Scroll with mouse wheel + if (CheckCollisionPointRec(GetMousePosition(), (Rectangle){ lx, ly, lw - 10, lh }) && app.browserFileCount > 0) { int wheel = GetMouseWheelMove(); if (wheel > 0) app.browserScroll--; if (wheel < 0) app.browserScroll++; if (app.browserScroll < 0) app.browserScroll = 0; - if (app.browserScroll > app.browserFileCount - visibleItems) app.browserScroll = app.browserFileCount - visibleItems; + 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 - 10, lh }) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { + + if (CheckCollisionPointRec(GetMousePosition(), (Rectangle){ lx, ly, lw - 10, lh }) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && app.browserFileCount > 0) { int clicked = app.browserScroll + (int)((GetMouseY() - ly) / 26); if (clicked >= 0 && clicked < app.browserFileCount) { app.browserSelected = clicked; @@ -656,26 +682,26 @@ static void DrawFileBrowser(void) lastClick = GetTime(); } } - + // Buttons float btnY = by + bh - 50; Rectangle openBtn = { bx + bw - 160, btnY, 140, 38 }; Rectangle cancelBtn = { bx + 15, btnY, 110, 38 }; - + if (CheckCollisionPointRec(GetMousePosition(), openBtn)) DrawRectangleRec(openBtn, (Color){ 80, 80, 90, 255 }); DrawRectangleLinesEx(openBtn, 1, WHITE); DrawText("OPEN (Enter)", openBtn.x + 25, openBtn.y + 12, 16, WHITE); - + DrawRectangleRec(cancelBtn, (Color){ 100, 40, 40, 255 }); DrawText("ESC Cancel", cancelBtn.x + 18, cancelBtn.y + 12, 16, WHITE); - - if (IsKeyPressed(KEY_ENTER) && app.browserSelected >= 0) LoadSelectedFile(); + + if (IsKeyPressed(KEY_ENTER) && app.browserSelected >= 0 && app.browserFileCount > 0) LoadSelectedFile(); if (IsKeyPressed(KEY_ESCAPE)) app.showFileBrowser = false; - if (IsKeyPressed(KEY_UP) && app.browserSelected > 0) { + 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) { + 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; } @@ -832,6 +858,10 @@ int main(int argc, char* argv[]) app.amplitudeCeilingDb = 0.0f; app.showFileBrowser = false; app.isBrowsing = false; + app.visibleTexture = (Texture2D){ 0 }; + app.cachedVisibleStart = -1; + app.cachedVisibleEnd = -1; + app.visibleTextureValid = false; GenerateColormapTexture(); ScanDirectory(GetWorkingDirectory()); @@ -864,6 +894,10 @@ int main(int argc, char* argv[]) app.loaded = true; app.stftComputed = false; app.viewStart = 0.0f; app.viewEnd = 1.0f; + // Invalidate visible texture cache + if (app.visibleTexture.id != 0) UnloadTexture(app.visibleTexture); + app.visibleTexture = (Texture2D){ 0 }; + app.visibleTextureValid = false; } } } @@ -965,30 +999,30 @@ int main(int argc, char* argv[]) } // Selection - Rectangle bounds = { 100, 50, GetScreenWidth() - 150, GetScreenHeight() - 150 }; + Rectangle selBounds = { 100, 50, GetScreenWidth() - 150, GetScreenHeight() - 150 }; Vector2 mousePos = GetMousePosition(); - - if (app.loaded && !app.showFileBrowser && CheckCollisionPointRec(mousePos, bounds)) { + + if (app.loaded && !app.showFileBrowser && CheckCollisionPointRec(mousePos, selBounds)) { bool shiftHeld = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT); - + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON) && !app.isPanning) { if (shiftHeld) { app.isFreqSelecting = true; - float t = 1.0f - (mousePos.y - bounds.y) / bounds.height; + float t = 1.0f - (mousePos.y - selBounds.y) / selBounds.height; app.freqSelectionStart = Clamp(t, 0.0f, 1.0f); app.freqSelectionEnd = app.freqSelectionStart; } else if (!IsKeyDown(KEY_LEFT_ALT) && !IsKeyDown(KEY_RIGHT_ALT)) { app.isTimeSelecting = true; - app.timeSelectionStart = Clamp((mousePos.x - bounds.x) / bounds.width, 0.0f, 1.0f); + app.timeSelectionStart = Clamp((mousePos.x - selBounds.x) / selBounds.width, 0.0f, 1.0f); app.timeSelectionEnd = app.timeSelectionStart; } } - + if (app.isTimeSelecting && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) { - app.timeSelectionEnd = Clamp((mousePos.x - bounds.x) / bounds.width, 0.0f, 1.0f); + app.timeSelectionEnd = Clamp((mousePos.x - selBounds.x) / selBounds.width, 0.0f, 1.0f); } if (app.isFreqSelecting && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) { - float t = 1.0f - (mousePos.y - bounds.y) / bounds.height; + float t = 1.0f - (mousePos.y - selBounds.y) / selBounds.height; app.freqSelectionEnd = Clamp(t, 0.0f, 1.0f); } @@ -1025,42 +1059,59 @@ int main(int argc, char* argv[]) // Rendering BeginDrawing(); ClearBackground((Color){ 30, 30, 30, 255 }); - - if (app.showFileBrowser) DrawFileBrowser(); - + + Rectangle viewBounds = { 100, 50, GetScreenWidth() - 150, GetScreenHeight() - 150 }; + + // Draw spectrogram first (background) if (app.loaded && app.stftComputed) { + // Calculate visible region int visibleStart = (int)(app.viewStart * app.spectrogramImage.width); int visibleEnd = (int)(app.viewEnd * app.spectrogramImage.width); int visibleWidth = visibleEnd - visibleStart; + + // Invalidate cache if view changed or texture not valid + bool cacheInvalid = !app.visibleTextureValid || + visibleStart != app.cachedVisibleStart || + visibleEnd != app.cachedVisibleEnd || + visibleWidth <= 0; - if (visibleWidth > 0 && visibleStart >= 0) { + if (cacheInvalid && visibleWidth > 0 && visibleStart >= 0) { + // Free old texture if exists + if (app.visibleTexture.id != 0) UnloadTexture(app.visibleTexture); + // Create a sub-image for the visible region Image visibleImage = GenImageColor(visibleWidth, app.spectrogramImage.height, BLACK); Color* srcPixels = (Color*)app.spectrogramImage.data; Color* dstPixels = (Color*)visibleImage.data; - + for (int y = 0; y < app.spectrogramImage.height; y++) { for (int x = 0; x < visibleWidth; x++) { dstPixels[y * visibleWidth + x] = srcPixels[y * app.spectrogramImage.width + visibleStart + x]; } } - - Texture2D visibleTexture = LoadTextureFromImage(visibleImage); - DrawTexturePro(visibleTexture, - (Rectangle){ 0, 0, visibleWidth, app.spectrogramImage.height }, - bounds, (Vector2){ 0, 0 }, 0.0f, WHITE); - - UnloadTexture(visibleTexture); + + app.visibleTexture = LoadTextureFromImage(visibleImage); UnloadImage(visibleImage); + + app.cachedVisibleStart = visibleStart; + app.cachedVisibleEnd = visibleEnd; + app.visibleTextureValid = true; } - - if (app.showGrid) DrawSpectrogramGrid(bounds, 10, 8, Fade(GRAY, 0.3f)); - DrawSelection(bounds); - DrawLabels(bounds); - DrawText("Spectrogram", bounds.x, bounds.y - 30, 20, LIGHTGRAY); + + // Draw cached texture + if (app.visibleTextureValid && app.visibleTexture.id != 0) { + DrawTexturePro(app.visibleTexture, + (Rectangle){ 0, 0, visibleWidth, app.spectrogramImage.height }, + viewBounds, (Vector2){ 0, 0 }, 0.0f, WHITE); + } + + if (app.showGrid) DrawSpectrogramGrid(viewBounds, 10, 8, Fade(GRAY, 0.3f)); + DrawSelection(viewBounds); + DrawLabels(viewBounds); + DrawText("Spectrogram", viewBounds.x, viewBounds.y - 30, 20, LIGHTGRAY); DrawText(TextFormat("Frequency (Hz) - Max: %d", app.signal.sampleRate / 2), - bounds.x - 50, bounds.y + bounds.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) { const char* msg1 = "Press 'O' for file browser or drag & drop WAV"; const char* msg2 = "Or use command line: ./rspektrum "; @@ -1069,8 +1120,12 @@ int main(int argc, char* argv[]) DrawText(msg1, GetScreenWidth() / 2 - t1.x / 2, GetScreenHeight() / 2 - t1.y / 2 - 15, 24, LIGHTGRAY); DrawText(msg2, GetScreenWidth() / 2 - t2.x / 2, GetScreenHeight() / 2 - t2.y / 2 + 15, 18, GRAY); } - - DrawInfo(bounds); + + // 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); EndDrawing(); } @@ -1078,6 +1133,9 @@ int main(int argc, char* argv[]) TraceLog(LOG_INFO, "Shutting down..."); if (AudioPlaybackSound.frameCount != 0) UnloadSound(AudioPlaybackSound); if (app.stftComputed) { FreeSTFT(&app.stft); UnloadImage(app.spectrogramImage); UnloadTexture(app.spectrogramTexture); } + if (app.visibleTexture.id != 0) UnloadTexture(app.visibleTexture); + app.visibleTexture = (Texture2D){ 0 }; + app.visibleTextureValid = false; UnloadTexture(colormapTexture); FreeBrowserFiles(); FreeSignal(&app.signal);