Stage 04 — The Smooth Conductor
You will build: measurably smooth note motion — and the measurement itself, because "looks fine to me" is not data. You'll learn: why the audio clock stutters · blending two clocks · measure-first optimization — and you'll make design decision 1 from the architecture doc. Warm-up — from memory: the four clocks from the architecture's timing model, and which one is ground truth.
Why this exists#
Your conductor reads time from the audio system, and the audio system thinks in chunks:
get_playback_position() advances in ~10ms steps, not continuously. The
get_time_since_last_mix() term interpolates inside a chunk, but the result still jitters —
and at 144Hz, where a frame is 7ms, the jitter is bigger than a frame. Notes visibly
shudder. The fix is a second opinion: the system clock (Time.get_ticks_usec()) is
perfectly steady but drifts from the audio; the audio clock is truthful but jittery. A
smooth conductor blends them — steady short-term, truthful long-term. The official demo you
studied does exactly this (that's what its filter was for).
Build it#
A — Measure first#
Before changing anything, prove the problem exists. In the conductor, each frame, record the delta of song time between frames into an array; after ten seconds, print min/max/average. A perfect clock at 144Hz shows ~6.9ms every frame. What do you actually see? Write the numbers down — they're your before picture.
B — Choose your blend (decision 1)#
- (A) The follower: keep a
smoothed_timethat moves toward raw song time each frame — fast enough to never lag audibly, slow enough to flatten the steps. One tunable. - (B) The dual-clock: advance
smoothed_timeby the system clock's delta each frame, then nudge it toward raw song time by a small correction factor — the demo's structure without the filter math. Slightly more code; drift correction is explicit and tunable.
Both are ~10 lines. Pick one, record the choice and reason in the architecture doc, build
it behind the existing public API — get_current_beat() callers must not change. (If the
API boundary from stage 03 was drawn well, this is a one-file change. If it wasn't, you
just learned why boundaries matter — fix the boundary first.)
C — Measure again#
Same measurement, same duration. Before vs after, in numbers. Then the eyeball test: a slow song, a 144Hz screen if you have one, notes crossing the judgment line.
D — The edge cases#
Answer by experiment, not guesswork: what does your smoothed clock do when the song starts (raw time jumps from negative silence to zero)? When the game hitches for 200ms (drag the window)? Neither answer should be "notes teleport."
E — Commit#
git add . && git commit -m "stage 04: smoothed conductor, jitter measured before/after"
Checkpoint — definition of done#
- Before/after jitter numbers written down, and after is honestly better
- No caller of the conductor changed a line
- Song start and a deliberate 200ms hitch both recover gracefully
- Judgments still feel identical (smoothing display time must not have moved judgment time — did it? should it? — you now know enough to argue either side)
- Decision 1 recorded with your reason
Stretch (no instructions)#
Expose the smoothing strength in a debug overlay and find the value where you can first feel the lag. That number is worth more than any tutorial's default.
If you get stuck#
- Smoothed time oscillates → you're correcting toward a target that itself jitters; correct gently — what happens as your factor approaches 1?
- Notes lag behind the music audibly → your smoothing is too slow; the ear notices ~30ms. The tension between "smooth" and "honest" is the entire design space here.
- Numbers look identical before/after → are you measuring the smoothed value, or the raw one you started with?