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:
@@ -44,10 +44,15 @@ format.
|
|||||||
- **mLnL annotation overlay** — labelled boxes from the WAV's embedded annotation
|
- **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,
|
chunk; hover a box (or its region on the scope) for per-frame detail (sequence,
|
||||||
channel, rate, scheduling offset…).
|
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.
|
- **Region selection** — box a time *and* frequency range with the mouse.
|
||||||
- **Filtered playback** — play just the selected region, band-limited to the
|
- **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.
|
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.
|
- **Waveform scope** — toggleable time-domain view beneath the spectrum.
|
||||||
- **Marker / ruler** and a **spectrum slice (PSD)** readout.
|
- **Marker / ruler** and a **spectrum slice (PSD)** readout.
|
||||||
- **Export** — save the view as a PNG, or the selected region as a WAV.
|
- **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 |
|
| **Ctrl+wheel** | Zoom the frequency axis only |
|
||||||
| **Wheel on a scrollbar** | Pan that axis (**Shift** to zoom it) |
|
| **Wheel on a scrollbar** | Pan that axis (**Shift** to zoom it) |
|
||||||
| **Space** | Play / stop the selected region |
|
| **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 |
|
| **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 |
|
| **N** / **Shift+N** | Jump to the next / previous collision |
|
||||||
| **O** | Open file browser |
|
| **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
|
overlays toggled); panning and zooming just move the rectangle drawn on top, so
|
||||||
navigation costs nothing.
|
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
|
### Inspecting overlapping transmissions
|
||||||
|
|
||||||
When several stations are on the air at once their annotation boxes stack, and
|
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)
|
## Driving the GUI headlessly (agents / CI)
|
||||||
|
|
||||||
The app can be run, screenshotted, and clicked on a virtual X display with no
|
The app runs, screenshots, and takes synthetic input on a virtual X display with
|
||||||
monitor or GPU (Mesa software GL under Xvfb). The full playbook lives in
|
no monitor or GPU (Mesa software GL under Xvfb). The playbook is in
|
||||||
[`AGENTS.md`](AGENTS.md); the working reference implementation is
|
[`AGENTS.md`](AGENTS.md); [`shot_input.sh`](shot_input.sh) is the working
|
||||||
[`shot_input.sh`](shot_input.sh).
|
reference implementation.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+20
-2
@@ -195,8 +195,16 @@ static float* BuildSelectionAudio(int* outNumSamples)
|
|||||||
{
|
{
|
||||||
if (!app.loaded || !app.stftComputed) return NULL;
|
if (!app.loaded || !app.stftComputed) return NULL;
|
||||||
|
|
||||||
int startSample = (int)(app.sel.timeStart * app.signal.numSamples);
|
// playFromT lets playback begin partway into the selection (the user
|
||||||
int endSample = (int)(app.sel.timeEnd * app.signal.numSamples);
|
// scrubbed the playhead) without disturbing the selection itself — the
|
||||||
|
// whole point is to replay a subrange while keeping the region intact.
|
||||||
|
float t0 = app.sel.timeStart;
|
||||||
|
float t1 = app.sel.timeEnd;
|
||||||
|
if (app.playFromT > 0.0f && app.playFromT < 1.0f)
|
||||||
|
t0 = t0 + app.playFromT * (t1 - t0);
|
||||||
|
|
||||||
|
int startSample = (int)(t0 * app.signal.numSamples);
|
||||||
|
int endSample = (int)(t1 * app.signal.numSamples);
|
||||||
int numSamples = endSample - startSample;
|
int numSamples = endSample - startSample;
|
||||||
if (numSamples <= 0 || startSample < 0 || endSample > app.signal.numSamples) return NULL;
|
if (numSamples <= 0 || startSample < 0 || endSample > app.signal.numSamples) return NULL;
|
||||||
|
|
||||||
@@ -246,10 +254,20 @@ void PlaySelectedRegion(void)
|
|||||||
// Snapshot what we're about to play so the playhead tracks THIS region even
|
// 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
|
// if the user moves the selection mid-playback. Duration comes from the
|
||||||
// buffer we actually built, not from app.signal.duration.
|
// buffer we actually built, not from app.signal.duration.
|
||||||
|
// Snapshot the span actually handed to the device, which is the selection
|
||||||
|
// narrowed by any scrub offset — the playhead is measured against this.
|
||||||
app.playSelStart = app.sel.timeStart;
|
app.playSelStart = app.sel.timeStart;
|
||||||
app.playSelEnd = app.sel.timeEnd;
|
app.playSelEnd = app.sel.timeEnd;
|
||||||
|
if (app.playFromT > 0.0f && app.playFromT < 1.0f)
|
||||||
|
app.playSelStart += app.playFromT * (app.sel.timeEnd - app.sel.timeStart);
|
||||||
app.playDuration = (app.signal.sampleRate > 0)
|
app.playDuration = (app.signal.sampleRate > 0)
|
||||||
? (float)numSamples / (float)app.signal.sampleRate : 0.0f;
|
? (float)numSamples / (float)app.signal.sampleRate : 0.0f;
|
||||||
|
// Anchor the playhead to the wall clock (see playStartTime). Set here, at
|
||||||
|
// the moment the buffer is handed to the device, so the marker can't drift
|
||||||
|
// from the audio no matter how long a frame takes.
|
||||||
|
app.playStartTime = GetTime();
|
||||||
|
app.playheadSeekT = 0.0f;
|
||||||
|
app.playheadT = 0.0f;
|
||||||
|
|
||||||
EnsureAudioDevice(); // opened on demand; released again once playback ends
|
EnsureAudioDevice(); // opened on demand; released again once playback ends
|
||||||
if (AudioPlaybackSound.frameCount != 0) UnloadSound(AudioPlaybackSound);
|
if (AudioPlaybackSound.frameCount != 0) UnloadSound(AudioPlaybackSound);
|
||||||
|
|||||||
+206
-8
@@ -2,6 +2,7 @@
|
|||||||
#include "render.h"
|
#include "render.h"
|
||||||
#include "stft.h" // ComputeSpectralStats for the selection panel
|
#include "stft.h" // ComputeSpectralStats for the selection panel
|
||||||
#include "utils.h" // ComputeSignalStats
|
#include "utils.h" // ComputeSignalStats
|
||||||
|
#include "audio.h" // PlaySelectedRegion for the selection transport
|
||||||
|
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -579,6 +580,181 @@ static void DrawStatPanel(Rectangle bounds, Rectangle sel)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ===== Selection transport bar =====
|
||||||
|
// Play controls attached to the selection box, so the common actions are where
|
||||||
|
// the user is already looking instead of over on the rail. Appears only when a
|
||||||
|
// region is selected.
|
||||||
|
//
|
||||||
|
// Geometry is split from drawing (SelectionTransportRect / TransportButtonRect)
|
||||||
|
// because input handling runs long before the draw pass, and a click here must
|
||||||
|
// be claimed before the pan/select handlers see it.
|
||||||
|
|
||||||
|
// Number of buttons: rewind-to-start, play/pause, stop, loop.
|
||||||
|
#define TRANSPORT_BUTTONS 4
|
||||||
|
|
||||||
|
Rectangle SelectionTransportRect(Rectangle bounds)
|
||||||
|
{
|
||||||
|
bool hasSelection = (app.sel.timeStart > 0.001f || app.sel.timeEnd < 0.999f ||
|
||||||
|
app.sel.freqStart > 0.001f || app.sel.freqEnd < 0.999f);
|
||||||
|
if (!app.loaded || !hasSelection || app.sel.isTimeSelecting || app.sel.isDragging)
|
||||||
|
return (Rectangle){ 0, 0, 0, 0 };
|
||||||
|
|
||||||
|
float scale = GetUIScale();
|
||||||
|
float bw = 26.0f * scale; // per-button
|
||||||
|
float w = bw * TRANSPORT_BUTTONS + 8.0f * scale;
|
||||||
|
float h = 24.0f * scale;
|
||||||
|
|
||||||
|
float viewWidth = app.view.end - app.view.start;
|
||||||
|
if (viewWidth <= 0.0f) return (Rectangle){ 0, 0, 0, 0 };
|
||||||
|
float x0 = bounds.x + ((app.sel.timeStart - app.view.start) / viewWidth) * bounds.width;
|
||||||
|
float x1 = bounds.x + ((app.sel.timeEnd - app.view.start) / viewWidth) * bounds.width;
|
||||||
|
|
||||||
|
float freqWidth = app.view.freqEnd - app.view.freqStart;
|
||||||
|
float yTop = bounds.y + bounds.height -
|
||||||
|
((app.sel.freqEnd - app.view.freqStart) / freqWidth) * bounds.height;
|
||||||
|
|
||||||
|
// Centred on the selection, just above its top edge; flipped below when
|
||||||
|
// there isn't room, and clamped so it stays inside the viewport.
|
||||||
|
float cx = (fmaxf(x0, bounds.x) + fminf(x1, bounds.x + bounds.width)) * 0.5f;
|
||||||
|
float bx = cx - w * 0.5f;
|
||||||
|
float by = yTop - h - 6.0f * scale;
|
||||||
|
if (by < bounds.y + 2.0f) by = yTop + 6.0f * scale;
|
||||||
|
if (by + h > bounds.y + bounds.height) by = bounds.y + bounds.height - h - 2.0f;
|
||||||
|
if (bx < bounds.x) bx = bounds.x;
|
||||||
|
if (bx + w > bounds.x + bounds.width) bx = bounds.x + bounds.width - w;
|
||||||
|
|
||||||
|
// Off-screen selection: nothing to attach to.
|
||||||
|
if (x1 < bounds.x || x0 > bounds.x + bounds.width) return (Rectangle){ 0, 0, 0, 0 };
|
||||||
|
return (Rectangle){ bx, by, w, h };
|
||||||
|
}
|
||||||
|
|
||||||
|
static Rectangle TransportButtonRect(Rectangle bar, int i)
|
||||||
|
{
|
||||||
|
float scale = GetUIScale();
|
||||||
|
float bw = 26.0f * scale;
|
||||||
|
return (Rectangle){ bar.x + 4.0f * scale + i * bw, bar.y + 2.0f * scale,
|
||||||
|
bw, bar.height - 4.0f * scale };
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TransportCapturesMouse(Rectangle bounds)
|
||||||
|
{
|
||||||
|
Rectangle bar = SelectionTransportRect(bounds);
|
||||||
|
if (bar.width <= 0.0f) return false;
|
||||||
|
return CheckCollisionPointRec(GetMousePosition(), bar);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act on a click. Separate from drawing so it runs during the input phase.
|
||||||
|
void UpdateSelectionTransport(Rectangle bounds)
|
||||||
|
{
|
||||||
|
Rectangle bar = SelectionTransportRect(bounds);
|
||||||
|
if (bar.width <= 0.0f) return;
|
||||||
|
if (!IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) return;
|
||||||
|
|
||||||
|
Vector2 m = GetMousePosition();
|
||||||
|
for (int i = 0; i < TRANSPORT_BUTTONS; i++) {
|
||||||
|
if (!CheckCollisionPointRec(m, TransportButtonRect(bar, i))) continue;
|
||||||
|
switch (i) {
|
||||||
|
case 0: // rewind to the start of the selection
|
||||||
|
app.playFromT = 0.0f;
|
||||||
|
app.playheadT = 0.0f;
|
||||||
|
if (app.isPlaying) {
|
||||||
|
StopSound(AudioPlaybackSound);
|
||||||
|
PlaySelectedRegion();
|
||||||
|
app.playbackFinished = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 1: // play / pause
|
||||||
|
if (app.isPlaying) {
|
||||||
|
// Pause: remember where we are so play resumes from here.
|
||||||
|
float stopAbs = app.playSelStart +
|
||||||
|
app.playheadT * (app.playSelEnd - app.playSelStart);
|
||||||
|
float selSpan = app.sel.timeEnd - app.sel.timeStart;
|
||||||
|
app.playFromT = (selSpan > 1e-6f)
|
||||||
|
? Clamp((stopAbs - app.sel.timeStart) / selSpan, 0.0f, 0.999f)
|
||||||
|
: 0.0f;
|
||||||
|
StopSound(AudioPlaybackSound);
|
||||||
|
app.isPlaying = false;
|
||||||
|
app.playbackFinished = false;
|
||||||
|
} else {
|
||||||
|
PlaySelectedRegion();
|
||||||
|
app.isPlaying = true;
|
||||||
|
app.playbackFinished = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 2: // stop: halt and rewind
|
||||||
|
if (app.isPlaying) StopSound(AudioPlaybackSound);
|
||||||
|
app.isPlaying = false;
|
||||||
|
app.playbackFinished = false;
|
||||||
|
app.playFromT = 0.0f;
|
||||||
|
app.playheadT = 0.0f;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
app.loopPlayback = !app.loopPlayback;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawSelectionTransport(Rectangle bounds)
|
||||||
|
{
|
||||||
|
Rectangle bar = SelectionTransportRect(bounds);
|
||||||
|
if (bar.width <= 0.0f) return;
|
||||||
|
|
||||||
|
float scale = GetUIScale();
|
||||||
|
Vector2 m = GetMousePosition();
|
||||||
|
|
||||||
|
DrawRectangleRec(bar, (Color){ 26, 26, 32, 235 });
|
||||||
|
DrawRectangleLinesEx(bar, 1, (Color){ 110, 110, 130, 255 });
|
||||||
|
|
||||||
|
for (int i = 0; i < TRANSPORT_BUTTONS; i++) {
|
||||||
|
Rectangle b = TransportButtonRect(bar, i);
|
||||||
|
bool over = CheckCollisionPointRec(m, b);
|
||||||
|
bool on = (i == 3) ? app.loopPlayback : false;
|
||||||
|
if (over) DrawRectangleRec(b, (Color){ 60, 60, 76, 255 });
|
||||||
|
if (on) DrawRectangleRec(b, (Color){ 50, 80, 60, 255 });
|
||||||
|
|
||||||
|
Color c = on ? (Color){ 150, 230, 170, 255 } : (Color){ 210, 210, 220, 255 };
|
||||||
|
float cx = b.x + b.width * 0.5f, cy = b.y + b.height * 0.5f;
|
||||||
|
float s = b.height * 0.26f;
|
||||||
|
|
||||||
|
switch (i) {
|
||||||
|
case 0: // |< rewind to start
|
||||||
|
// Bar on the left, triangle pointing INTO it. raylib wants
|
||||||
|
// counter-clockwise vertices; the previous winding put the apex
|
||||||
|
// to the right of the base, so the triangle never rendered and
|
||||||
|
// this read as a lone vertical bar.
|
||||||
|
DrawRectangleRec((Rectangle){ cx - s, cy - s, 2.0f * scale, s * 2 }, c);
|
||||||
|
DrawTriangle((Vector2){ cx - s + 3 * scale, cy },
|
||||||
|
(Vector2){ cx + s, cy + s },
|
||||||
|
(Vector2){ cx + s, cy - s }, c);
|
||||||
|
break;
|
||||||
|
case 1: // play / pause
|
||||||
|
if (app.isPlaying) {
|
||||||
|
DrawRectangleRec((Rectangle){ cx - s, cy - s, s * 0.7f, s * 2 }, c);
|
||||||
|
DrawRectangleRec((Rectangle){ cx + s * 0.3f, cy - s, s * 0.7f, s * 2 }, c);
|
||||||
|
} else {
|
||||||
|
DrawTriangle((Vector2){ cx - s * 0.7f, cy - s },
|
||||||
|
(Vector2){ cx - s * 0.7f, cy + s },
|
||||||
|
(Vector2){ cx + s, cy }, c);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 2: // stop
|
||||||
|
DrawRectangleRec((Rectangle){ cx - s * 0.85f, cy - s * 0.85f,
|
||||||
|
s * 1.7f, s * 1.7f }, c);
|
||||||
|
break;
|
||||||
|
case 3: // loop
|
||||||
|
DrawRectangleLinesEx((Rectangle){ cx - s, cy - s * 0.7f, s * 2, s * 1.4f },
|
||||||
|
1.4f, c);
|
||||||
|
DrawTriangle((Vector2){ cx + s * 0.2f, cy - s * 1.3f },
|
||||||
|
(Vector2){ cx + s * 0.2f, cy - s * 0.1f },
|
||||||
|
(Vector2){ cx + s * 1.1f, cy - s * 0.7f }, c);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void DrawSelection(Rectangle bounds)
|
void DrawSelection(Rectangle bounds)
|
||||||
{
|
{
|
||||||
// Only draw if selection is not full range AND not currently dragging
|
// Only draw if selection is not full range AND not currently dragging
|
||||||
@@ -1395,7 +1571,11 @@ void DrawAnnotations(Rectangle bounds)
|
|||||||
bool mouseInBounds = CheckCollisionPointRec(m, bounds);
|
bool mouseInBounds = CheckCollisionPointRec(m, bounds);
|
||||||
bool suppressHover = app.sel.isTimeSelecting || app.sel.isFreqSelecting ||
|
bool suppressHover = app.sel.isTimeSelecting || app.sel.isFreqSelecting ||
|
||||||
app.sel.isDragging || app.view.isPanning || app.marker.dragging ||
|
app.sel.isDragging || app.view.isPanning || app.marker.dragging ||
|
||||||
app.markerMode;
|
app.markerMode ||
|
||||||
|
// The transport bar floats over the spectrogram; an
|
||||||
|
// annotation tooltip firing underneath it would cover
|
||||||
|
// the buttons being reached for.
|
||||||
|
TransportCapturesMouse(bounds);
|
||||||
|
|
||||||
int hoverEvent = -1;
|
int hoverEvent = -1;
|
||||||
// hover prefers later layers (assertions over bursts, point markers over both),
|
// hover prefers later layers (assertions over bursts, point markers over both),
|
||||||
@@ -2316,21 +2496,39 @@ void DrawTimeline(Rectangle lane)
|
|||||||
// Playhead
|
// Playhead
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
void DrawPlayhead(Rectangle bounds)
|
// Screen X of the playhead within `bounds`, or a negative value when there is
|
||||||
|
// nothing to draw. Shared with the scrub hit-test so the marker and its grab
|
||||||
|
// region can't disagree about where it is.
|
||||||
|
float PlayheadScreenX(Rectangle bounds)
|
||||||
{
|
{
|
||||||
if (!app.isPlaying || app.playheadT < 0.0f || app.playheadT > 1.0f) return;
|
if (!app.loaded || app.playheadT < 0.0f || app.playheadT > 1.0f) return -1.0f;
|
||||||
|
if (app.playSelEnd <= app.playSelStart) return -1.0f;
|
||||||
|
|
||||||
// Against the snapshot of the playing region, not the live selection.
|
// Against the snapshot of the playing region, not the live selection.
|
||||||
float timePos = app.playSelStart + app.playheadT * (app.playSelEnd - app.playSelStart);
|
float timePos = app.playSelStart + app.playheadT * (app.playSelEnd - app.playSelStart);
|
||||||
float viewWidth = app.view.end - app.view.start;
|
float viewWidth = app.view.end - app.view.start;
|
||||||
|
if (viewWidth <= 0.0f) return -1.0f;
|
||||||
float t = (timePos - app.view.start) / viewWidth;
|
float t = (timePos - app.view.start) / viewWidth;
|
||||||
float x = bounds.x + t * bounds.width;
|
return bounds.x + t * bounds.width;
|
||||||
|
}
|
||||||
|
|
||||||
// Clamp to bounds
|
void DrawPlayhead(Rectangle bounds)
|
||||||
|
{
|
||||||
|
// Drawn while stopped too, so the marker stays put and can be dragged to
|
||||||
|
// replay a subrange without redoing the selection.
|
||||||
|
float x = PlayheadScreenX(bounds);
|
||||||
|
if (x < 0.0f) return;
|
||||||
|
if (x < bounds.x - 4 || x > bounds.x + bounds.width + 4) return;
|
||||||
x = fmaxf(bounds.x, fminf(bounds.x + bounds.width, x));
|
x = fmaxf(bounds.x, fminf(bounds.x + bounds.width, x));
|
||||||
|
|
||||||
// Draw vertical line
|
Color c = app.isPlaying ? RED : (Color){ 255, 120, 120, 255 };
|
||||||
DrawLine(x, bounds.y, x, bounds.y + bounds.height, RED);
|
DrawLine(x, bounds.y, x, bounds.y + bounds.height, c);
|
||||||
// Draw semi-transparent overlay to make it stand out
|
|
||||||
DrawRectangle(x - 2, bounds.y, 4, bounds.height, (Color){ 255, 0, 0, 60 });
|
DrawRectangle(x - 2, bounds.y, 4, bounds.height, (Color){ 255, 0, 0, 60 });
|
||||||
|
|
||||||
|
// Grab tab at the top, so the marker reads as draggable when stopped.
|
||||||
|
if (!app.isPlaying) {
|
||||||
|
DrawTriangle((Vector2){ x - 5, bounds.y },
|
||||||
|
(Vector2){ x + 5, bounds.y },
|
||||||
|
(Vector2){ x, bounds.y + 8 }, c);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,11 +45,23 @@ void DrawAnnotationsToImage(Image* img, Font font);
|
|||||||
void DrawSpectrogramGrid(Rectangle bounds, int numCellsX, int numCellsY, Color color);
|
void DrawSpectrogramGrid(Rectangle bounds, int numCellsX, int numCellsY, Color color);
|
||||||
void DrawLabels(Rectangle bounds);
|
void DrawLabels(Rectangle bounds);
|
||||||
void DrawSelection(Rectangle bounds);
|
void DrawSelection(Rectangle bounds);
|
||||||
|
|
||||||
|
// --- Selection transport bar ---
|
||||||
|
// Play controls attached to the selection box. Geometry and input are split
|
||||||
|
// from drawing so a click can be claimed during the input phase, before the
|
||||||
|
// pan/select handlers run.
|
||||||
|
Rectangle SelectionTransportRect(Rectangle bounds);
|
||||||
|
bool TransportCapturesMouse(Rectangle bounds);
|
||||||
|
void UpdateSelectionTransport(Rectangle bounds);
|
||||||
|
void DrawSelectionTransport(Rectangle bounds);
|
||||||
void DrawSelectionDrag(Rectangle bounds);
|
void DrawSelectionDrag(Rectangle bounds);
|
||||||
void DrawCursorReadout(Rectangle bounds);
|
void DrawCursorReadout(Rectangle bounds);
|
||||||
void DrawMarkers(Rectangle bounds);
|
void DrawMarkers(Rectangle bounds);
|
||||||
void DrawSpectrumPanel(Rectangle bounds);
|
void DrawSpectrumPanel(Rectangle bounds);
|
||||||
void DrawPlayhead(Rectangle bounds);
|
void DrawPlayhead(Rectangle bounds);
|
||||||
|
// Playhead X within bounds, or <0 when there's nothing to draw. Used by the
|
||||||
|
// scrub hit-test so the marker and its grab region agree.
|
||||||
|
float PlayheadScreenX(Rectangle bounds);
|
||||||
void DrawAnnotations(Rectangle bounds);
|
void DrawAnnotations(Rectangle bounds);
|
||||||
|
|
||||||
// --- Minimap ---
|
// --- Minimap ---
|
||||||
|
|||||||
+96
-34
@@ -292,7 +292,7 @@ void ResetForNewSignal(void)
|
|||||||
if (app.isPlaying && AudioPlaybackSound.frameCount > 0) StopSound(AudioPlaybackSound);
|
if (app.isPlaying && AudioPlaybackSound.frameCount > 0) StopSound(AudioPlaybackSound);
|
||||||
app.isPlaying = false;
|
app.isPlaying = false;
|
||||||
app.playbackFinished = false;
|
app.playbackFinished = false;
|
||||||
app.playheadElapsed = 0.0f;
|
app.playheadSeekT = 0.0f;
|
||||||
app.playheadT = 0.0f;
|
app.playheadT = 0.0f;
|
||||||
|
|
||||||
// Invalidate the cached visible texture.
|
// Invalidate the cached visible texture.
|
||||||
@@ -1097,6 +1097,11 @@ int main(int argc, char* argv[])
|
|||||||
{
|
{
|
||||||
g_frameCounter++;
|
g_frameCounter++;
|
||||||
|
|
||||||
|
// Cursor is decided once per frame: handlers below record a request and
|
||||||
|
// the highest-priority one is applied at the end. Reset here.
|
||||||
|
app.cursorRequest = MOUSE_CURSOR_DEFAULT;
|
||||||
|
app.cursorPriority = -1;
|
||||||
|
|
||||||
// Set when the window is in the background but still has compute to
|
// Set when the window is in the background but still has compute to
|
||||||
// finish: the frame runs its logic and skips presenting. See the
|
// finish: the frame runs its logic and skips presenting. See the
|
||||||
// power-management block below.
|
// power-management block below.
|
||||||
@@ -1199,17 +1204,29 @@ int main(int argc, char* argv[])
|
|||||||
if (app.isPlaying && AudioPlaybackSound.frameCount > 0) {
|
if (app.isPlaying && AudioPlaybackSound.frameCount > 0) {
|
||||||
// Check if sound stopped playing (IsSoundPlaying returns false when done)
|
// Check if sound stopped playing (IsSoundPlaying returns false when done)
|
||||||
if (!IsSoundPlaying(AudioPlaybackSound)) {
|
if (!IsSoundPlaying(AudioPlaybackSound)) {
|
||||||
app.isPlaying = false;
|
if (app.loopPlayback) {
|
||||||
app.playbackFinished = true;
|
// Restart from the top of the selection, ignoring any scrub
|
||||||
|
// offset: a loop should repeat the whole region, not the
|
||||||
|
// tail the last play happened to start from.
|
||||||
|
app.playFromT = 0.0f;
|
||||||
|
PlaySelectedRegion();
|
||||||
|
app.isPlaying = true;
|
||||||
|
app.playbackFinished = false;
|
||||||
|
} else {
|
||||||
|
app.isPlaying = false;
|
||||||
|
app.playbackFinished = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Track playhead position manually, against the length of the buffer
|
// Derive the playhead from the wall clock, not from summed frame
|
||||||
// that's actually playing (see playDuration) rather than a length
|
// times. Audio plays on its own clock; accumulating GetFrameTime()
|
||||||
// re-derived from the live selection — the user can move the
|
// counted every frame's render work as playback time, so the marker
|
||||||
// selection mid-playback without the marker jumping.
|
// ran ahead — by seconds on a long region, and worst when zoomed out
|
||||||
app.playheadElapsed += GetFrameTime();
|
// where each frame does the most work.
|
||||||
if (app.playDuration > 0.0f) {
|
if (app.playDuration > 0.0f) {
|
||||||
app.playheadT = app.playheadElapsed / app.playDuration;
|
double elapsed = GetTime() - app.playStartTime;
|
||||||
|
app.playheadT = app.playheadSeekT + (float)(elapsed / app.playDuration);
|
||||||
if (app.playheadT > 1.0f) app.playheadT = 1.0f;
|
if (app.playheadT > 1.0f) app.playheadT = 1.0f;
|
||||||
|
if (app.playheadT < 0.0f) app.playheadT = 0.0f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1233,6 +1250,43 @@ int main(int argc, char* argv[])
|
|||||||
float spectroHeight = L.spectroHeight;
|
float spectroHeight = L.spectroHeight;
|
||||||
Rectangle viewBounds = L.viewBounds;
|
Rectangle viewBounds = L.viewBounds;
|
||||||
|
|
||||||
|
// Selection transport. Claims its clicks before the pan/select
|
||||||
|
// handlers below, which are gated on TransportCapturesMouse().
|
||||||
|
UpdateSelectionTransport(viewBounds);
|
||||||
|
|
||||||
|
// Playhead scrub. Only while stopped: dragging the marker sets
|
||||||
|
// where the next play starts within the selection, so a subrange
|
||||||
|
// can be replayed without redrawing the region. Claims the press
|
||||||
|
// ahead of the pan handler, which is gated on playheadDragging.
|
||||||
|
if (!app.isPlaying && app.loaded && app.playSelEnd > app.playSelStart) {
|
||||||
|
float phx = PlayheadScreenX(viewBounds);
|
||||||
|
Vector2 pm = GetMousePosition();
|
||||||
|
bool nearPlayhead = phx >= 0.0f && fabsf(pm.x - phx) <= 6.0f * viewScale &&
|
||||||
|
CheckCollisionPointRec(pm, viewBounds);
|
||||||
|
if (nearPlayhead && IsMouseButtonPressed(MOUSE_LEFT_BUTTON))
|
||||||
|
app.playheadDragging = true;
|
||||||
|
if (!IsMouseButtonDown(MOUSE_LEFT_BUTTON)) app.playheadDragging = false;
|
||||||
|
|
||||||
|
if (app.playheadDragging) {
|
||||||
|
RequestCursor(MOUSE_CURSOR_RESIZE_EW, CURSOR_PRI_ACTIVE);
|
||||||
|
// Screen X -> time -> fraction of the SELECTION, which is
|
||||||
|
// what playFromT means.
|
||||||
|
float vt = (pm.x - viewBounds.x) / viewBounds.width;
|
||||||
|
float tAbs = app.view.start + vt * (app.view.end - app.view.start);
|
||||||
|
float selSpan = app.sel.timeEnd - app.sel.timeStart;
|
||||||
|
if (selSpan > 1e-6f) {
|
||||||
|
app.playFromT = Clamp((tAbs - app.sel.timeStart) / selSpan, 0.0f, 1.0f);
|
||||||
|
// Keep the drawn marker under the cursor: it is measured
|
||||||
|
// against playSelStart/End, not the selection.
|
||||||
|
app.playSelStart = app.sel.timeStart;
|
||||||
|
app.playSelEnd = app.sel.timeEnd;
|
||||||
|
app.playheadT = app.playFromT;
|
||||||
|
}
|
||||||
|
} else if (nearPlayhead) {
|
||||||
|
RequestCursor(MOUSE_CURSOR_RESIZE_EW, CURSOR_PRI_HOVER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Minimap scrub. Runs BEFORE the pan/zoom handlers below so a press
|
// Minimap scrub. Runs BEFORE the pan/zoom handlers below so a press
|
||||||
// inside the minimap is claimed here; those handlers are gated on
|
// inside the minimap is claimed here; those handlers are gated on
|
||||||
// MinimapCapturesMouse() and will skip it. Both writing app.view in
|
// MinimapCapturesMouse() and will skip it. Both writing app.view in
|
||||||
@@ -1335,7 +1389,8 @@ int main(int argc, char* argv[])
|
|||||||
// Never start a pan on a click that belongs to the UI chrome.
|
// Never start a pan on a click that belongs to the UI chrome.
|
||||||
bool overView = CheckCollisionPointRec(GetMousePosition(), viewBounds) &&
|
bool overView = CheckCollisionPointRec(GetMousePosition(), viewBounds) &&
|
||||||
!MenubarCapturesMouse() && !SidebarCapturesMouse() &&
|
!MenubarCapturesMouse() && !SidebarCapturesMouse() &&
|
||||||
!MinimapCapturesMouse();
|
!MinimapCapturesMouse() && !app.playheadDragging &&
|
||||||
|
!TransportCapturesMouse(viewBounds);
|
||||||
if (overView && ((canPan && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) ||
|
if (overView && ((canPan && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) ||
|
||||||
IsMouseButtonPressed(MOUSE_BUTTON_MIDDLE))) {
|
IsMouseButtonPressed(MOUSE_BUTTON_MIDDLE))) {
|
||||||
app.view.isPanning = true;
|
app.view.isPanning = true;
|
||||||
@@ -1574,8 +1629,20 @@ 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;
|
// Leave the marker where playback stopped rather than snapping it
|
||||||
app.playheadT = 0;
|
// to the start: it stays grabbable, and the next play resumes
|
||||||
|
// from here. playheadT is a fraction of the PLAYED span, which is
|
||||||
|
// not the selection once a scrub offset is in play — convert
|
||||||
|
// through absolute file time, the only frame the two share.
|
||||||
|
{
|
||||||
|
float stopAbs = app.playSelStart +
|
||||||
|
app.playheadT * (app.playSelEnd - app.playSelStart);
|
||||||
|
float selSpan = app.sel.timeEnd - app.sel.timeStart;
|
||||||
|
app.playFromT = (selSpan > 1e-6f)
|
||||||
|
? Clamp((stopAbs - app.sel.timeStart) / selSpan, 0.0f, 0.999f)
|
||||||
|
: 0.0f;
|
||||||
|
}
|
||||||
|
app.playheadSeekT = 0.0f;
|
||||||
} else if (app.playbackFinished) {
|
} else if (app.playbackFinished) {
|
||||||
// Playback finished naturally - restart from beginning
|
// Playback finished naturally - restart from beginning
|
||||||
PlaySelectedRegion();
|
PlaySelectedRegion();
|
||||||
@@ -1622,7 +1689,8 @@ int main(int argc, char* argv[])
|
|||||||
// Parking the cursor off-screen is enough to make every hit test below
|
// Parking the cursor off-screen is enough to make every hit test below
|
||||||
// miss without threading a flag through each one.
|
// miss without threading a flag through each one.
|
||||||
if (MenubarCapturesMouse() || SidebarCapturesMouse() ||
|
if (MenubarCapturesMouse() || SidebarCapturesMouse() ||
|
||||||
MinimapCapturesMouse()) mousePos = (Vector2){ -1000, -1000 };
|
MinimapCapturesMouse() || app.playheadDragging ||
|
||||||
|
TransportCapturesMouse(selBounds)) mousePos = (Vector2){ -1000, -1000 };
|
||||||
|
|
||||||
|
|
||||||
// Calculate divider screen position (for hover detection)
|
// Calculate divider screen position (for hover detection)
|
||||||
@@ -1670,39 +1738,28 @@ int main(int argc, char* argv[])
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set cursor based on context
|
|
||||||
if (app.sel.isDragging) {
|
|
||||||
SetMouseCursor(MOUSE_CURSOR_RESIZE_ALL); // 4-way arrow while dragging
|
|
||||||
} else if (hoverInsideSelection) {
|
|
||||||
SetMouseCursor(MOUSE_CURSOR_POINTING_HAND); // Pointing hand on hover
|
|
||||||
} else {
|
|
||||||
SetMouseCursor(MOUSE_CURSOR_DEFAULT); // Normal arrow
|
|
||||||
}
|
|
||||||
|
|
||||||
// LMB drag = box select (time + frequency) OR drag existing selection
|
// LMB drag = box select (time + frequency) OR drag existing selection
|
||||||
if (app.loaded && !UiModalOpen() && CheckCollisionPointRec(mousePos, selBounds)) {
|
if (app.loaded && !UiModalOpen() && CheckCollisionPointRec(mousePos, selBounds)) {
|
||||||
// Set cursor to resize all when near divider
|
// Cursor for the spectrogram area. One decision, stated as a
|
||||||
if (mouseNearDivider && !app.isDividing) {
|
// request — a second block used to run unconditionally before this
|
||||||
SetMouseCursor(MOUSE_CURSOR_RESIZE_ALL);
|
// one and the two disagreed, which is what made the shape flicker.
|
||||||
} else if (app.sel.isDragging) {
|
if (app.sel.isDragging || app.view.isPanning) {
|
||||||
SetMouseCursor(MOUSE_CURSOR_RESIZE_ALL); // 4-way arrow while dragging
|
RequestCursor(MOUSE_CURSOR_RESIZE_ALL, CURSOR_PRI_ACTIVE);
|
||||||
|
} else if (mouseNearDivider && !app.isDividing) {
|
||||||
|
RequestCursor(MOUSE_CURSOR_RESIZE_NS, CURSOR_PRI_HOVER);
|
||||||
} else if (hoverInsideSelection && selSelects) {
|
} else if (hoverInsideSelection && selSelects) {
|
||||||
SetMouseCursor(MOUSE_CURSOR_POINTING_HAND); // Pointing hand on hover
|
RequestCursor(MOUSE_CURSOR_POINTING_HAND, CURSOR_PRI_HOVER);
|
||||||
} else if (selSelects) {
|
} else if (selSelects) {
|
||||||
// Crosshair whenever a left-drag would draw a box, so the two
|
// Crosshair whenever a left-drag would draw a box, so the two
|
||||||
// gestures are distinguishable without guessing.
|
// gestures are distinguishable without guessing.
|
||||||
SetMouseCursor(MOUSE_CURSOR_CROSSHAIR);
|
RequestCursor(MOUSE_CURSOR_CROSSHAIR, CURSOR_PRI_HOVER);
|
||||||
} else if (app.view.isPanning) {
|
|
||||||
SetMouseCursor(MOUSE_CURSOR_RESIZE_ALL);
|
|
||||||
} else {
|
|
||||||
SetMouseCursor(MOUSE_CURSOR_DEFAULT); // Normal arrow
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (app.markerMode) {
|
if (app.markerMode) {
|
||||||
// Marker/ruler mode: LMB press drops point A, dragging moves B,
|
// Marker/ruler mode: LMB press drops point A, dragging moves B,
|
||||||
// release finalizes. Alt / middle-drag still pans (handled
|
// release finalizes. Alt / middle-drag still pans (handled
|
||||||
// above), so don't drop a marker while panning.
|
// above), so don't drop a marker while panning.
|
||||||
SetMouseCursor(MOUSE_CURSOR_CROSSHAIR);
|
RequestCursor(MOUSE_CURSOR_CROSSHAIR, CURSOR_PRI_HOVER);
|
||||||
bool altPan = IsKeyDown(KEY_LEFT_ALT) || IsKeyDown(KEY_RIGHT_ALT) ||
|
bool altPan = IsKeyDown(KEY_LEFT_ALT) || IsKeyDown(KEY_RIGHT_ALT) ||
|
||||||
IsMouseButtonDown(MOUSE_BUTTON_MIDDLE);
|
IsMouseButtonDown(MOUSE_BUTTON_MIDDLE);
|
||||||
if (!altPan) {
|
if (!altPan) {
|
||||||
@@ -1734,6 +1791,7 @@ int main(int argc, char* argv[])
|
|||||||
app.sel.isTimeSelecting = true;
|
app.sel.isTimeSelecting = true;
|
||||||
app.sel.isFreqSelecting = true;
|
app.sel.isFreqSelecting = true;
|
||||||
app.sel.selectStartPos = mousePos;
|
app.sel.selectStartPos = mousePos;
|
||||||
|
app.playFromT = 0.0f; // new region plays from its start
|
||||||
|
|
||||||
// Convert screen position to signal coordinates (accounting for zoom)
|
// Convert screen position to signal coordinates (accounting for zoom)
|
||||||
float viewportT = (mousePos.x - selBounds.x) / selBounds.width;
|
float viewportT = (mousePos.x - selBounds.x) / selBounds.width;
|
||||||
@@ -2142,6 +2200,7 @@ int main(int argc, char* argv[])
|
|||||||
if (L.timelineHeight > 0) DrawTimeline(L.timelineBounds);
|
if (L.timelineHeight > 0) DrawTimeline(L.timelineBounds);
|
||||||
DrawAnnotations(viewBounds);
|
DrawAnnotations(viewBounds);
|
||||||
DrawSelection(viewBounds);
|
DrawSelection(viewBounds);
|
||||||
|
DrawSelectionTransport(viewBounds);
|
||||||
DrawSelectionDrag(viewBounds);
|
DrawSelectionDrag(viewBounds);
|
||||||
DrawMarkers(viewBounds);
|
DrawMarkers(viewBounds);
|
||||||
DrawPlayhead(viewBounds);
|
DrawPlayhead(viewBounds);
|
||||||
@@ -2276,6 +2335,9 @@ int main(int argc, char* argv[])
|
|||||||
if (app.exportMessageTimer <= 0.0f) app.exportMessage[0] = '\0';
|
if (app.exportMessageTimer <= 0.0f) app.exportMessage[0] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply the frame's cursor decision (see RequestCursor).
|
||||||
|
SetMouseCursor(app.cursorRequest);
|
||||||
|
|
||||||
EndDrawing();
|
EndDrawing();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+41
-1
@@ -202,7 +202,28 @@ typedef struct {
|
|||||||
|
|
||||||
// Playback state
|
// Playback state
|
||||||
float playheadT; // 0-1 normalized position within the PLAYING region
|
float playheadT; // 0-1 normalized position within the PLAYING region
|
||||||
float playheadElapsed; // Elapsed seconds since play started
|
// Wall-clock instant (GetTime()) at which the current buffer started
|
||||||
|
// playing. The playhead is derived from this rather than accumulated per
|
||||||
|
// frame: summing GetFrameTime() drifts, because every frame spent on
|
||||||
|
// rendering work is counted as playback time, and the error compounds over
|
||||||
|
// a long region. Audio runs on its own clock, so the playhead has to as
|
||||||
|
// well. Offset by playheadSeekT when the user scrubs.
|
||||||
|
double playStartTime;
|
||||||
|
float playheadSeekT; // 0-1 offset into the buffer that playback began at
|
||||||
|
// Where the next play should start within the selection (0-1). Set by
|
||||||
|
// dragging the playhead while stopped; reset once the selection changes.
|
||||||
|
float playFromT;
|
||||||
|
bool playheadDragging;
|
||||||
|
// Repeat the selection when it reaches the end, instead of stopping.
|
||||||
|
bool loopPlayback;
|
||||||
|
|
||||||
|
// Mouse cursor requested for this frame, applied once at the end of it.
|
||||||
|
// Several handlers have an opinion about the cursor and they run in
|
||||||
|
// sequence, each calling SetMouseCursor unconditionally — so whichever ran
|
||||||
|
// last won, and two of them disagreeing produced a visible flicker between
|
||||||
|
// shapes. Handlers now record a request and the highest-priority one wins.
|
||||||
|
int cursorRequest;
|
||||||
|
int cursorPriority;
|
||||||
|
|
||||||
// Snapshot of the region actually handed to the audio device, captured at
|
// Snapshot of the region actually handed to the audio device, captured at
|
||||||
// PlaySelectedRegion time. The playhead must be measured against this, not
|
// PlaySelectedRegion time. The playhead must be measured against this, not
|
||||||
@@ -449,11 +470,30 @@ static inline bool UiModalOpen(void)
|
|||||||
return app.showFileBrowser || app.showAbout;
|
return app.showFileBrowser || app.showAbout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Request a mouse cursor for this frame. Higher priority wins; ties go to the
|
||||||
|
// first caller. Applied once per frame (see the end of the main loop), so
|
||||||
|
// handlers can state their preference without fighting each other.
|
||||||
|
static inline void RequestCursor(int shape, int priority)
|
||||||
|
{
|
||||||
|
if (priority > app.cursorPriority) {
|
||||||
|
app.cursorPriority = priority;
|
||||||
|
app.cursorRequest = shape;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cursor priorities: an active drag outranks a hover hint, which outranks the
|
||||||
|
// default. Keeps "what am I doing" ahead of "what could I do".
|
||||||
|
#define CURSOR_PRI_DEFAULT 0
|
||||||
|
#define CURSOR_PRI_HOVER 10
|
||||||
|
#define CURSOR_PRI_ACTIVE 20
|
||||||
|
|
||||||
// Reset the box selection to the full signal (the "no selection" state).
|
// Reset the box selection to the full signal (the "no selection" state).
|
||||||
static inline void ClearSelection(void)
|
static inline void ClearSelection(void)
|
||||||
{
|
{
|
||||||
app.sel.timeStart = 0.0f; app.sel.timeEnd = 1.0f;
|
app.sel.timeStart = 0.0f; app.sel.timeEnd = 1.0f;
|
||||||
app.sel.freqStart = 0.0f; app.sel.freqEnd = 1.0f;
|
app.sel.freqStart = 0.0f; app.sel.freqEnd = 1.0f;
|
||||||
|
// A scrub offset is meaningless against a selection that no longer exists.
|
||||||
|
app.playFromT = 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Effective top of the displayed frequency axis (Hz). Capped at the actual
|
// Effective top of the displayed frequency axis (Hz). Capped at the actual
|
||||||
|
|||||||
@@ -873,11 +873,27 @@ void DrawSidebar(void)
|
|||||||
StopSound(AudioPlaybackSound);
|
StopSound(AudioPlaybackSound);
|
||||||
app.isPlaying = false;
|
app.isPlaying = false;
|
||||||
app.playbackFinished = false;
|
app.playbackFinished = false;
|
||||||
app.playheadElapsed = 0;
|
// Leave the marker where playback stopped rather than snapping it
|
||||||
app.playheadT = 0;
|
// to the start: it stays grabbable, and the next play resumes
|
||||||
|
// from here. playheadT is a fraction of the PLAYED span, which is
|
||||||
|
// not the selection once a scrub offset is in play — convert
|
||||||
|
// through absolute file time, the only frame the two share.
|
||||||
|
{
|
||||||
|
float stopAbs = app.playSelStart +
|
||||||
|
app.playheadT * (app.playSelEnd - app.playSelStart);
|
||||||
|
float selSpan = app.sel.timeEnd - app.sel.timeStart;
|
||||||
|
app.playFromT = (selSpan > 1e-6f)
|
||||||
|
? Clamp((stopAbs - app.sel.timeStart) / selSpan, 0.0f, 0.999f)
|
||||||
|
: 0.0f;
|
||||||
|
}
|
||||||
|
app.playheadSeekT = 0.0f;
|
||||||
} else {
|
} else {
|
||||||
PlaySelectedRegion();
|
PlaySelectedRegion();
|
||||||
app.isPlaying = true;
|
app.isPlaying = true;
|
||||||
|
// Must clear: PlaySelectedRegion restarts from the top, and leaving
|
||||||
|
// this set left the playhead parked at the end of the previous run
|
||||||
|
// while the audio played from the beginning.
|
||||||
|
app.playbackFinished = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
y += btn + gap;
|
y += btn + gap;
|
||||||
|
|||||||
Reference in New Issue
Block a user