feat: add playhead indicator during audio playback

Add a red vertical line that tracks playback position within the
selected region. The playhead shows the current position in the
spectrogram as audio plays, making it easy to see what part of
the signal is currently being heard.

- playheadT tracks 0-1 normalized position in the selection
- Red line drawn across the spectrogram viewport while playing
- Semi-transparent red highlight behind the line for visibility
- Reset playhead to 0 when playback stops or is stopped
This commit is contained in:
2026-05-24 22:32:48 -07:00
parent 78cd21c957
commit 0c64202670
+37
View File
@@ -102,6 +102,10 @@ typedef struct {
bool loaded; bool loaded;
bool stftComputed; bool stftComputed;
// Playback state
float playheadT; // 0-1 normalized position in selection
float playheadElapsed; // Elapsed seconds since play started
// Time selection (0-1 normalized) // Time selection (0-1 normalized)
float timeSelectionStart; float timeSelectionStart;
float timeSelectionEnd; float timeSelectionEnd;
@@ -1238,6 +1242,28 @@ static void DrawSelectionDrag(Rectangle bounds)
DrawRectangleLinesEx((Rectangle){ x, y, w, h }, 2, YELLOW); DrawRectangleLinesEx((Rectangle){ x, y, w, h }, 2, YELLOW);
} }
// ============================================================================
// Playhead
// ============================================================================
static void DrawPlayhead(Rectangle bounds)
{
if (!app.isPlaying || app.playheadT < 0.0f || app.playheadT > 1.0f) return;
float timePos = app.timeSelectionStart + app.playheadT * (app.timeSelectionEnd - app.timeSelectionStart);
float viewWidth = app.viewEnd - app.viewStart;
float t = (timePos - app.viewStart) / viewWidth;
float x = bounds.x + t * bounds.width;
// Clamp to bounds
x = fmaxf(bounds.x, fminf(bounds.x + bounds.width, x));
// Draw vertical line
DrawLine(x, bounds.y, x, bounds.y + bounds.height, RED);
// Draw semi-transparent overlay to make it stand out
DrawRectangle(x - 2, bounds.y, 4, bounds.height, (Color){ 255, 0, 0, 60 });
}
// ============================================================================ // ============================================================================
// PNG Export // PNG Export
// ============================================================================ // ============================================================================
@@ -1416,6 +1442,8 @@ static void DrawSidebar(void)
if (app.isPlaying && AudioPlaybackSound.frameCount > 0) { if (app.isPlaying && AudioPlaybackSound.frameCount > 0) {
StopSound(AudioPlaybackSound); StopSound(AudioPlaybackSound);
app.isPlaying = false; app.isPlaying = false;
app.playheadElapsed = 0;
app.playheadT = 0;
} else { } else {
PlaySelectedRegion(); PlaySelectedRegion();
app.isPlaying = true; app.isPlaying = true;
@@ -1665,6 +1693,12 @@ int main(int argc, char* argv[])
app.isPlaying = false; app.isPlaying = false;
app.playbackFinished = true; app.playbackFinished = true;
} }
// Track playhead position manually
app.playheadElapsed += GetFrameTime();
float selectionDuration = (app.timeSelectionEnd - app.timeSelectionStart) * app.signal.duration;
if (selectionDuration > 0) {
app.playheadT = app.playheadElapsed / selectionDuration;
}
} }
// Handle window resize // Handle window resize
@@ -1828,6 +1862,8 @@ int main(int argc, char* argv[])
StopSound(AudioPlaybackSound); StopSound(AudioPlaybackSound);
app.isPlaying = false; app.isPlaying = false;
app.playbackFinished = false; app.playbackFinished = false;
app.playheadElapsed = 0;
app.playheadT = 0;
} else if (app.playbackFinished) { } else if (app.playbackFinished) {
// Playback finished naturally - restart from beginning // Playback finished naturally - restart from beginning
PlaySelectedRegion(); PlaySelectedRegion();
@@ -2233,6 +2269,7 @@ int main(int argc, char* argv[])
if (app.showGrid) DrawSpectrogramGrid(viewBounds, 10, 8, Fade(GRAY, 0.3f)); if (app.showGrid) DrawSpectrogramGrid(viewBounds, 10, 8, Fade(GRAY, 0.3f));
DrawSelection(viewBounds); DrawSelection(viewBounds);
DrawSelectionDrag(viewBounds); DrawSelectionDrag(viewBounds);
DrawPlayhead(viewBounds);
DrawLabels(viewBounds); DrawLabels(viewBounds);
float maxFreq = (float)app.signal.sampleRate / 2.0f; float maxFreq = (float)app.signal.sampleRate / 2.0f;
float freqMin = app.freqViewStart * maxFreq; float freqMin = app.freqViewStart * maxFreq;