Skip to content

feat(audio): warm-up gate on the adaptive NMI-rate loop to kill the start/seek pitch glide#39

Merged
kfox merged 1 commit into
mainfrom
feat/nmi-rate-loop-warmup-gate
Jun 24, 2026
Merged

feat(audio): warm-up gate on the adaptive NMI-rate loop to kill the start/seek pitch glide#39
kfox merged 1 commit into
mainfrom
feat/nmi-rate-loop-warmup-gate

Conversation

@kfox

@kfox kfox commented Jun 24, 2026

Copy link
Copy Markdown
Owner

Problem

On the host-DMA audio path, the adaptive NMI-rate loop seeds its R-rate EMA off the first measurements after the consumer starts — but those are unrepresentative. Right after a start/seek the video pipeline's bus load hasn't reached steady state yet (post-seek decode catch-up plus the playlist's frame-drop snap-forward), so the measured consumer rate R reads high (~11.5k Hz) until the steady-state VIC/DMA tick-loss arrives a few seconds later and R settles (~10.1k Hz).

The loop chased that transient — stepping the CIA #2 latch away from its seed and then all the way back — producing an audible start-of-playback pitch glide. The motion is pure waste: for a bitmap scene the seed latch is the ceiling, which is already the converged value, so holding the seed steady would have landed dead-on.

Fix

Add a warm-up gate to the rate loop. For NMI_RATE_LOOP_WARMUP_S (3.0 s) after the consumer starts — and after a large playback disturbance — the loop keeps feeding R into its EMA but holds the latch at the near-converged seed, then makes its first decision from a settled estimate. The seed plays at ~the correct rate during the hold, so there's no glide either way.

Arming points:

  • Consumer start — armed in the worker's post-prebuffer reset (covers the initial seek).
  • Large frame-drop — the playlist calls the new AudioStreamer.note_playback_disturbance() when a deadline snap-forward drops more than _AUDIO_DISTURBANCE_DROP_S (0.5 s) of frames (a seek catch-up or a stream rebuffer). Routine 1–3 frame drops stay below the bar.
  • Mid-stream display-mode change — the bus-halt profile (hence R) shifts with the mode, so the new seed is held until the new load settles.

The EMA is left intact across a disturbance (not re-seeded) so it keeps tracking through the gap. Steady-state behavior is unchanged — only the start/seek/disturbance transient is affected. Host-DMA path only (REU pump has its own C64-side governor; open-loop/static paths are unaffected).

Tests

  • Audio: warm-up holds a would-be latch step, still warms the EMA during the hold, releases and steps after the deadline, and is re-armed by note_playback_disturbance().
  • Playlist: a large drop signals the audio loop, a small drop does not, and the audio=None case is safe.

make lint typecheck test all clean (ruff, mypy --strict, pyright, full unittest suite).

…tart/seek pitch glide

The host-DMA adaptive NMI-rate loop seeded its R-rate EMA off the first
measurements after the consumer started, but those are unrepresentative:
right after a start/seek the video pipeline's bus load hasn't reached
steady state (post-seek decode catch-up + the playlist's frame-drop snap),
so R reads high (~11.5k vs the ~10.1k it settles to in ~3 s). The loop
chased that transient — stepping the latch away from the seed and back —
producing an audible start-of-playback pitch glide of pure wasted motion,
since the bitmap seed (= ceiling) is already the converged latch.

Add a warm-up gate: for NMI_RATE_LOOP_WARMUP_S after the consumer starts
(and after a large playback disturbance), keep feeding R into the EMA but
hold the latch at the near-converged seed, then make the first decision
from a settled estimate. The seed plays at ~the right rate during the hold,
so there's no glide either way.

The playlist signals a large disturbance (seek catch-up / stream rebuffer)
via the new AudioStreamer.note_playback_disturbance() when a deadline
snap-forward drops more than _AUDIO_DISTURBANCE_DROP_S of frames; routine
1-3 frame drops stay below the bar. A mid-stream display-mode change arms
it too (the bus-halt profile shifts with the mode).

Tests: warm-up holds a would-be step, keeps warming the EMA, releases after
the deadline, and is re-armed by note_playback_disturbance / a large drop;
a small drop and the audio=None case are covered. make lint/typecheck/test
clean.
@kfox kfox merged commit e3d3aea into main Jun 24, 2026
5 checks passed
@kfox kfox deleted the feat/nmi-rate-loop-warmup-gate branch June 24, 2026 02:19
@codecov

codecov Bot commented Jun 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.56%. Comparing base (77067af) to head (8ea8e27).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #39      +/-   ##
==========================================
+ Coverage   79.55%   79.56%   +0.01%     
==========================================
  Files          68       68              
  Lines       12847    12858      +11     
  Branches     1896     1898       +2     
==========================================
+ Hits        10220    10231      +11     
  Misses       2188     2188              
  Partials      439      439              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant