fix: playhead drift, replay, and cursor flicker; add selection transport

**Playhead ran ahead of the audio.** It was dead-reckoned by summing
GetFrameTime() every frame, so every frame's *render* work counted as
playback time and the error compounded — by seconds over a long region,
and worst when zoomed out where each frame does the most work. That is why
it tracked fine zoomed in. It is now derived from a wall-clock instant
captured when the buffer is handed to the device, so it cannot drift from
the audio regardless of frame timing.

**Replay after a natural finish left the marker stuck at the end.** The
rail's play button never cleared playbackFinished, so the stale
playheadT > 1.0 persisted and DrawPlayhead early-returned while the audio
played from the top. The Space path already handled this; the button did
not.

**No way to replay a subrange.** The playhead is now drawn while stopped
(with a grab tab) and can be dragged to set where the next play starts
*within* the selection, leaving the region itself intact. Stopping parks
the marker where it stopped rather than snapping to the start.

playheadT is a fraction of the *played* span, which stops being the
selection once a scrub offset exists — so every conversion goes through
absolute file time, the only frame the two share. Getting this wrong made
stop-after-scrub jump backwards.

**Selection transport bar.** Rewind / play-pause / stop / loop, attached
above the selection box. Pause resumes where it left off; loop restarts
from the top of the region rather than repeating whatever tail the last
play started from.

**Cursor flicker.** Twelve unconditional SetMouseCursor calls ran per
frame and the last one won. Two blocks in particular both ran every frame:
one set a cursor regardless of mouse position, and a second overrode it
only when the mouse was inside the spectrogram — so when a capture guard
had parked mousePos off-screen, the first block's stale choice stuck. That
is the fight between finger/pointer, resize/pointer and crosshair/pointer.
Handlers now record a prioritised request (active drag > hover hint >
default) and it is applied once at the end of the frame. The divider hint
is RESIZE_NS rather than the 4-way arrow while here.

The rewind glyph drew its bar and triangle with a gap between them and read
as a lone vertical bar; verified the fix by rendering it offscreen and
dumping pixels rather than eyeballing the geometry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8ZWfr5XZyyDttvkhJUgHN
This commit is contained in:
2026-08-12 15:56:33 -07:00
parent cce96659b1
commit 56b1666850
7 changed files with 415 additions and 67 deletions
+22 -20
View File
@@ -44,10 +44,15 @@ format.
- **mLnL annotation overlay** — labelled boxes from the WAV's embedded annotation
chunk; hover a box (or its region on the scope) for per-frame detail (sequence,
channel, rate, scheduling offset…).
- **Zoom & pan** the time/frequency view.
- **Zoom & pan** the time/frequency view, with a **minimap** for navigating a
long capture without zooming out and back in.
- **Collision detection** — finds transmissions that genuinely overlap in *both*
time and frequency, and jumps between them.
- **Region selection** — box a time *and* frequency range with the mouse.
- **Filtered playback** — play just the selected region, band-limited to the
selected frequency box via an FFT bandpass. What you hear is what you'd export.
Transport controls (play/pause, rewind, loop) attach to the selection, and the
playhead can be dragged to replay a subrange.
- **Waveform scope** — toggleable time-domain view beneath the spectrum.
- **Marker / ruler** and a **spectrum slice (PSD)** readout.
- **Export** — save the view as a PNG, or the selected region as a WAV.
@@ -141,6 +146,7 @@ Middle-drag always pans.
| **Ctrl+wheel** | Zoom the frequency axis only |
| **Wheel on a scrollbar** | Pan that axis (**Shift** to zoom it) |
| **Space** | Play / stop the selected region |
| **Drag the playhead** | (while stopped) set where the next play starts |
| **Hover an annotation** | Tooltip with that frame's mLnL detail; lists **every** overlapping frame under the cursor |
| **N** / **Shift+N** | Jump to the next / previous collision |
| **O** | Open file browser |
@@ -180,6 +186,17 @@ texture and only rebuilt when its *content* changes (new file, colormap,
overlays toggled); panning and zooming just move the rectangle drawn on top, so
navigation costs nothing.
### Playback
**Space** plays the selected region, band-limited to the selected frequency box.
Selecting a region also brings up a small transport bar above it — rewind,
play/pause, stop, and loop. Pause resumes where it left off; loop repeats the
whole region.
While stopped, the playhead stays where it is and can be dragged: that sets
where the next play begins *within* the selection, so a subrange can be replayed
without redrawing the region. Clearing or redrawing the selection resets it.
### Inspecting overlapping transmissions
When several stations are on the air at once their annotation boxes stack, and
@@ -255,25 +272,10 @@ Annotation kinds: `tx_frame`, `tx_burst`, `control`, `channel_up`,
## Driving the GUI headlessly (agents / CI)
The app can be run, screenshotted, and clicked on a virtual X display with no
monitor or GPU (Mesa software GL under Xvfb). The full playbook lives in
[`AGENTS.md`](AGENTS.md); the working reference implementation is
[`shot_input.sh`](shot_input.sh).
The loop in one breath:
```bash
Xvfb :99 -screen 0 1280x800x24 >/tmp/xvfb.log 2>&1 & # 1. fake screen
DISPLAY=:99 ./bin/Debug/rspektrum mlnl_samples.wav \
>/tmp/app.log 2>&1 & # 2. run on it
sleep 2 # 3. reach a steady frame
DISPLAY=:99 import -window root /tmp/shot.png # 4. grab the frame
```
Prerequisites (Debian/Ubuntu): `sudo apt-get install xvfb imagemagick xdotool`
(plus `libgl1-mesa-dri` and `LIBGL_ALWAYS_SOFTWARE=1` if GL fails / frames are
black). Synthesize input with `xdotool` against `DISPLAY=:99` to exercise UI
paths.
The app runs, screenshots, and takes synthetic input on a virtual X display with
no monitor or GPU (Mesa software GL under Xvfb). The playbook is in
[`AGENTS.md`](AGENTS.md); [`shot_input.sh`](shot_input.sh) is the working
reference implementation.
---