Skip to content

fix: stop the ad mute stalling the poll loop for 200ms - #2

Merged
FilbertNg merged 1 commit into
mainfrom
fix/faster-ad-mute
Sep 10, 2026
Merged

FilbertNg merged 1 commit into
mainfrom
fix/faster-ad-mute

Conversation

@FilbertNg

Copy link
Copy Markdown
Owner

Closes #1.

set_spotify_muted() took ~207 ms per call and runs on the asyncio thread, so every ad boundary froze the poll loop for longer than a poll_interval — twice per ad, plus ~207 ms of startup via the cleanup call in cli.py.

Almost all of it was waste. pycaw's AudioUtilities.GetAllSessions() reaches the session manager through CreateDevice(), which reads the audio device's entire property store (~200 properties, each with a GUID-to-string conversion) to build a description nothing here looks at. cProfile put 200 ms of the 207 ms inside CreateDevice; the SetMute calls themselves took 0.303 ms.

Change

looptify/audio.py now goes to IAudioSessionManager2 directly off the default render endpoint — CoCreateInstanceGetDefaultAudioEndpointActivate — and matches sessions by GetProcessId() instead of pycaw's per-session psutil wrapper.

196.7 ms → 8.3 ms median (24×.)

The manager is deliberately not cached between calls. GetDefaultAudioEndpoint() is 4.5 ms of the remaining 8.3 ms, and the only staleness check for a cached manager costs that same 4.5 ms — so caching would buy ~5 ms in exchange for silently failing to mute after the default audio device changes (plugging in headphones). Not a good trade at these magnitudes.

Both new direct imports, comtypes and psutil, were already declared in requirements.txt.

Verification

CONTRIBUTING.md is explicit that adapters get verification scripts rather than unit tests, since mocking COM would only test the mock. This PR adds scripts/verify_audio.py, which reads mute state back through pycaw's own enumeration so the new code cannot vouch for itself, and asserts a 50 ms budget. Run against the old code first, then the new:

correctness timing exit
before mute [1,1], unmute [0,0] 196.7 ms median 1 (fail)
after mute [1,1], unmute [0,0] 8.3 ms median 0 (pass)

Also confirmed:

  • 54/54 existing tests pass
  • the no-matching-session path (Spotify closed) returns 0 without raising
  • logic.py still imports no Windows APIs, per the architectural rule
  • the app runs end to end for 20 s with empty stderr and no [warn] lines, exercising the startup unmute

Not in scope

Profiling turned up three other findings — the idle Tk notification thread costing 0.31% of a core, redundant 6.67 Hz SMTC polling against a source that pushes every 4.51 s, and the unconditional console redraw. Those are larger changes and are left for separate work.

🤖 Generated with Claude Code

set_spotify_muted() went through pycaw's AudioUtilities.GetAllSessions(),
which reaches the audio session manager via CreateDevice() -- and that reads
the device's entire property store, around 200 properties, to build a
description nothing here looks at. Profiling put 200ms of the 207ms call
inside CreateDevice; the SetMute calls themselves took 0.3ms.

That ran on the asyncio thread, so every ad boundary froze the poll loop for
longer than a poll interval.

Go to the session manager directly off the default endpoint and match
sessions by process id instead. Measured 196.7ms -> 8.3ms median, with the
same two Spotify sessions muted and unmuted.

Deliberately not cached between calls: GetDefaultAudioEndpoint() is 4.5ms of
the remaining 8.3ms, and the only staleness check for a cached manager costs
that same 4.5ms -- so caching would buy ~5ms in exchange for silently
failing to mute after the default audio device changes.

Adds scripts/verify_audio.py, which reads mute state back through pycaw's
own enumeration so the new code cannot vouch for itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@FilbertNg
FilbertNg merged commit 0fd0d28 into main Sep 10, 2026
2 checks passed
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.

Optimizing Ads Mute Spotify

1 participant