feat(audio): warm-up gate on the adaptive NMI-rate loop to kill the start/seek pitch glide#39
Merged
Merged
Conversation
…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.
Codecov Report✅ All modified and coverable lines are covered by tests. 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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.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
note_playback_disturbance().audio=Nonecase is safe.make lint typecheck testall clean (ruff, mypy --strict, pyright, full unittest suite).