Skip to content

feat(macos): capture system audio with a Core Audio tap — no screen recording permission - #16

Merged
PCcoding666 merged 6 commits into
masterfrom
feat/macos-core-audio-tap
Oct 10, 2026
Merged

PCcoding666 merged 6 commits into
masterfrom
feat/macos-core-audio-tap

Conversation

@PCcoding666

Copy link
Copy Markdown
Owner

Summary

System audio no longer needs Screen Recording. It is captured with a Core Audio process tap (macOS 14.2+; LCT requires 15), whose consent is a normal in-app Allow prompt — no trip to System Settings, no relaunch, no periodic re-confirmation of screen recording.

  • SystemAudioTap: private global stereo tap excluding LCT's own process → private aggregate device → IO proc on a dedicated queue (built nonisolated; a closure formed in a @MainActor context traps on Core Audio's IO thread) → MicrophoneFormatConverter to 16 kHz mono. Idempotent teardown in Core Audio order; every step's OSStatus logged. Core Audio calls sit behind SystemAudioTapHardware so tests never create a real tap.
  • AudioCaptureService: tap first; the ScreenCaptureKit stream is kept only as a fallback when the tap cannot be created (only that path checks screen recording, with a sticky warning).
  • Onboarding: Screen Recording row, restart prompt and every SCShareableContent call removed; new System Audio · Allow row runs SystemAudioPermissionProbe off the main thread so the consent prompt appears during setup instead of on the first Start.
  • Authorization detection: macOS reports success on every setup step regardless of consent; an authorized tap calls back continuously (~95/s, even in silence). A first-callback watchdog (3 s) treats a silent tap as not authorized → error notice with Open Settings; the microphone lane keeps running.
  • Startup no longer preflights screen recording; NSAudioCaptureUsageDescription added; diagnostics and user docs updated.

Findings from real-Mac testing (macOS 27)

  • Spike under a separate test identity with no screen-recording grant (CGPreflightScreenCaptureAccess() == false) captured system audio: 2837 callbacks in 30 s, peak RMS 0.37.
  • First Start of the real app showed "LCTMac would like access to record your system audio" with Allow; while it is on screen, AudioDeviceCreateIOProcIDWithBlock blocks (UI shows starting…) — hence the onboarding row.
  • A tap whose IO proc was created while the prompt was pending stayed silent after Allow; a fresh tap worked. The watchdog and the probe therefore rebuild a silent tap once before reporting denial (regression tests for both). A true "Don't Allow" could not be reproduced (macOS did not show the prompt again for the test identities), so the denied path is covered by unit tests only.

Test results

  • ./Scripts/run_tests.sh: 444 tests, 0 failures, no new warnings; CLT build clean. Tests use fake taps only — no real tap, no consent prompt.
  • GUI: system-lane captions and translation complete (大家好,今天我们来聊一聊大语言模型… → "Hello everyone, today we'd like to discuss how large language models are trained…"); a 1.5 h dual-lane session produced 701 system-lane results; watchdog verdict "authorized" 22 ms after start; second start: tap 0.35 s, whole start 3.8 s; onboarding shows Microphone ✓ / Speech ✓ / System Audio [Allow] → probe granted; no screen-recording prompt anywhere.
  • Not exercised in GUI: switching output devices (headphones) mid-capture; the ScreenCaptureKit fallback (tap creation never failed).

Code by Qoder (Kimi-K3) from task briefs plus a Claude-written spike; the silent-tap rebuild, review and GUI verification by Claude.

🤖 Generated with Claude Code

PCcoding666 and others added 6 commits October 10, 2026 20:27
Replace the ScreenCaptureKit-only system lane with a Core Audio
process tap (macOS 14.2+), which needs no screen-recording TCC grant:
a private global stereo tap excluding LCT itself, wrapped in a private
aggregate device and read through an IO proc on a serial queue, with
buffers converted to 16 kHz mono via MicrophoneFormatConverter.
ScreenCaptureKit stays as the fallback and only then checks the
screen-recording permission; the active backend and the tap failure
reason are recorded for the UI. The IO block is built in a
nonisolated context so it never inherits MainActor isolation on Core
Audio's realtime thread, and non-interleaved buffer lists that the
zero-copy wrap rejects fall back to a per-channel copy.
start() no longer checks the screen-recording permission before
enabling the system lane — the Core Audio tap needs none. The old
mic-only fallback warning is gone; when the tap fails and the lane
runs on ScreenCaptureKit a sticky warning names the failing step and
OSStatus, and only that fallback can raise the screen-recording
error notice. The capture service and speech engine are now
injectable into the view model so this flow is testable offline.
The welcome flow lists only microphone and speech recognition — the
screen-recording row, restart hint and resume-store usage are gone,
and AppDelegate no longer preflights screen capture at launch, so a
first run never triggers the screen-recording prompt.
OnboardingResumeStore stays for its tests but nothing writes to it.
Add NSAudioCaptureUsageDescription for the process tap, annotate the
diagnostics report (screen recording is only needed for the fallback,
system audio runs on the Core Audio tap), rewrite
diagnose_permissions.sh for the new permission model, and update the
user docs to say system audio capture needs no extra permission.
…ect denial

The Core Audio tap reports success on every setup step even when macOS
withholds the system-audio consent — a denied tap simply never delivers
an IO callback, while an authorized one calls back continuously, even in
silence. Use that first-callback signal twice:

- Onboarding gains an optional System Audio row whose Allow button runs
  SystemAudioPermissionProbe: start a tap off the main thread (start()
  blocks while the TCC prompt is up), watch for the first callback, then
  always stop it. Granted shows a check, denied offers the Settings pane.
- AudioCaptureService starts a 3s first-callback watchdog after every
  tap start. On silence it stops and drops the tap (never falling back
  to ScreenCaptureKit), keeps the microphone lane running, and the
  ViewModel surfaces an error with an Open Settings action; a
  system-only session winds down to idle.

The last verdict (granted/denied) is persisted and shown as a status
line in the diagnostics report.
…enial

On a real Mac, a tap whose IO proc was created while the consent prompt
was on screen delivered no callbacks after the user clicked Allow; the next
tap worked. Both the capture watchdog and the onboarding probe now rebuild
a silent tap once, so allowing access is no longer misreported as denied.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 10, 2026 10:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@PCcoding666
PCcoding666 merged commit efdd064 into master Oct 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.

2 participants