feat(incoming): rewrite Clean Incoming to a Core Audio process tap (no BlackHole) - #16
Merged
Merged
Conversation
Replace the BlackHole-loopback incoming path with macOS 14.4 Core Audio process taps: single toggle, all-system-audio-minus-NoNoise, muted originals, cleaned playback auto-following the default output. Feature disabled with a 'requires macOS 14.4+' message below 14.4. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… review
Codex-approved (gpt-5.5, plan mode, 4 rounds). Incorporates all blocking findings:
- Realtime-safety contract: tap IOProc is allocation-free + lock-free; treats HAL
inInputData as read-only; new lock-free SPSC ring (acquire/release publication,
modeled on Driver nn_ring) bridges the two realtime threads, not os_unfair_lock.
- Own-process resolution hard-fails start() -> false (no tap) to avoid self-capture.
- Single idempotent teardown order + start order (pin playback before muting).
- Availability isolated behind #available(macOS 14.4,*) so the .macOS(.v13) package
still builds (C AudioHardwareCreateProcessTap, not the macOS 15 Swift overlay).
- Canonical incomingCleanupStatus so the toggle never lies on start() failure.
- Orphan-cleanup checklist + legacy mv.incoming{Source,Output}UID key removal.
Co-authored-by: Cursor <cursoragent@cursor.com>
…o BlackHole) Replace the AVCaptureSession-of-a-loopback path with a process tap (CATapDescription stereo-global-exclude + private aggregate + IOProc) that captures all system audio except NoNoise, cleans it (DFN only), and re-renders to the current default output (auto-following device changes; originals muted). Collapses the source/monitor pickers to a single toggle; no BlackHole or manual routing. macOS 14.4+ (whole engine @available-gated so .macOS(.v13) still builds). Bridge the tap IOProc and AVAudioSourceNode (both realtime threads) with a new lock-free C11-atomics SPSC ring (CTapRing/tap_ring + TapAudioRing) modeled on the driver's tested nn_ring — never the os_unfair_lock RingBuffer. Both callbacks are allocation/lock/syscall-free and treat the HAL input read-only. start() -> Bool is truthful (hard-fails on unresolved own-process id; playback starts before the mute engages); stop() is one idempotent teardown run on every failure path so a leaked muted tap can't mute other apps. AudioModel owns it as a lazy AnyObject? and binds the UI to a never-lying incomingCleanupStatus. Remove incomingSource/monitor state, fetchIncomingDevices, the fetchOutputDevices monitor branch, isSelectableIncomingSource/isSelectableMonitorOutput, and DeviceInfo.hasInput/transportType. Add NSAudioCaptureUsageDescription (usage string, not a new entitlement). New TapAudioRingTests + IncomingTapLogicTests; delete IncomingCleanupTests. swift test 223 green; debug + release arm64 build. Co-authored-by: Cursor <cursoragent@cursor.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.
Summary
Rewrites Clean Incoming ("clean the other side") from the BlackHole-based capture session to a
Core Audio process tap (macOS 14.4+). The app now taps all system audio except NoNoise's own
process (global-exclude tap, originals muted), cleans it through a per-instance
DeepFilterNetDSP,and re-renders to the current default output — auto-following device changes. No BlackHole, no
source/monitor pickers: a single toggle + status line.
Sources/CTapRing, C11 atomics, acquire/release) bridges the realtimeIOProc (producer) and the
AVAudioSourceNoderender thread (consumer), mirroring the existingnn_ringpattern. Thin Swift wrapper inTapAudioRing.swift; pure re-pin/rebuild decision logic inIncomingTapLogic.swift(unit-tested).start() -> Boolis honest — the owner (AudioModel) retains only agenuinely running pipeline. Runtime self-teardown (default output vanished, re-pin/rebuild failed)
fires
onRuntimeFailureso the toggle drops.cleaning → .failedinstead of lying.aggregate is verified at 48 kHz (Set + read-back) before feeding DFN; a runtime re-pin that can't
follow the new default tears down rather than playing to the wrong/old device.
isSelectableIncomingSource/isSelectableMonitorOutputpredicates,source/monitor pickers, and the
mv.incomingSource*/mv.incomingOutput*UID keys.NSAudioCaptureUsageDescription(TCC consent for audio capture).AGENTS.md+CONCEPTS.mdincoming sections updated;docs/knowledgerecords the designdecision and supersedes the 2026-06-15 BlackHole entries; design spec under
docs/superpowers/specs.Review
verify), F4 (no-output guard), and the default-output auto-follow refinement all closed; the outer
DeepFilterNetDSP.processallocation was accepted as pre-existing/blessed byAGENTS.md(out ofscope for this rewrite).
Test plan
swift build(debug) — cleanswift test— 227 tests, 0 failures (incl. newTapAudioRingTests,IncomingTapLogicTests,IncomingDownmixTests)swift build -c release --arch arm64— clean.app+ TCC grant): enable Clean Incoming, confirm guest audio isde-noised and the user's own playback is unaffected
confirm playback follows the new default
state (no system-wide mute)