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:
@@ -0,0 +1,72 @@
|
||||
# Known bugs & rough edges
|
||||
|
||||
Behaviour that is unspecified, awkward, or known-imperfect — as distinct from
|
||||
outright breakage. Each entry says what happens, why, and what a real fix would
|
||||
need to decide.
|
||||
|
||||
---
|
||||
|
||||
## Playhead vs. a selection edited mid-playback
|
||||
|
||||
**Status:** partially addressed; underlying semantics still undefined.
|
||||
|
||||
Playback hands a *snapshot* of the selected region to the audio device — the
|
||||
samples are copied, bandpassed, and normalised up front, so the sound coming out
|
||||
of the speakers is fixed the moment **Space** is pressed. The selection box,
|
||||
however, stays live and editable while that audio plays.
|
||||
|
||||
Previously the playhead marker was drawn against the *live* `app.sel`, so moving
|
||||
or resizing the selection during playback made the marker jump, run off the end,
|
||||
or scale to a region that had nothing to do with what was audible. The playhead
|
||||
is now measured against `playSelStart` / `playSelEnd` / `playDuration`, captured
|
||||
at `PlaySelectedRegion()` time, so it tracks the audio that is actually playing.
|
||||
|
||||
What remains undefined is the *product* question, not the drawing math:
|
||||
|
||||
- If the user drags the selection somewhere else mid-playback, should the audio
|
||||
follow (restart / re-seek against the new region), or should playback keep
|
||||
going with the old buffer and the marker stay where it is (current behaviour)?
|
||||
- Should editing the selection during playback simply stop playback?
|
||||
- Should the playhead remain visible when the region it refers to is scrolled
|
||||
off-screen, or has been replaced by a selection elsewhere in the file?
|
||||
|
||||
Current behaviour is the conservative reading: **the sound wins**. The marker
|
||||
always describes real audio, and a mid-playback edit is treated as staging the
|
||||
*next* thing to play rather than modifying the current one. That is defensible
|
||||
but was never explicitly chosen, and the UI gives no feedback that the box on
|
||||
screen and the audio in flight have diverged.
|
||||
|
||||
Related: a sub-threshold click *inside* an existing selection deliberately does
|
||||
not clear it (`hoverInsideSelection` in `spectrogram.c`), because silently
|
||||
clearing changes what **Space** would play. A click on empty space still resets
|
||||
to full range.
|
||||
|
||||
**Touches:** `audio.c` (`PlaySelectedRegion`), `spectrogram.c` (playhead
|
||||
advance, scope cursor), `render.c` (`DrawPlayhead`), `spectrogram_types.h`
|
||||
(`playSelStart` / `playSelEnd` / `playDuration`).
|
||||
|
||||
---
|
||||
|
||||
## Long-file zoom sharpness lags the zoom gesture
|
||||
|
||||
**Status:** working as designed, but reads as a bug.
|
||||
|
||||
`ComputeSkipFactor()` (`stft.c`) strides the initial STFT pass for long files —
|
||||
every 8th segment past 10 minutes — so the overview loads promptly. The missing
|
||||
segments are filled at full resolution afterwards: the visible range first, then
|
||||
a background sweep of the whole file.
|
||||
|
||||
The practical effect is that a hard zoom into a 30-minute file can look blocky
|
||||
for a moment before the foreground fill catches up and it sharpens. The fill is
|
||||
gated on `view.end - view.start <= 0.25f`, so it only runs once reasonably zoomed
|
||||
in. If a view stays blocky indefinitely, the fill is not reaching that range and
|
||||
that *is* a real bug worth chasing.
|
||||
|
||||
---
|
||||
|
||||
## Load time on long files is unbounded and unreported
|
||||
|
||||
A 30-minute 48 kHz file spends a long time in `Processing…` before the UI is
|
||||
usable, and the percentage indicator advances non-linearly (the strided overview
|
||||
completes fast, the high-res fill does not). There is no cancel. Headless/scripted
|
||||
runs must wait this out; see `AGENTS.md`.
|
||||
Reference in New Issue
Block a user