Skip to main content

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:

  1. Check System Settings > Privacy & Security > Screen & System Audio Recording
  2. Ensure the app launching catap has permission (Terminal, iTerm, etc.)
  3. Retry recording from the same terminal app after granting access

How It Works

  1. Process enumeration: uses Core Audio's kAudioHardwarePropertyProcessObjectList to find audio processes.
  2. Tap creation: creates a CATapDescription via PyObjC and calls AudioHardwareCreateProcessTap.
  3. Aggregate device setup: wraps the tap in an aggregate device, which Core Audio requires before a tap can be read.
  4. Audio capture: registers an AudioDeviceIOProc callback to receive audio buffers.
  5. WAV output: uses Core Audio AudioConverter to 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

catap-0.4.2.tar.gz (44.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

catap-0.4.2-py3-none-any.whl (54.3 kB view details)

Uploaded Python 3

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

Hashes for catap-0.4.2.tar.gz
Algorithm Hash digest
SHA256 450241c75578a6dd67d4e6e837f8340c5b0a26701294114e5a38d26cdd2b0b87
MD5 8adb369bee46481fd045efb01f7fa075
BLAKE2b-256 ef9427809335ca2fc4f8ccf443931d36e5374dcf07c2b96b8c24f3f6a1b2a538

See more details on using hashes here.

Provenance

The following attestation bundles were made for catap-0.4.2.tar.gz:

Publisher: publish.yml on sbetko/catap

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for catap-0.4.2-py3-none-any.whl
Algorithm Hash digest
SHA256 819ba5a6b242aa0927c5a8a277da327b19dcf71f2543e0f98c1e2a74b8d41866
MD5 281def0614c7fdca616edb6ae698b14e
BLAKE2b-256 d69ab7351cc4cb4da326fe6a20076af4c482a925e85e4f89d2bedfc23e800eb3

See more details on using hashes here.

Provenance

The following attestation bundles were made for catap-0.4.2-py3-none-any.whl:

Publisher: publish.yml on sbetko/catap

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page