feat: scale time zoom by duration, decouple the two axes
The minimum visible time window was a flat 2% of the file, but view.start and view.end are normalized to the whole signal — so the achievable time resolution scaled with file length. A 30-minute recording could never show less than a 36-second span, while a 30-second one reached 0.6 s. Long captures were effectively unreadable at the sample level no matter how far you scrolled. Derive the floor from the STFT hop instead (MinTimeViewWidth): segments sit fftSize/HOP_RATIO samples apart, so the real limit is the point where only a handful of segments span the viewport and further zoom would interpolate rather than reveal. The floor is now a constant ~43 ms at 48 kHz/1024 regardless of duration — an 844x improvement on a 30-minute file, and it tightens further with a smaller FFT. Guards cover the unloaded (sampleRate 0) and shorter-than-the-floor cases. Zooming is also no longer forced to move both axes together. The bare wheel keeps the existing coupled behaviour; Shift+wheel is time-only and Ctrl+wheel frequency-only, so a long capture can be stretched along time without collapsing the frequency range to match. Time-axis labels now pick their precision from the span between adjacent ticks (1 to 4 decimals, and m:ss.sss past a minute). At the spans this change makes reachable the old fixed "%.1fs" printed the same value in every slot, which read as a frozen axis. Adds a `wheel X Y N [mod]` action to shot_input.sh for exercising zoom headlessly, and known_bugs.md for behaviour that is unspecified rather than broken. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8ZWfr5XZyyDttvkhJUgHN
This commit is contained in:
+21
-4
@@ -976,18 +976,32 @@ int main(int argc, char* argv[])
|
||||
float spectroHeight = L.spectroHeight;
|
||||
Rectangle viewBounds = L.viewBounds;
|
||||
|
||||
// Zoom with mouse wheel (zooms both time and frequency to maintain aspect ratio)
|
||||
// Zoom with mouse wheel. Bare wheel zooms both axes together (keeps
|
||||
// the aspect ratio); Shift+wheel is time-only and Ctrl+wheel is
|
||||
// frequency-only, for when you need to stretch one axis alone.
|
||||
if (GetMousePosition().x > sidebarWidth + 5 && CheckCollisionPointRec(GetMousePosition(), viewBounds)) {
|
||||
int wheel = GetMouseWheelMove();
|
||||
if (wheel != 0) {
|
||||
float zoomFactor = (wheel > 0) ? 0.8f : 1.2f;
|
||||
|
||||
|
||||
bool shiftHeld = IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT);
|
||||
bool ctrlHeld = IsKeyDown(KEY_LEFT_CONTROL) || IsKeyDown(KEY_RIGHT_CONTROL);
|
||||
bool zoomTime = !ctrlHeld; // Ctrl = frequency only
|
||||
bool zoomFreq = !shiftHeld; // Shift = time only
|
||||
|
||||
// --- Time axis zoom (around cursor X) ---
|
||||
if (zoomTime) {
|
||||
float mouseT = (GetMousePosition().x - viewBounds.x) / viewBounds.width;
|
||||
mouseT = app.view.start + mouseT * (app.view.end - app.view.start);
|
||||
float viewWidth = app.view.end - app.view.start;
|
||||
float newWidth = viewWidth * zoomFactor;
|
||||
if (newWidth < 0.02f) newWidth = 0.02f;
|
||||
// Floor the window in SECONDS, not as a fraction of the file.
|
||||
// A flat 2% floor meant a 30-minute recording could never show
|
||||
// less than 36 s, while a 30-second one bottomed out at 0.6 s.
|
||||
// The real limit is the STFT hop: once fewer than a handful of
|
||||
// segments span the viewport there is no more detail to expose.
|
||||
float minWidth = MinTimeViewWidth();
|
||||
if (newWidth < minWidth) newWidth = minWidth;
|
||||
if (newWidth > 1.0f) newWidth = 1.0f;
|
||||
float leftOfMouse = mouseT - app.view.start;
|
||||
float rightOfMouse = app.view.end - mouseT;
|
||||
@@ -995,8 +1009,10 @@ int main(int argc, char* argv[])
|
||||
app.view.end = mouseT + rightOfMouse * (newWidth / viewWidth);
|
||||
if (app.view.start < 0) { app.view.start = 0; app.view.end = newWidth; }
|
||||
if (app.view.end > 1) { app.view.end = 1; app.view.start = 1 - newWidth; }
|
||||
|
||||
}
|
||||
|
||||
// --- Frequency axis zoom (around cursor Y) ---
|
||||
if (zoomFreq) {
|
||||
float mouseF = 1.0f - (GetMousePosition().y - viewBounds.y) / viewBounds.height;
|
||||
mouseF = app.view.freqStart + mouseF * (app.view.freqEnd - app.view.freqStart);
|
||||
float freqWidth = app.view.freqEnd - app.view.freqStart;
|
||||
@@ -1009,6 +1025,7 @@ int main(int argc, char* argv[])
|
||||
// Clamp to physical frequency limits [0, 1] — can't see beyond Nyquist or below 0 Hz
|
||||
if (app.view.freqStart < 0) { app.view.freqStart = 0; app.view.freqEnd = fminf(app.view.freqEnd, 1.0f); }
|
||||
if (app.view.freqEnd > 1) { app.view.freqEnd = 1; app.view.freqStart = fmaxf(app.view.freqStart, 0.0f); }
|
||||
}
|
||||
|
||||
// Invalidate texture cache
|
||||
app.visibleTextureValid = false;
|
||||
|
||||
Reference in New Issue
Block a user