fix: stop the ad mute stalling the poll loop for 200ms - #2
Merged
Merged
Conversation
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>
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.
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 apoll_interval— twice per ad, plus ~207 ms of startup via the cleanup call incli.py.Almost all of it was waste. pycaw's
AudioUtilities.GetAllSessions()reaches the session manager throughCreateDevice(), 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.cProfileput 200 ms of the 207 ms insideCreateDevice; theSetMutecalls themselves took 0.303 ms.Change
looptify/audio.pynow goes toIAudioSessionManager2directly off the default render endpoint —CoCreateInstance→GetDefaultAudioEndpoint→Activate— and matches sessions byGetProcessId()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,
comtypesandpsutil, were already declared inrequirements.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:[1,1], unmute[0,0][1,1], unmute[0,0]Also confirmed:
logic.pystill imports no Windows APIs, per the architectural rule[warn]lines, exercising the startup unmuteNot 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