From c4687ce80db93585a8abfd7075c9f1ca698a448d Mon Sep 17 00:00:00 2001 From: Tyler Date: Wed, 12 Aug 2026 00:19:02 -0700 Subject: [PATCH] fix: track the playhead against the region actually playing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01V8ZWfr5XZyyDttvkhJUgHN --- src/audio.c | 8 ++++++++ src/render.c | 3 ++- src/spectrogram.c | 22 +++++++++++++++------- src/spectrogram_types.h | 12 +++++++++++- 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/audio.c b/src/audio.c index 71c47e0..d7111e2 100644 --- a/src/audio.c +++ b/src/audio.c @@ -243,6 +243,14 @@ void PlaySelectedRegion(void) float* regionSamples = BuildSelectionAudio(&numSamples); if (!regionSamples) return; + // Snapshot what we're about to play so the playhead tracks THIS region even + // if the user moves the selection mid-playback. Duration comes from the + // buffer we actually built, not from app.signal.duration. + app.playSelStart = app.sel.timeStart; + app.playSelEnd = app.sel.timeEnd; + app.playDuration = (app.signal.sampleRate > 0) + ? (float)numSamples / (float)app.signal.sampleRate : 0.0f; + EnsureAudioDevice(); // opened on demand; released again once playback ends if (AudioPlaybackSound.frameCount != 0) UnloadSound(AudioPlaybackSound); diff --git a/src/render.c b/src/render.c index 22e382b..6fb5988 100644 --- a/src/render.c +++ b/src/render.c @@ -1685,7 +1685,8 @@ void DrawPlayhead(Rectangle bounds) { if (!app.isPlaying || app.playheadT < 0.0f || app.playheadT > 1.0f) return; - float timePos = app.sel.timeStart + app.playheadT * (app.sel.timeEnd - app.sel.timeStart); + // Against the snapshot of the playing region, not the live selection. + float timePos = app.playSelStart + app.playheadT * (app.playSelEnd - app.playSelStart); float viewWidth = app.view.end - app.view.start; float t = (timePos - app.view.start) / viewWidth; float x = bounds.x + t * bounds.width; diff --git a/src/spectrogram.c b/src/spectrogram.c index ef872e3..337e82e 100644 --- a/src/spectrogram.c +++ b/src/spectrogram.c @@ -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); } diff --git a/src/spectrogram_types.h b/src/spectrogram_types.h index 9a7e580..fbb8a83 100644 --- a/src/spectrogram_types.h +++ b/src/spectrogram_types.h @@ -138,9 +138,19 @@ typedef struct { bool stftComputed; // Playback state - float playheadT; // 0-1 normalized position in selection + float playheadT; // 0-1 normalized position within the PLAYING region float playheadElapsed; // Elapsed seconds since play started + // Snapshot of the region actually handed to the audio device, captured at + // PlaySelectedRegion time. The playhead must be measured against this, not + // against the live app.sel — the user can move or resize the selection while + // audio is still playing, and the marker has to keep tracking the sound + // that's really coming out. playDuration comes from the buffer's own sample + // count / sampleRate, so it can't drift from app.signal.duration (which is + // derived pre-mono-downmix and disagrees for stereo files). + float playSelStart, playSelEnd; // sel.timeStart/End when playback began + float playDuration; // true length of the playing buffer, seconds + // Time + frequency box selection and its drag/move interaction state. Selection sel;