Skip to content

Repository files navigation

audiotap

Record macOS system audio and the microphone to two separate WAV files, from the command line, with no kernel extension, no virtual audio driver, and no screen recording.

Two small Swift tools:

Tool What it does Permission it costs
audiotap captures system output + mic to two WAVs System Audio Recording Only, and Microphone
audioprocs lists which apps currently hold the mic, as JSON none

Requires macOS 14.2+ (Core Audio process taps) and Xcode command line tools. Built and run on macOS 26.

./build.sh
open -a "$PWD/AudioTap.app" --args /tmp/system.wav /tmp/mic.wav 600   # 600s, or omit to run until SIGTERM
./audioprocs                                                          # one JSON array
./audioprocs --watch --heartbeat 10                                   # a line per change, forever

The thing you came here for

A bare binary gets silence, not an error.

macOS attributes an audio-capture permission to a code-signed bundle identity, and only when the process was launched through LaunchServices — open -a Something.app. Run the same compiled binary straight from a shell and Core Audio hands it a stream of zeroes. Nothing fails. No prompt appears. The WAV is exactly the right length and completely empty.

That is why this repo builds an .app bundle around a command line tool and signs it, and why you launch it with open -a, not by path. If you take one idea from here, take that one.

It also explains a trap one layer down: don't shell out to sox or ffmpeg for the mic half. A helper spawned from a background shell is a different responsible process and gets silently denied the same way. One signed bundle captures both streams, so there is exactly one thing to authorise.

The other thing, which cost a 43-minute meeting

Never bind to a device ID at launch. Both capture chains here re-read whatever device macOS is currently defaulting to, and rebuild themselves when that changes.

The version that read the default input and output device once at startup produced 38 minutes of digital silence on both tracks because someone put AirPods in a minute after recording began. The aggregate device sat watching a route with no audio on it, the engine's input node stopped delivering buffers, and nothing errored. The files were full-length and full of zeroes; the transcriber then hallucinated the same sentence 342 times into the output. Pulling the AirPods back out restored both tracks in the same instant, which is what identified the cause.

Anything that pins a device ID and never re-reads it will lose a whole recording, quietly.

Why two files instead of one

The speakers carry everyone else; the mic only ever carries you. Kept apart, "who said what" is free — no diarisation model required. Merge them into one stereo WAV afterwards if that is what your transcriber wants, or feed the two tracks separately and label them.

Detecting a call without asking for anything

audioprocs reads a Core Audio property list that says which processes hold audio input. When Zoom, Meet or Slack opens the mic, a call started. That read costs zero TCC permissions, so a watcher can poll forever without a single privacy prompt — useful when you want the expensive, permission-carrying capture to start only once there is actually something to record.

--watch re-registers a per-process listener every time the process list changes, because an app that opens the mic for the first time after the watcher started is the normal case, not an edge case. It also emits {"heartbeat": <unix seconds>} on an interval, so a parent can tell "nothing changed" apart from "the child died".

When it goes silent

  1. Was it launched with open -a? If you ran the binary directly, that is the whole answer.
  2. Did you just rebuild? An ad-hoc signature's identity includes the code hash, so a rebuild can invalidate the grant. Remove the entry under System Settings → Privacy & Security → System Audio Recording Only (and Microphone), then launch once more and approve again.
  3. Check the log: ~/Library/Logs/audiotap/audiotap.log.
  4. Did the audio route change? That is fixed here, but it is the first thing to suspect in any other tool that does this.

Notes

  • No frames, no screenshots, no screen recording permission — audio only.
  • objc_try.m is a small Objective-C shim, because AVFoundation throws NSExceptions that Swift cannot catch, and an uncaught one takes the whole capture down with it.
  • Extracted from a larger private meeting-notes system, so the commit history starts here. Issues and PRs welcome; treat it as a working reference rather than a supported product.

MIT.

About

Record macOS system audio and the mic to two separate WAV files. No kexts, no virtual driver, no screen recording.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages