Swift CLI that records macOS system audio and microphone to separate tracks using ScreenCaptureKit. No virtual audio driver, no aggregate device.
The executable is named audiorec. The primary use case is recording video conference calls: system (application) audio and the microphone are captured simultaneously through a single SCStream, written to separate lossless CAF track files, and optionally mixed down to a single AAC file afterwards. Nothing is ever mixed live.
This project was vibe coded with Claude Fable. As such the author can not be held responsible for lost, truncated, or silent recordings, tracks that drift out of sync, a mixdown that clips or ducks the wrong voice, a disk filled by hours of lossless audio, a crash partway through a conversation you only get to have once, permission prompts that appear when they should not or stay away when they should not, or anything else this code decides to do on your machine, etc.
Read it before trusting it with something you cannot record twice, and do a throwaway run first. The MIT license disclaims warranty in the usual terms, and those terms mean what they say here.
Install | First-run permission grant | Usage | Speaker bleed | Track alignment | Requirements | Build and sign | Known limitations | Recording consent
brew install jftuga/tap/mac-audio-recorder
The released binary is Apple Silicon only and requires macOS 15 or later; Homebrew refuses the install on anything else rather than handing you a binary that cannot run. To build it yourself instead, see Build and sign.
Released binaries are ad-hoc signed, which has one consequence worth knowing before you rely on it: the signature changes with every release, macOS treats each version as a different program, and the permission grants do not carry over. Expect to re-approve Screen Recording after a brew upgrade, ideally before a call rather than during one.
Whether you installed or built it, audiorec needs its permissions granted once before it can record anything.
The tool needs two permissions:
- Screen Recording (named "Screen & System Audio Recording" in current System Settings). ScreenCaptureKit gates all capture behind this, including microphone-only capture.
- Microphone, only when the microphone track is enabled.
Run the interactive one-time setup from a local GUI session:
audiorec permissions
This reports the current state, triggers the consent prompts where possible, and exits non-zero if either permission is missing. Screen Recording consent cannot be granted programmatically: it requires a one-time interactive approval in the prompt or System Settings, or an MDM PPPC configuration profile. The prompt will not appear over SSH. There is no headless path. Once granted, unattended runs work.
If a prompt was previously denied, macOS will not prompt again; flip the toggle manually in System Settings under Privacy & Security.
audiorec --help lists the subcommands, and each takes --help of its own. audiorec --version prints the version followed by this repository's URL, so a binary found without context can be traced back to its source.
audiorec record Record everything until Ctrl-C
audiorec record --duration 3600 Stop after one hour
audiorec record --target-bundle-id us.zoom.xos Only Zoom's audio, plus the mic
audiorec record --no-mic System audio only
audiorec record --no-system Microphone only
audiorec record --mic-device <uid> Specific microphone (uid from "audiorec devices")
audiorec record --output ~/Recordings --name standup
audiorec record --mix --mic-gain 1.5 --system-gain 0.8
audiorec record --mix --reduce-bleed normal Recording on speakers, not headphones
audiorec record --name standup --force Overwrite an earlier take of the same name
audiorec record --quiet
The tracks are written separately and never mixed live:
<name>-system.caf- system audio, unless--no-system<name>-mic.caf- microphone, unless--no-mic<name>-tracks.json- where each track starts relative to the other; see Track alignment
A track that received no audio at all is not written, and the sidecar is written only when both tracks were captured, since it exists to relate one to the other.
<name> defaults to a timestamp such as 20260724-153042. The files are 32-bit float PCM CAF at whatever sample rate and channel count the streams actually deliver; formats are read from the incoming buffers, not assumed. CAF survives an unclean termination far better than containers that require a finalized header.
Recording refuses to start if any output file for that basename already exists, including the <name>-mix.m4a that --mix would produce, so a repeated --name cannot silently destroy an earlier take. Pass --force to overwrite deliberately.
On SIGINT (Ctrl-C) or SIGTERM the writers are finalized cleanly, the written paths are reported, and the exit code is 0. Signals are absorbed only until the track files are closed; a second Ctrl-C during a long mixdown terminates the process, leaving the finalized tracks intact. Elapsed time and per-track peak levels are printed to stderr every two seconds unless --quiet is given. If the stream fails mid-capture (display reconfigured, target application quit, audio device removed), whatever was captured is finalized and the exit code is non-zero.
--mix produces <name>-mix.m4a (stereo AAC, 192 kbps) as a discrete second step after the track files are finalized. The mixdown resamples each track to the higher of the two sample rates; --mic-gain, --system-gain, and --reduce-bleed apply only during mixdown and are invalid without --mix. See Speaker bleed below for --reduce-bleed.
Gains are linear multipliers, greater than zero and no larger than 15.8. That upper bound sits just inside the +24 dB ceiling of the mixdown's gain stage, and a larger value is rejected rather than quietly reduced. If the summed tracks exceed full scale the mix is written anyway and the peak is reported on stderr, since the fix is a gain choice rather than something the tool should decide for you.
audiorec mix session-system.caf session-mic.caf
audiorec mix session-system.caf session-mic.caf --reduce-bleed normal
audiorec mix session-system.caf session-mic.caf --output ~/Desktop/call.m4a --mic-gain 1.5
audiorec mix session-system.caf session-mic.caf --force Replace an existing mix
Mixes two already-finalized track files into one m4a. This is the same mixdown that record --mix performs, exposed separately so a mix can be produced or re-produced afterwards with different gains or bleed reduction, without recording again. The track files are only read, never modified, so you can try several settings against the same recording.
With --output omitted, the output name is derived from the system track: a trailing -system is replaced with -mix, so 20260724-235014-system.caf becomes 20260724-235014-mix.m4a in the same directory. The output must be an .m4a file, and an existing one is not overwritten without --force.
audiorec devices
audiorec devices --json
Lists microphone input devices with the unique IDs accepted by --mic-device, and running applications with the bundle identifiers accepted by --target-bundle-id. Application enumeration goes through ScreenCaptureKit and therefore requires the Screen Recording permission; microphone enumeration does not.
audiorec permissions
Reports the TCC authorization state for Screen Recording and Microphone, triggers the consent prompts if not yet decided, and exits non-zero if either is missing.
If you record with open speakers rather than headphones, the microphone also picks up the system audio acoustically. The mic track then holds a delayed, room-filtered copy of the far end, and summing it with the clean system track in the mix produces an audible doubling or slap-back echo. The individual track files are unaffected; only the mix is.
Headphones eliminate this at the source and remain the recommended setup. When that is not possible, --reduce-bleed attenuates the microphone during passages where it appears to be carrying only bleed:
audiorec mix session-system.caf session-mic.caf --reduce-bleed normal
| Strength | Bleed-only passages | Overlapping speech |
|---|---|---|
| light | -20 dB | -6 dB |
| normal | -40 dB | -12 dB |
| aggressive | muted | -18 dB |
This is a sidechain ducker keyed by the system track, not echo cancellation. It removes the artifact where only the far end is speaking, which is where doubling is most audible. When both parties speak at once it attenuates the whole microphone signal by the smaller amount shown above, because a single gain cannot separate your voice from the bleed underneath it; some doubling therefore remains during overlapping speech, masked by the two voices.
The strengths differ only in attenuation depth, never in how readily a passage is judged to be bleed. Detection is deliberately conservative in all three, because leaving some bleed audible is recoverable while cutting a speaker off mid-sentence is not.
Before processing, the two envelopes are correlated to confirm bleed is actually present. If it is not, as when the recording was made on headphones, the microphone track is mixed unchanged and the reason is reported. Passing --reduce-bleed to a headphone recording is therefore harmless.
The microphone does not start delivering audio at the same instant system capture does; it typically lags by a fraction of a second while the input device spins up. Both tracks are timestamped from one SCStream, so record measures that gap from the first frame of each track and writes it to <name>-tracks.json. The mixdown then delays the later track by exactly that much. Without this the mix would place both files at t=0 and the microphone would run early by the startup gap, which is audible as a conversation that no longer lines up.
If the two timestamps differ by more than five seconds, they are treated as not comparable rather than as a real head start: the tracks are left unaligned and a warning is printed, on the grounds that a wrong shift is worse than no shift.
audiorec mix reads the sidecar automatically. If it is missing, as it is for recordings made before this existed, both tracks are assumed to start together and a note is printed. A sidecar that is malformed or from a newer schema version is ignored the same way. Override it either way with --mic-offset-ms:
audiorec mix session-system.caf session-mic.caf --mic-offset-ms 250 Mic started 250 ms late
audiorec mix session-system.caf session-mic.caf --mic-offset-ms=-80 Mic started 80 ms early
audiorec mix session-system.caf session-mic.caf --mic-offset-ms 0 Ignore the sidecar
A negative value has to be written with an equals sign, as --mic-offset-ms=-80. Separated by a space, the argument parser reads the leading minus as the start of another option and rejects the command.
Alignment also matters to --reduce-bleed, which correlates the two tracks' envelopes and cannot judge them correctly if they disagree about when they started.
- macOS 15 or later (developed and tested on macOS 26.5, Apple Silicon)
- Xcode toolchain with the macOS 15+ SDK (
swift buildfrom the command line is sufficient) - Swift 6.3 or later, since
Package.swiftdeclaresswift-tools-version: 6.3. An older toolchain fails while reading the manifest, before it compiles anything. - No third-party dependencies except swift-argument-parser, resolved automatically. The tests use swift-testing, which ships with the toolchain.
make
This builds the release binary, embeds the Info.plist into the __TEXT,__info_plist section (done by linker flags in Package.swift), strips its local symbols, code signs it, and verifies the signature, the embedded plist, and that the version in Sources/audiorec/AppVersion.swift matches CFBundleShortVersionString. The binary lands at .build/release/audiorec; copy it anywhere on your PATH.
Stripping runs before signing, since it rewrites the file and would otherwise invalidate the signature. It halves the binary, from 1.9 MB to 1.0 MB. strip -x is used rather than a full strip: the two are within 6 KB of each other in the compressed release tarball, but -x keeps global symbols so a crash report still names functions. Full debug info stays in .build/release/audiorec.dSYM regardless.
make test runs the signal-processing tests: the bleed reducer measured against synthetic tracks with a known amount of leakage, the mixdown's offset and gain arithmetic, and the alignment sidecar. The capture path is not covered, since it needs live permission grants.
Build from the package root. The linker flag that embeds Resources/Info.plist uses a path relative to the package root, so swift build invoked from another directory will fail to find it.
Signing uses an ad-hoc identity by default because that requires no certificate. Ad-hoc signing changes the binary's signature on every rebuild, and macOS then treats each build as a different program, which re-triggers the permission prompts. To keep permission grants across rebuilds, sign with a stable identity:
make IDENTITY="Apple Development: Your Name (TEAMID)"
Any codesigning certificate in your keychain works, including a self-signed one created with Keychain Access (Certificate Assistant > Create a Certificate > Code Signing).
- ScreenCaptureKit has no audio-only mode. A content filter over a display is required, so an unavoidable video stream runs alongside the audio; it is configured to 2x2 pixels at one frame per second and its frames are discarded.
- Screen Recording permission is required even for microphone-only capture, because the microphone is captured through the same
SCStream. - System audio delivery is capped at 48 kHz by ScreenCaptureKit.
- The tool's own process audio is excluded from system capture, so its output can never feed back into the recording.
--reduce-bleedis a ducker, not echo cancellation. It cannot remove bleed from underneath your own voice, so overlapping speech keeps some doubling; see Speaker bleed.- Mixing needs both tracks.
--mixis rejected alongside--no-micor--no-system, and there is no way to transcode a single track to m4a, so a system-audio-only or microphone-only recording stays a CAF file. - Track files are never removed after a mix. Lossless 32-bit float CAF runs about 21 MB per minute per track at 48 kHz stereo, so an hour-long call with both tracks leaves roughly 2.5 GB behind on top of the mix. Delete them yourself once you are satisfied with the mix.
- Ad-hoc signed builds re-trigger permission prompts after every rebuild; see Build and sign.
- A bare
swift buildbinary works, but onlymake(or an equivalent codesign invocation) produces the signed binary with the embedded bundle identifier that keeps TCC grants attached to the tool rather than to the invoking terminal.
This tool records without the in-call recording indicator that conferencing applications display for their own recording features. Recording participants generally requires their consent, and in some jurisdictions the consent of every party on the call. You are responsible for complying with the laws that apply to you.
0 comments
log in to comment.