Python wrapper for Apple's Core Audio Tap API
Project description
catap
A Python wrapper for Apple's Core Audio Tap API (macOS 14.2+). Capture audio from any application without loopback drivers or virtual audio devices.
Install
pip install catap # macOS 14.2+, Python 3.11+
catap is macOS-only. On other platforms, imports raise an ImportError.
Free-threaded CPython 3.13t and 3.14t builds are supported on macOS.
Tested by the author on an Apple Silicon M5 MacBook Pro running macOS Tahoe 26.2. The package is intended for macOS 14.2 and newer, but Core Audio tap behavior is still under-documented by Apple. Reports from other Macs and macOS versions are welcome.
Quick start
CLI:
catap record Safari -d 10 -o safari.wav # record an app for 10 seconds
catap record --system -d 10 -o mix.wav # record the full system mix
catap list-apps # see what's producing audio
Python:
from catap import record_process
session = record_process("Safari", output_path="safari.wav")
session.record_for(10)
print(f"Recorded {session.duration_seconds:.2f} seconds")
What It Can Do
- Record a single app, the full system mix, or an existing visible tap.
- Exclude selected apps from a system recording.
- Mute an app while recording it, so it is captured but not played aloud.
- Build taps for a specific hardware device stream.
- Write WAV files, or stream PCM buffers to your own callback.
- Keep the audio queue bounded. If the worker falls behind, dropped buffers are reported when recording stops.
Usage
Command Line
The CLI is intentionally small. These are the commands I use most often:
catap list-apps
catap list-apps --all
catap record Safari -d 30 -o safari.wav
catap record Spotify --mute -d 60 -o spotify.wav
catap record --system -e Music -e Zoom -d 30 -o system.wav
Run catap record --help for the full set of recording options.
Python API
from catap import record_process
# High-level API: catap manages tap creation, startup, shutdown, and cleanup.
session = record_process("Safari", output_path="output.wav")
session.record_for(5)
print(f"Recorded {session.duration_seconds:.2f} seconds")
If you use on_data=..., the callback runs on catap's background worker
thread so the Core Audio callback can stay lightweight.
If you want to control the recording lifetime yourself, use the session as a context manager:
import time
from catap import record_process
with record_process("Safari", output_path="output.wav", mute=False) as session:
time.sleep(5)
print(f"Recorded {session.duration_seconds:.2f} seconds")
If you want streaming-only mode, pass on_data=... and omit output_path.
By default, catap queues up to 256 pending audio buffers before treating a
slow writer or callback as a capture failure. You can tune this with
max_pending_buffers=... on record_process, record_system_audio,
RecordingSession, or AudioRecorder.
If a process query matches more than one audio process, catap reports the
candidate processes instead of picking one arbitrarily.
Mute Behavior
For record_process(..., mute=True), the app stays muted for the lifetime of
the recording session. The two underlying modes (MUTED and
MUTED_WHEN_TAPPED) have different lifecycle semantics. See
docs/mute-behavior.md for empirical probe results
and when each mode transitions between audible and inaudible.
Low-level API
For advanced use cases, the low-level API is still available:
from catap import (
AudioRecorder,
TapDescription,
TapMuteBehavior,
create_process_tap,
destroy_process_tap,
find_process_by_name,
list_audio_taps,
record_tap,
)
process = find_process_by_name("Safari")
print(f"Found: {process.name} (PID: {process.pid})")
tap_desc = TapDescription.stereo_mixdown_of_processes([process.audio_object_id])
tap_desc.name = "My Recording"
tap_desc.mute_behavior = TapMuteBehavior.UNMUTED # or MUTED
tap_id = create_process_tap(tap_desc)
recorder = AudioRecorder(tap_id, "output.wav")
recorder.start()
import time
time.sleep(5)
recorder.stop()
print(f"Recorded {recorder.duration_seconds:.2f} seconds")
destroy_process_tap(tap_id)
If another app has already created a non-private tap, you can discover it and attach a recorder without taking ownership of the tap itself:
from catap import list_audio_taps, record_tap
tap = next(tap for tap in list_audio_taps() if tap.name == "Shared Mix")
session = record_tap(tap, output_path="shared-mix.wav")
session.record_for(5)
Device-targeted taps can be built directly from discovered hardware streams:
from catap import TapDescription, find_process_by_name, list_audio_devices
process = find_process_by_name("Safari")
device = next(device for device in list_audio_devices() if device.is_default_output)
stream = device.output_streams[0]
tap_desc = TapDescription.of_processes_for_device_stream(
[process.audio_object_id],
stream,
)
tap_desc.name = "Safari on default speakers"
Permissions
Core Audio Tap requires audio capture permissions. The first time you record, macOS will prompt for permission.
If you run from a terminal (for example uv run catap record Spotify), macOS
attributes audio capture to that terminal app. Grant permission to Terminal,
iTerm, or whichever host app is launching catap.
Permission Troubleshooting
If recording fails with permission errors:
- Check System Settings > Privacy & Security > Screen & System Audio Recording
- Ensure the app launching
cataphas permission (Terminal, iTerm, etc.) - Retry recording from the same terminal app after granting access
How It Works
- Process enumeration: uses Core Audio's
kAudioHardwarePropertyProcessObjectListto find audio processes. - Tap creation: creates a
CATapDescriptionvia PyObjC and callsAudioHardwareCreateProcessTap. - Aggregate device setup: wraps the tap in an aggregate device, which Core Audio requires before a tap can be read.
- Audio capture: registers an
AudioDeviceIOProccallback to receive audio buffers. - WAV output: uses Core Audio
AudioConverterto convert float32 audio to 16-bit PCM before writing WAV output.
For Core Audio implementation notes (header locations, tap property codes,
aggregate-device dictionary keys, references), see
docs/core-audio-notes.md.
For the recorder's callback and queueing design, see
docs/performance.md.
Interactive lab
For a Tkinter lab that exercises process browsing, mute modes, callback streaming, shared-tap attachment, device-stream-targeted taps, and a built-in helper tone launcher, run:
uv sync --group dev
uv run python scripts/catap_core_lab.py
Development
git clone https://github.com/sbetko/catap.git
cd catap
uv sync --group dev
Quality checks
uv run --group dev ruff check .
uv run --group dev ty check --error-on-warning src tests
uv run --group dev pytest
uv run --group dev python -m build
uv run --group dev twine check dist/*
Free-threaded checks:
uv python install 3.13t 3.14t
uv run --python 3.13t --group dev pytest
uv run --python 3.14t --group dev pytest
CATAP_RUN_INTEGRATION=1 uv run --python 3.14t --group dev pytest \
tests/test_integration.py::test_record_system_audio_smoke
Optional integration smoke test
CATAP_RUN_INTEGRATION=1 uv run --group dev pytest -m integration
This opt-in smoke test exercises the real macOS Core Audio bridge without making the default test suite flaky. It covers both process enumeration and a short real recording that verifies tap startup, shutdown, and WAV finalization.
Advanced signal-oracle testing with the internal tone fixtures is documented in
docs/headless-signal-fixtures.md.
See RELEASE.md for the release checklist.
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file catap-0.4.2.tar.gz.
File metadata
- Download URL: catap-0.4.2.tar.gz
- Upload date:
- Size: 44.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
450241c75578a6dd67d4e6e837f8340c5b0a26701294114e5a38d26cdd2b0b87
|
|
| MD5 |
8adb369bee46481fd045efb01f7fa075
|
|
| BLAKE2b-256 |
ef9427809335ca2fc4f8ccf443931d36e5374dcf07c2b96b8c24f3f6a1b2a538
|
Provenance
The following attestation bundles were made for catap-0.4.2.tar.gz:
Publisher:
publish.yml on sbetko/catap
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
catap-0.4.2.tar.gz -
Subject digest:
450241c75578a6dd67d4e6e837f8340c5b0a26701294114e5a38d26cdd2b0b87 - Sigstore transparency entry: 1392111588
- Sigstore integration time:
-
Permalink:
sbetko/catap@bc2aa6dac4584afc2b087e0583b02d9b323d783c -
Branch / Tag:
refs/tags/v0.4.2 - Owner: https://github.com/sbetko
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@bc2aa6dac4584afc2b087e0583b02d9b323d783c -
Trigger Event:
release
-
Statement type:
File details
Details for the file catap-0.4.2-py3-none-any.whl.
File metadata
- Download URL: catap-0.4.2-py3-none-any.whl
- Upload date:
- Size: 54.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
819ba5a6b242aa0927c5a8a277da327b19dcf71f2543e0f98c1e2a74b8d41866
|
|
| MD5 |
281def0614c7fdca616edb6ae698b14e
|
|
| BLAKE2b-256 |
d69ab7351cc4cb4da326fe6a20076af4c482a925e85e4f89d2bedfc23e800eb3
|
Provenance
The following attestation bundles were made for catap-0.4.2-py3-none-any.whl:
Publisher:
publish.yml on sbetko/catap
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
catap-0.4.2-py3-none-any.whl -
Subject digest:
819ba5a6b242aa0927c5a8a277da327b19dcf71f2543e0f98c1e2a74b8d41866 - Sigstore transparency entry: 1392111633
- Sigstore integration time:
-
Permalink:
sbetko/catap@bc2aa6dac4584afc2b087e0583b02d9b323d783c -
Branch / Tag:
refs/tags/v0.4.2 - Owner: https://github.com/sbetko
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@bc2aa6dac4584afc2b087e0583b02d9b323d783c -
Trigger Event:
release
-
Statement type: