fix: track the playhead against the region actually playing

PlaySelectedRegion copies, bandpasses and normalises the selected span up
front, so the audio in flight is fixed the moment Space is pressed. The
playhead, though, was drawn against the live app.sel and its duration
re-derived from the live selection — so moving or resizing the selection
mid-playback made the marker jump, overrun, or scale against a region
that had nothing to do with what was audible.

Snapshot the played region (playSelStart/playSelEnd) and take the duration
from the buffer's own sample count rather than app.signal.duration, which
is derived pre-mono-downmix and disagrees for stereo files. The playhead
and the scope cursor both read the snapshot; playheadT is clamped at 1.

Also: a sub-threshold click inside an existing selection no longer clears
it. Silently resetting to full range there changes what Space plays, which
is surprising when the click was an aborted drag. A click on empty space
still resets as before.

The *semantics* of editing a selection during playback remain undefined —
whether audio should follow the box, stop, or keep going as it does now.
This commit only makes the marker honest about what is coming out of the
speakers. Recorded in known_bugs.md.

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 00:19:02 -07:00
parent 46e796cdeb
commit c4687ce80d
4 changed files with 36 additions and 9 deletions
+15 -7
View File
@@ -945,11 +945,14 @@ int main(int argc, char* argv[])
app.isPlaying = false;
app.playbackFinished = true;
}
// Track playhead position manually
// Track playhead position manually, against the length of the buffer
// that's actually playing (see playDuration) rather than a length
// re-derived from the live selection — the user can move the
// selection mid-playback without the marker jumping.
app.playheadElapsed += GetFrameTime();
float selectionDuration = (app.sel.timeEnd - app.sel.timeStart) * app.signal.duration;
if (selectionDuration > 0) {
app.playheadT = app.playheadElapsed / selectionDuration;
if (app.playDuration > 0.0f) {
app.playheadT = app.playheadElapsed / app.playDuration;
if (app.playheadT > 1.0f) app.playheadT = 1.0f;
}
}
@@ -1324,8 +1327,13 @@ int main(int argc, char* argv[])
app.sel.freqStart = app.sel.freqEnd;
app.sel.freqEnd = tmp;
}
} else {
// Drag too small - revert to full range
} else if (!hoverInsideSelection) {
// Sub-threshold drag outside any existing selection: treat
// as a click on empty space and reset to full range. A
// stray click *inside* the current box leaves it alone —
// silently clearing it there changes what Space plays.
// (hoverInsideSelection, not clickInsideSelection: the
// latter is press-frame-only and is always false here.)
ClearSelection();
}
app.sel.isTimeSelecting = false;
@@ -1715,7 +1723,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.sel.timeStart + app.playheadT * (app.sel.timeEnd - app.sel.timeStart));
DrawScopeView(&app.scopeView, app.playSelStart + app.playheadT * (app.playSelEnd - app.playSelStart));
} else {
DrawScopeView(&app.scopeView, -1.0f);
}