From 8e4250ae639b0dfc303503e9451c3a2834d8e878 Mon Sep 17 00:00:00 2001 From: Tyler Date: Mon, 25 May 2026 09:15:50 -0700 Subject: [PATCH] refactor: group selection state into a Selection sub-struct + ClearSelection() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The box-selection state was 11 fields spread across three comment blocks (time sel, freq sel, drag state). Consolidate them into one Selection sub-struct (app.sel.*) and extract the 4-line 'clear to full range' block — duplicated in 5 places — into a ClearSelection() helper. Pure mechanical rename (anchored to app./spa-> so the ScopeView fields of the same name are untouched) + behavior-identical dedup. Fully-settled render verified pixel-identical (AE=0). Co-Authored-By: Claude Opus 4.7 --- src/audio.c | 8 +-- src/render.c | 36 ++++++------- src/spectrogram.c | 114 ++++++++++++++++++---------------------- src/spectrogram_types.h | 39 ++++++++------ src/ui.c | 13 ++--- 5 files changed, 103 insertions(+), 107 deletions(-) diff --git a/src/audio.c b/src/audio.c index d3b2931..33f3a99 100644 --- a/src/audio.c +++ b/src/audio.c @@ -182,8 +182,8 @@ void PlaySelectedRegion(void) { if (!app.loaded || !app.stftComputed) return; - int startSample = (int)(app.timeSelectionStart * app.signal.numSamples); - int endSample = (int)(app.timeSelectionEnd * app.signal.numSamples); + int startSample = (int)(app.sel.timeStart * app.signal.numSamples); + int endSample = (int)(app.sel.timeEnd * app.signal.numSamples); int numSamples = endSample - startSample; if (numSamples <= 0 || startSample < 0 || endSample > app.signal.numSamples) return; @@ -191,8 +191,8 @@ void PlaySelectedRegion(void) memcpy(regionSamples, app.signal.samples + startSample, numSamples * sizeof(float)); float maxFreq = (float)app.signal.sampleRate / 2.0f; - float freqLow = app.freqSelectionStart * maxFreq; - float freqHigh = app.freqSelectionEnd * maxFreq; + float freqLow = app.sel.freqStart * maxFreq; + float freqHigh = app.sel.freqEnd * maxFreq; if (freqLow > 10.0f || freqHigh < maxFreq - 10.0f) { TraceLog(LOG_INFO, "Applying bandpass filter: %.0f - %.0f Hz", freqLow, freqHigh); ApplyBandpassFilter(regionSamples, numSamples, app.signal.sampleRate, freqLow, freqHigh); diff --git a/src/render.c b/src/render.c index 1e4d67e..8245878 100644 --- a/src/render.c +++ b/src/render.c @@ -341,9 +341,9 @@ void DrawLabels(Rectangle bounds) void DrawSelection(Rectangle bounds) { // Only draw if selection is not full range AND not currently dragging - bool hasSelection = (app.timeSelectionStart > 0.001f || app.timeSelectionEnd < 0.999f || - app.freqSelectionStart > 0.001f || app.freqSelectionEnd < 0.999f); - if (!hasSelection || app.isTimeSelecting) return; // Don't draw overlay while dragging + bool hasSelection = (app.sel.timeStart > 0.001f || app.sel.timeEnd < 0.999f || + app.sel.freqStart > 0.001f || app.sel.freqEnd < 0.999f); + if (!hasSelection || app.sel.isTimeSelecting) return; // Don't draw overlay while dragging Color overlayColor = Fade(BLACK, 0.25f); // Lighter overlay @@ -351,10 +351,10 @@ void DrawSelection(Rectangle bounds) float viewWidth = app.viewEnd - app.viewStart; float freqWidth = app.freqViewEnd - app.freqViewStart; - float selStartX = bounds.x + ((app.timeSelectionStart - app.viewStart) / viewWidth) * bounds.width; - float selEndX = bounds.x + ((app.timeSelectionEnd - app.viewStart) / viewWidth) * bounds.width; - float selStartY = bounds.y + bounds.height - ((app.freqSelectionEnd - app.freqViewStart) / freqWidth) * bounds.height; - float selEndY = bounds.y + bounds.height - ((app.freqSelectionStart - app.freqViewStart) / freqWidth) * bounds.height; + float selStartX = bounds.x + ((app.sel.timeStart - app.viewStart) / viewWidth) * bounds.width; + float selEndX = bounds.x + ((app.sel.timeEnd - app.viewStart) / viewWidth) * bounds.width; + float selStartY = bounds.y + bounds.height - ((app.sel.freqEnd - app.freqViewStart) / freqWidth) * bounds.height; + float selEndY = bounds.y + bounds.height - ((app.sel.freqStart - app.freqViewStart) / freqWidth) * bounds.height; // Clamp to viewport bounds selStartX = fmaxf(bounds.x, fminf(bounds.x + bounds.width, selStartX)); @@ -373,8 +373,8 @@ void DrawSelection(Rectangle bounds) // Display selection stats inside viewport, clamped to fit { - int startSample = (int)(app.timeSelectionStart * app.signal.numSamples); - int endSample = (int)(app.timeSelectionEnd * app.signal.numSamples); + int startSample = (int)(app.sel.timeStart * app.signal.numSamples); + int endSample = (int)(app.sel.timeEnd * app.signal.numSamples); SignalStats stats = ComputeSignalStats(&app.signal, startSample, endSample); if (stats.durationSec > 0.0f && app.signal.samples != NULL) { @@ -439,17 +439,17 @@ void DrawSelection(Rectangle bounds) void DrawSelectionDrag(Rectangle bounds) { // Draw bounding box while dragging (no overlay) - if ((!app.isTimeSelecting && !app.isFreqSelecting && !app.isDraggingSelection) || - (app.isDraggingSelection && !IsMouseButtonDown(MOUSE_LEFT_BUTTON))) return; + if ((!app.sel.isTimeSelecting && !app.sel.isFreqSelecting && !app.sel.isDragging) || + (app.sel.isDragging && !IsMouseButtonDown(MOUSE_LEFT_BUTTON))) return; // Convert signal coordinates to viewport coordinates float viewWidth = app.viewEnd - app.viewStart; float freqWidth = app.freqViewEnd - app.freqViewStart; - float selStartX = bounds.x + ((app.timeSelectionStart - app.viewStart) / viewWidth) * bounds.width; - float selEndX = bounds.x + ((app.timeSelectionEnd - app.viewStart) / viewWidth) * bounds.width; - float selStartY = bounds.y + bounds.height - ((app.freqSelectionEnd - app.freqViewStart) / freqWidth) * bounds.height; - float selEndY = bounds.y + bounds.height - ((app.freqSelectionStart - app.freqViewStart) / freqWidth) * bounds.height; + float selStartX = bounds.x + ((app.sel.timeStart - app.viewStart) / viewWidth) * bounds.width; + float selEndX = bounds.x + ((app.sel.timeEnd - app.viewStart) / viewWidth) * bounds.width; + float selStartY = bounds.y + bounds.height - ((app.sel.freqEnd - app.freqViewStart) / freqWidth) * bounds.height; + float selEndY = bounds.y + bounds.height - ((app.sel.freqStart - app.freqViewStart) / freqWidth) * bounds.height; // Clamp to viewport bounds selStartX = fmaxf(bounds.x, fminf(bounds.x + bounds.width, selStartX)); @@ -467,8 +467,8 @@ void DrawSelectionDrag(Rectangle bounds) // Display live stats while dragging (inside viewport, clamped to fit) { - int startSample = (int)(app.timeSelectionStart * app.signal.numSamples); - int endSample = (int)(app.timeSelectionEnd * app.signal.numSamples); + int startSample = (int)(app.sel.timeStart * app.signal.numSamples); + int endSample = (int)(app.sel.timeEnd * app.signal.numSamples); SignalStats stats = ComputeSignalStats(&app.signal, startSample, endSample); if (stats.durationSec > 0.0f && app.signal.samples != NULL) { @@ -538,7 +538,7 @@ void DrawPlayhead(Rectangle bounds) { if (!app.isPlaying || app.playheadT < 0.0f || app.playheadT > 1.0f) return; - float timePos = app.timeSelectionStart + app.playheadT * (app.timeSelectionEnd - app.timeSelectionStart); + float timePos = app.sel.timeStart + app.playheadT * (app.sel.timeEnd - app.sel.timeStart); float viewWidth = app.viewEnd - app.viewStart; float t = (timePos - app.viewStart) / viewWidth; float x = bounds.x + t * bounds.width; diff --git a/src/spectrogram.c b/src/spectrogram.c index ee513f7..f78678a 100644 --- a/src/spectrogram.c +++ b/src/spectrogram.c @@ -120,8 +120,7 @@ void ResetForNewSignal(void) // Reset view + selection to full range. app.viewStart = 0.0f; app.viewEnd = 1.0f; - app.timeSelectionStart = 0.0f; app.timeSelectionEnd = 1.0f; - app.freqSelectionStart = 0.0f; app.freqSelectionEnd = 1.0f; + ClearSelection(); // Invalidate the cached visible texture. if (app.visibleTexture.id != 0) UnloadTexture(app.visibleTexture); @@ -223,8 +222,8 @@ int main(int argc, char* argv[]) TraceLog(LOG_WARNING, "Failed to load TTF font, using default bitmap font"); } - app.timeSelectionStart = 0.0f; app.timeSelectionEnd = 1.0f; - app.freqSelectionStart = 0.0f; app.freqSelectionEnd = 1.0f; + app.sel.timeStart = 0.0f; app.sel.timeEnd = 1.0f; + app.sel.freqStart = 0.0f; app.sel.freqEnd = 1.0f; app.viewStart = 0.0f; app.viewEnd = 1.0f; app.freqViewStart = 0.0f; app.freqViewEnd = 1.0f; app.showGrid = true; @@ -326,7 +325,7 @@ int main(int argc, char* argv[]) } // Track playhead position manually app.playheadElapsed += GetFrameTime(); - float selectionDuration = (app.timeSelectionEnd - app.timeSelectionStart) * app.signal.duration; + float selectionDuration = (app.sel.timeEnd - app.sel.timeStart) * app.signal.duration; if (selectionDuration > 0) { app.playheadT = app.playheadElapsed / selectionDuration; } @@ -528,10 +527,7 @@ int main(int argc, char* argv[]) 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; + ClearSelection(); } } @@ -556,15 +552,12 @@ int main(int argc, char* argv[]) // Right-click clears selection if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT) && CheckCollisionPointRec(mousePos, selBounds)) { - app.timeSelectionStart = 0.0f; - app.timeSelectionEnd = 1.0f; - app.freqSelectionStart = 0.0f; - app.freqSelectionEnd = 1.0f; + ClearSelection(); } // Check if click is inside existing selection (for dragging) - bool hasSelection = (app.timeSelectionStart > 0.001f || app.timeSelectionEnd < 0.999f || - app.freqSelectionStart > 0.001f || app.freqSelectionEnd < 0.999f); + bool hasSelection = (app.sel.timeStart > 0.001f || app.sel.timeEnd < 0.999f || + app.sel.freqStart > 0.001f || app.sel.freqEnd < 0.999f); bool clickInsideSelection = false; bool hoverInsideSelection = false; if (hasSelection && CheckCollisionPointRec(mousePos, selBounds)) { @@ -574,8 +567,8 @@ int main(int argc, char* argv[]) float mouseTime = app.viewStart + ((mousePos.x - selBounds.x) / selBounds.width) * viewWidth; float mouseFreq = app.freqViewStart + (1.0f - (mousePos.y - selBounds.y) / selBounds.height) * freqWidth; - if (mouseTime >= app.timeSelectionStart && mouseTime <= app.timeSelectionEnd && - mouseFreq >= app.freqSelectionStart && mouseFreq <= app.freqSelectionEnd) { + if (mouseTime >= app.sel.timeStart && mouseTime <= app.sel.timeEnd && + mouseFreq >= app.sel.freqStart && mouseFreq <= app.sel.freqEnd) { hoverInsideSelection = true; if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { clickInsideSelection = true; @@ -584,7 +577,7 @@ int main(int argc, char* argv[]) } // Set cursor based on context - if (app.isDraggingSelection) { + if (app.sel.isDragging) { SetMouseCursor(MOUSE_CURSOR_RESIZE_ALL); // 4-way arrow while dragging } else if (hoverInsideSelection) { SetMouseCursor(MOUSE_CURSOR_POINTING_HAND); // Pointing hand on hover @@ -597,7 +590,7 @@ int main(int argc, char* argv[]) // Set cursor to resize all when near divider if (mouseNearDivider && !app.isDividing) { SetMouseCursor(MOUSE_CURSOR_RESIZE_ALL); - } else if (app.isDraggingSelection) { + } else if (app.sel.isDragging) { SetMouseCursor(MOUSE_CURSOR_RESIZE_ALL); // 4-way arrow while dragging } else if (hoverInsideSelection) { SetMouseCursor(MOUSE_CURSOR_POINTING_HAND); // Pointing hand on hover @@ -607,85 +600,82 @@ int main(int argc, char* argv[]) if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { if (clickInsideSelection) { // Start dragging existing selection - app.isDraggingSelection = true; - app.dragSelectionStartPos = mousePos; - app.dragSelectionTimeStart = app.timeSelectionStart; - app.dragSelectionFreqStart = app.freqSelectionStart; + app.sel.isDragging = true; + app.sel.dragStartPos = mousePos; + app.sel.dragTimeStart = app.sel.timeStart; + app.sel.dragFreqStart = app.sel.freqStart; } else { // Start new box selection - app.isTimeSelecting = true; - app.isFreqSelecting = true; - app.selectStartPos = mousePos; + app.sel.isTimeSelecting = true; + app.sel.isFreqSelecting = true; + app.sel.selectStartPos = mousePos; // Convert screen position to signal coordinates (accounting for zoom) float viewportT = (mousePos.x - selBounds.x) / selBounds.width; float viewportF = 1.0f - (mousePos.y - selBounds.y) / selBounds.height; - app.timeSelectionStart = Clamp(app.viewStart + viewportT * (app.viewEnd - app.viewStart), 0.0f, 1.0f); - app.timeSelectionEnd = app.timeSelectionStart; - app.freqSelectionStart = Clamp(app.freqViewStart + viewportF * (app.freqViewEnd - app.freqViewStart), 0.0f, 1.0f); - app.freqSelectionEnd = app.freqSelectionStart; + app.sel.timeStart = Clamp(app.viewStart + viewportT * (app.viewEnd - app.viewStart), 0.0f, 1.0f); + app.sel.timeEnd = app.sel.timeStart; + app.sel.freqStart = Clamp(app.freqViewStart + viewportF * (app.freqViewEnd - app.freqViewStart), 0.0f, 1.0f); + app.sel.freqEnd = app.sel.freqStart; } } // Dragging existing selection - if (app.isDraggingSelection && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) { + if (app.sel.isDragging && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) { float viewWidth = app.viewEnd - app.viewStart; float freqWidth = app.freqViewEnd - app.freqViewStart; - float dx = (mousePos.x - app.dragSelectionStartPos.x) / selBounds.width; - float dy = (mousePos.y - app.dragSelectionStartPos.y) / selBounds.height; + float dx = (mousePos.x - app.sel.dragStartPos.x) / selBounds.width; + float dy = (mousePos.y - app.sel.dragStartPos.y) / selBounds.height; float timeShift = dx * viewWidth; float freqShift = -dy * freqWidth; // Y is inverted - float timeWidth = app.timeSelectionEnd - app.timeSelectionStart; - float freqHeight = app.freqSelectionEnd - app.freqSelectionStart; + float timeWidth = app.sel.timeEnd - app.sel.timeStart; + float freqHeight = app.sel.freqEnd - app.sel.freqStart; - app.timeSelectionStart = Clamp(app.dragSelectionTimeStart + timeShift, 0.0f, 1.0f - timeWidth); - app.timeSelectionEnd = app.timeSelectionStart + timeWidth; - app.freqSelectionStart = Clamp(app.dragSelectionFreqStart + freqShift, 0.0f, 1.0f - freqHeight); - app.freqSelectionEnd = app.freqSelectionStart + freqHeight; + app.sel.timeStart = Clamp(app.sel.dragTimeStart + timeShift, 0.0f, 1.0f - timeWidth); + app.sel.timeEnd = app.sel.timeStart + timeWidth; + app.sel.freqStart = Clamp(app.sel.dragFreqStart + freqShift, 0.0f, 1.0f - freqHeight); + app.sel.freqEnd = app.sel.freqStart + freqHeight; } // Creating new box selection - if ((app.isTimeSelecting || app.isFreqSelecting) && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) { + if ((app.sel.isTimeSelecting || app.sel.isFreqSelecting) && IsMouseButtonDown(MOUSE_LEFT_BUTTON)) { float viewportT = (mousePos.x - selBounds.x) / selBounds.width; float viewportF = 1.0f - (mousePos.y - selBounds.y) / selBounds.height; - app.timeSelectionEnd = Clamp(app.viewStart + viewportT * (app.viewEnd - app.viewStart), 0.0f, 1.0f); - app.freqSelectionEnd = Clamp(app.freqViewStart + viewportF * (app.freqViewEnd - app.freqViewStart), 0.0f, 1.0f); + app.sel.timeEnd = Clamp(app.viewStart + viewportT * (app.viewEnd - app.viewStart), 0.0f, 1.0f); + app.sel.freqEnd = Clamp(app.freqViewStart + viewportF * (app.freqViewEnd - app.freqViewStart), 0.0f, 1.0f); } if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { - if (app.isDraggingSelection) { - app.isDraggingSelection = false; - } else if (app.isTimeSelecting || app.isFreqSelecting) { + if (app.sel.isDragging) { + app.sel.isDragging = false; + } else if (app.sel.isTimeSelecting || app.sel.isFreqSelecting) { // Check if drag was large enough (minimum 5 pixels) - float dx = mousePos.x - app.selectStartPos.x; - float dy = mousePos.y - app.selectStartPos.y; + float dx = mousePos.x - app.sel.selectStartPos.x; + float dy = mousePos.y - app.sel.selectStartPos.y; float dragDist = sqrtf(dx * dx + dy * dy); if (dragDist > 5.0f) { // Normalize so start < end - if (app.timeSelectionEnd < app.timeSelectionStart) { - float tmp = app.timeSelectionStart; - app.timeSelectionStart = app.timeSelectionEnd; - app.timeSelectionEnd = tmp; + if (app.sel.timeEnd < app.sel.timeStart) { + float tmp = app.sel.timeStart; + app.sel.timeStart = app.sel.timeEnd; + app.sel.timeEnd = tmp; } - if (app.freqSelectionEnd < app.freqSelectionStart) { - float tmp = app.freqSelectionStart; - app.freqSelectionStart = app.freqSelectionEnd; - app.freqSelectionEnd = tmp; + if (app.sel.freqEnd < app.sel.freqStart) { + float tmp = app.sel.freqStart; + app.sel.freqStart = app.sel.freqEnd; + app.sel.freqEnd = tmp; } } else { // Drag too small - revert to full range - app.timeSelectionStart = 0.0f; - app.timeSelectionEnd = 1.0f; - app.freqSelectionStart = 0.0f; - app.freqSelectionEnd = 1.0f; + ClearSelection(); } - app.isTimeSelecting = false; - app.isFreqSelecting = false; + app.sel.isTimeSelecting = false; + app.sel.isFreqSelecting = false; } } } @@ -982,7 +972,7 @@ int main(int argc, char* argv[]) app.scopeView.data.sampleRate = app.signal.sampleRate; // Show playhead if playing if (app.isPlaying) { - DrawScopeView(&app.scopeView, app.timeSelectionStart + app.playheadT * (app.timeSelectionEnd - app.timeSelectionStart)); + DrawScopeView(&app.scopeView, app.sel.timeStart + app.playheadT * (app.sel.timeEnd - app.sel.timeStart)); } else { DrawScopeView(&app.scopeView, -1.0f); } diff --git a/src/spectrogram_types.h b/src/spectrogram_types.h index a6c45a5..426bcec 100644 --- a/src/spectrogram_types.h +++ b/src/spectrogram_types.h @@ -91,6 +91,21 @@ typedef struct { int nextOrder; } FFTSizeCache; +// The time+frequency box selection and its drag/move interaction state. +// All coordinates are 0-1 normalized. A "box-select" drags out a new box; +// a "move" drags an existing box around. +typedef struct { + float timeStart, timeEnd; // selected time span + float freqStart, freqEnd; // selected frequency span + bool isTimeSelecting; // dragging out a new time span + bool isFreqSelecting; // dragging out a new frequency span + Vector2 selectStartPos; // mouse pos when a box-select began (min-drag check) + bool isDragging; // moving an existing selection box + Vector2 dragStartPos; // mouse pos when the move began + float dragTimeStart; // selection start time when the move began + float dragFreqStart; // selection freq start when the move began +} Selection; + typedef struct { AudioSignal signal; StftResult stft; @@ -103,27 +118,14 @@ typedef struct { float playheadT; // 0-1 normalized position in selection float playheadElapsed; // Elapsed seconds since play started - // Time selection (0-1 normalized) - float timeSelectionStart; - float timeSelectionEnd; - bool isTimeSelecting; - - // Frequency selection (0-1 normalized) - float freqSelectionStart; - float freqSelectionEnd; - bool isFreqSelecting; + // Time + frequency box selection and its drag/move interaction state. + Selection sel; // Export settings float exportScale; char exportDir[4096]; char exportMessage[256]; - Vector2 selectStartPos; // For minimum drag distance check - bool isDraggingSelection; // Dragging existing selection box - Vector2 dragSelectionStartPos; // Mouse position when started dragging selection - float dragSelectionTimeStart; // Selection start time when dragging - float dragSelectionFreqStart; // Selection freq start when dragging - // Viewport/zoom controls float viewStart; // 0-1, start of visible time region float viewEnd; // 0-1, end of visible time region @@ -233,6 +235,13 @@ static inline bool UiModalOpen(void) return app.showFileBrowser || app.showAbout; } +// Reset the box selection to the full signal (the "no selection" state). +static inline void ClearSelection(void) +{ + app.sel.timeStart = 0.0f; app.sel.timeEnd = 1.0f; + app.sel.freqStart = 0.0f; app.sel.freqEnd = 1.0f; +} + // ============================================================================ // Keymap — single source of truth for global key bindings. // The dispatcher (DispatchKeymap in spectrogram.c) runs every entry whose diff --git a/src/ui.c b/src/ui.c index d676246..1e59e56 100644 --- a/src/ui.c +++ b/src/ui.c @@ -266,10 +266,10 @@ void ExportPNG(const SpectrogramApp* spa, const char* dirPath) 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); + int selX0 = (int)(spa->sel.timeStart * imgW); + int selX1 = (int)(spa->sel.timeEnd * imgW); + int selY0 = (int)((1.0f - spa->sel.freqEnd) * imgH); + int selY1 = (int)((1.0f - spa->sel.freqStart) * imgH); // Clamp to image bounds selX0 = Clamp(selX0, 0, imgW); @@ -468,10 +468,7 @@ void DrawSidebar(void) // Clear selection (identical to the Esc key) Rectangle clearButton = { x, y, sidebarWidth - 10 * scale, 25 * scale }; if (Clicked(clearButton)) { - app.timeSelectionStart = 0.0f; - app.timeSelectionEnd = 1.0f; - app.freqSelectionStart = 0.0f; - app.freqSelectionEnd = 1.0f; + ClearSelection(); } DrawPanelBox(clearButton, (Color){ 80, 50, 50, 255 }, RED); DrawTextScaled("Clear Selection (ESC)", clearButton.x + 10 * scale, clearButton.y + 6 * scale, 14, WHITE);