docs: system audio is silent unless quill runs as a LaunchAgent - #54
Open
psabbagh wants to merge 2 commits into
Open
docs: system audio is silent unless quill runs as a LaunchAgent#54psabbagh wants to merge 2 commits into
psabbagh wants to merge 2 commits into
Conversation
Launched from a terminal, quill's TCC request is attributed to the terminal rather than to quill. AudioHardwareCreateProcessTap returns noErr, the IO proc fires at the correct rate for the full session, and every sample is zero — a full-length silent system.caf with no error and no permission prompt. Measured across four takes: silent from a shell whether or not the embedded Info.plist is bound by the signature, and correct under launchd, where macOS prompts by name. Also corrects the Gotchas entry pointing at Screen Recording, which is a different TCC service and does not cover process taps. The RCA records the one untested combination (launchd plus the stock linker-signed binary), which decides whether install needs a codesign step.
Ran the experiment the RCA left open: a stock swift build binary, linker-signed with Info.plist not bound, captures system audio correctly once it runs under launchd (-0.6 dB peak, 54.6% non-zero). So the embedded-plist binding defect is real but not load-bearing, and the install recipe needs no codesign step — the fix is documentation plus a liveness check. Records the residual uncertainty about whether TCC keyed the grant to the signature or the path, which this machine's prior grant history could not isolate.
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.
The problem
Run quill from a terminal, as the README's "How to use" step 1 suggests, and
system.cafcomes out as a full-length file of digital silence. There is no error, no permission prompt, and nothing inquill doctorthat reveals it. You find out when the transcript has nothemsegments — after the meeting.Evidence
macOS 26.5.2 (25F84), Apple Silicon, Swift 6.3.3, at 855869e. Each take played the same speech through the default output device while recording; levels come from decoding the resulting CAF to 16-bit PCM.
The mic track is healthy throughout, so the sessions themselves are fine.
Why it is invisible
Nothing fails.
AudioHardwareCreateProcessTapreturnsnoErr,kAudioTapPropertyFormatreports a correct 2ch/48kHz stream, the aggregate device is created,AudioDeviceStartsucceeds, and the IO proc fires at the correct rate for the whole session — take 1 delivered 1,962 packets across 41.8s at 1,024 frames each, matching wall clock exactly. Every sample is zero.A correctly clocked device delivering well-formed all-zero buffers is an unauthorized tap, not a broken audio graph. No error path in
SystemAudioRecordercan observe it.Cause
TCC evaluates the request against the responsible process. Launched from a terminal, that is the terminal, not quill — so quill has no identity to grant, and because the responsible process already has its own TCC record, no prompt is raised either. Under launchd quill is its own responsible process, macOS prompts with
"quill" would like access to record your system audio, and capture works immediately.Take 3 is the decisive one: with the embedded Info.plist correctly bound by the signature, a shell-launched quill was still silent and still produced no prompt. Binding alone is not sufficient.
Two things ruled out during diagnosis, recorded in the RCA so nobody repeats them:
createAggregateDevicepasses an empty sub-device list and no main device, which looks like a missing clock source. Adding the default output device askAudioAggregateDeviceMainSubDeviceKeyand sole sub-device changed nothing. Reverted; not part of this PR.swift buildemits a linker-signed binary where the embedded Info.plist is not bound (Info.plist=not bound), which is a real defect but not load-bearing. A completely stock binary captures correctly once it runs under launchd (take 5). The install recipe needs nocodesignstep.A related trap worth knowing: system audio is gated on the System Audio Recording Only list, not Screen Recording. The current Gotchas entry sends users to a list quill can never appear in — and a bare binary can't be added by hand through the
+picker either.What this PR changes
Documentation only, no code:
--launch-at-loginis no longer marked "optional".issues/rca-002-system-tap-silent-outside-launchagent.mdwith the full write-upNotes
I wrote the RCA in the shape of
rca-001, since that seemed to be the house convention — happy to drop that file and keep this to the README changes if you would rather.issues/stay yours, or move it to a regular issue.The RCA proposes a code fix I have deliberately not included here, to keep this reviewable: a liveness check in
SystemAudioRecordermirroring the oneMicRecorderalready has from rca-001 — accumulate peak over the first second of tap callbacks and warn loudly if it is exactly zero. Unlike the mic case there is no fallback to switch to, so the right action is to tell the user rather than recover silently. It also notes thatdoctorcould checkgetppid() == 1and warn when quill is not running under launchd. Happy to open either as a follow-up if you want them.One honest gap, recorded in the RCA: on the machine used for testing, quill had prior grant history at the same path, so whether TCC keyed the working grant to the code signature or to the executable path was not isolated. On a machine that has never granted quill, prompt behaviour may differ. The practical claim — a stock binary under launchd obtains and uses a working grant — is demonstrated.