Add frequency zoom, scrollbars, dynamic layout that fills window
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
+204
-54
@@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user