Skip to content

feat(incoming): rewrite Clean Incoming to a Core Audio process tap (no BlackHole) - #16

Merged
ivalsaraj merged 3 commits into
mainfrom
feat/tap-based-clean-incoming
Jun 16, 2026
Merged

feat(incoming): rewrite Clean Incoming to a Core Audio process tap (no BlackHole)#16
ivalsaraj merged 3 commits into
mainfrom
feat/tap-based-clean-incoming

Conversation

@ivalsaraj

Copy link
Copy Markdown
Owner

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.

  • New lock-free SPSC ring (Sources/CTapRing, C11 atomics, acquire/release) bridges the realtime
    IOProc (producer) and the AVAudioSourceNode render thread (consumer), mirroring the existing
    nn_ring pattern. Thin Swift wrapper in TapAudioRing.swift; pure re-pin/rebuild decision logic in
    IncomingTapLogic.swift (unit-tested).
  • Truthful lifecycle: start() -> Bool is honest — the owner (AudioModel) retains only a
    genuinely running pipeline. Runtime self-teardown (default output vanished, re-pin/rebuild failed)
    fires onRuntimeFailure so the toggle drops .cleaning → .failed instead of lying.
  • Never silences other apps: the muted tap won't start without a usable default output; the
    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.
  • Removes the old BlackHole path: isSelectableIncomingSource/isSelectableMonitorOutput predicates,
    source/monitor pickers, and the mv.incomingSource* / mv.incomingOutput* UID keys.
  • Adds NSAudioCaptureUsageDescription (TCC consent for audio capture).
  • Docs: AGENTS.md + CONCEPTS.md incoming sections updated; docs/knowledge records the design
    decision and supersedes the 2026-06-15 BlackHole entries; design spec under docs/superpowers/specs.

Review

  • Design spec approved via Codex plan review (4 rounds).
  • Code approved via Codex code review (3 rounds): F1 (runtime teardown + owner notify), F3 (48 kHz
    verify), F4 (no-output guard), and the default-output auto-follow refinement all closed; the outer
    DeepFilterNetDSP.process allocation was accepted as pre-existing/blessed by AGENTS.md (out of
    scope for this rewrite).

Test plan

  • swift build (debug) — clean
  • swift test — 227 tests, 0 failures (incl. new TapAudioRingTests, IncomingTapLogicTests,
    IncomingDownmixTests)
  • swift build -c release --arch arm64 — clean
  • Manual (requires bundled .app + TCC grant): enable Clean Incoming, confirm guest audio is
    de-noised and the user's own playback is unaffected
  • Manual: switch the default output device while running (incl. Bluetooth/TWS auto-switch) and
    confirm playback follows the new default
  • Manual: unplug/remove the output device while running and confirm the toggle drops to a failed
    state (no system-wide mute)

ivalsaraj and others added 3 commits June 16, 2026 16:09
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>
@ivalsaraj
ivalsaraj merged commit 2c0b1c4 into main Jun 16, 2026
1 check 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.

1 participant