Skip to main content

Python toolkit for macOS app audio capture and ASR/transcription pipelines

Project description

macloop

CI PyPI TestPyPI codecov License

🎙️ Build programmable macOS app audio capture and speech-to-text pipelines in Python without routing your whole machine through a virtual driver.

macloop is a Python-first macOS audio capture toolkit backed by a real-time Rust engine. It lets you capture microphones, system audio, or individual applications, route one stream into multiple consumers, apply processors, and feed the results into sinks such as ASR (speech-to-text), transcription, and WAV recording from one in-process API.

If you are looking for a BlackHole alternative for programmable Python workflows, macloop focuses on in-process app audio capture and recording/transcription pipelines instead of system-wide virtual-device routing.

Why it is easy to adopt

  • Supports Python 3.9+
  • Install with a standard command: pip install macloop
  • No extra user-facing setup beyond normal installation and macOS permissions

✨ Why macloop?

Virtual devices such as BlackHole are useful when you need a system-wide virtual audio device. macloop targets a different workflow: programmable capture pipelines inside an application.

Typical use cases:

  • Capture audio from a single macOS application such as Zoom
  • Stream audio into ASR or speech-to-text systems in Python
  • Record and transcribe meetings from one pipeline
  • Replace manual BlackHole-style routing with an in-process Python API

macloop vs virtual-driver workflows

Capability macloop BlackHole-style virtual driver
Capture microphone audio
Capture system audio
Capture a single app (for example Zoom) ❌ typically not directly
Route one stream into several consumers ❌ external wiring needed
Run processors in the capture pipeline ❌ outside the driver
Voice-processed microphone path ✅ via vpio_enabled=True ❌ not provided by the driver itself
Noise suppression / echo cancellation as part of the pipeline ⚠️ pipeline-ready, but not exposed as built-in public nodes yet ❌ external tooling required
Feed Python ASR chunks directly ❌ extra bridge required
Record and transcribe the same meeting at once ⚠️ possible, but usually with extra routing glue
Requires changing your default output device often ✅
Requires a virtual audio device to be installed

Why this matters: if your goal is “capture, transform, split, and consume audio in Python”, macloop removes a lot of the manual patch-bay work.


🧱 Tech Stack

Layer Technology
Public API Python
Native bindings PyO3
Audio engine Rust
macOS capture backends CoreAudio, ScreenCaptureKit
Array transport to Python NumPy

🧩 What You Can Build

macloop is designed as a modular pipeline:

Source -> Processor(s) -> Route(s) -> Sink(s)

Examples:

  • Record a meeting to WAV while streaming microphone chunks to an ASR engine.
  • Capture only Zoom audio instead of the entire system mix.
  • Split one microphone stream into separate routes for transcription, monitoring, and archival recording.
  • Build deterministic tests with a synthetic source before touching real devices.

Current building blocks

Category Available today
Sources MicrophoneSource, SystemAudioSource, AppAudioSource, SyntheticSource
Processors GainProcessor
Sinks AsrSink, WavSink
ASR delivery sync iteration and asyncio iteration
Output formats AsrSink: f32 / i16, mono or stereo
Metrics engine.stats(), asr_sink.stats(), wav_sink.stats()

🚀 Installation

1. Create a virtual environment

python -m venv .venv
source .venv/bin/activate

2. Upgrade packaging tools

python -m pip install --upgrade pip

3. Install macloop

pip install macloop

Requirements

  • macOS
  • Python 3.9+

That is the full user-facing requirement for installation: create a normal Python environment and run pip install macloop.


▶️ Quick Start

The example below creates a small audio graph:

  • capture the microphone
  • apply a gain processor
  • split the stream into two routes
  • record one route to WAV
  • send the other route to an ASR sink
import macloop


with macloop.AudioEngine() as engine:
    mic = engine.create_stream(
        macloop.MicrophoneSource,
        device_id=None,
        vpio_enabled=True,
    )

    engine.add_processor(
        stream=mic,
        processor=macloop.GainProcessor(gain=1.2),
    )

    mic_for_asr = engine.route("mic_for_asr", stream=mic)
    mic_for_wav = engine.route("mic_for_wav", stream=mic)

    wav_sink = macloop.WavSink(route=mic_for_wav, file="out/mic.wav")
    asr_sink = macloop.AsrSink(
        routes=[mic_for_asr],
        chunk_frames=320,
        sample_rate=16_000,
        channels=1,
        sample_format="f32",
    )

    for chunk in asr_sink.chunks():
        print(chunk.route_id, chunk.frames, chunk.samples.dtype)
        break

    asr_sink.close()
    wav_sink.close()

AudioChunk.samples is a NumPy array:

  • np.float32 for sample_format="f32"
  • np.int16 for sample_format="i16"

🎧 Real Example: Record And Transcribe A Meeting

This is the workflow macloop is built for: one pipeline, multiple outputs.

import macloop


def find_zoom_pids() -> list[int]:
    pids = []
    for app in macloop.AppAudioSource.list_applications():
        if "zoom" in app["name"].lower():
            pids.append(int(app["pid"]))
    if not pids:
        raise RuntimeError("Zoom is not running")
    return pids


with macloop.AudioEngine() as engine:
    mic = engine.create_stream(macloop.MicrophoneSource, vpio_enabled=True)
    zoom = engine.create_stream(macloop.AppAudioSource, pids=find_zoom_pids())

    mic_for_asr = engine.route("mic_for_asr", stream=mic)
    zoom_for_asr = engine.route("zoom_for_asr", stream=zoom)
    mic_for_wav = engine.route("mic_for_wav", stream=mic)
    zoom_for_wav = engine.route("zoom_for_wav", stream=zoom)

    wav_sink = macloop.WavSink(
        routes=[mic_for_wav, zoom_for_wav],
        file="out/meeting.wav",
    )

    asr_sink = macloop.AsrSink(
        routes=[mic_for_asr, zoom_for_asr],
        chunk_frames=320,
        sample_rate=16_000,
        channels=1,
        sample_format="f32",
    )

    # Long-running pipeline: keep consuming until your app decides to stop.
    for chunk in asr_sink.chunks():
        print(chunk.route_id, chunk.frames)
        # Send chunk.samples into your ASR engine here.

Notes:

  • AsrSink emits independent chunks per route.
  • WavSink can mix several routes into one file.
  • If mix_gain is not provided, WavSink uses 1 / N by default.

⚡ Asyncio

AsrSink also supports async consumption:

import asyncio
import macloop


async def main() -> None:
    with macloop.AudioEngine() as engine:
        mic = engine.create_stream(macloop.MicrophoneSource, vpio_enabled=True)
        mic_for_asr = engine.route(stream=mic)

        with macloop.AsrSink(
            routes=[mic_for_asr],
            chunk_frames=320,
            sample_rate=16_000,
            channels=1,
            sample_format="f32",
        ) as asr_sink:
            async for chunk in asr_sink.chunks_async():
                print(chunk.route_id, chunk.frames)
                break


asyncio.run(main())

🔎 Device Discovery

Microphones

import macloop

for mic in macloop.MicrophoneSource.list_devices():
    print(mic["id"], mic["name"], mic["is_default"])

Displays

import macloop

for display in macloop.SystemAudioSource.list_displays():
    print(display["id"], display["name"], display["width"], display["height"])

Applications

import macloop

for app in macloop.AppAudioSource.list_applications():
    print(app["pid"], app["name"], app["bundle_id"])

If engine.create_stream(macloop.SystemAudioSource, ...) is called without an explicit display_id, macloop uses the first available display.

engine.create_stream(macloop.AppAudioSource, ...) requires explicit pids. Use AppAudioSource.list_applications() to choose one or more target applications first.


🛠️ Example Scripts

The scripts below live in this repository, so run them from a source checkout.

Record microphone audio to WAV

python examples/write_to_wav.py --seconds 5 --output out/mic.wav

Stream microphone audio into Sherpa ONNX

uv run --with sherpa-onnx --with huggingface_hub --reinstall-package macloop \
  python examples/sherpa_asr_demo.py --seconds 5

📊 Telemetry

macloop exposes metrics at different levels of the pipeline:

  • engine.stats() for per-stream real-time pipeline and processor metrics
  • asr_sink.stats() for per-route ASR sink metrics
  • wav_sink.stats() for WAV writer metrics

This makes it possible to inspect latency and drops at the node level instead of relying only on a single average number.


🗺️ Roadmap

  • Add more built-in processors beyond GainProcessor
  • Add zero-copy / lease-release delivery for Python
  • Add richer pipeline examples for meeting bots and voice agents
  • Add WebRTC AEC in a future iteration, with a routing model that can handle capture and reference streams cleanly

📄 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

macloop-0.3.0.tar.gz (64.6 kB view details)

Uploaded Source

Built Distributions

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

macloop-0.3.0-cp39-abi3-macosx_11_0_x86_64.whl (718.0 kB view details)

Uploaded CPython 3.9+macOS 11.0+ x86-64

macloop-0.3.0-cp39-abi3-macosx_11_0_arm64.whl (675.6 kB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

File details

Details for the file macloop-0.3.0.tar.gz.

File metadata

  • Download URL: macloop-0.3.0.tar.gz
  • Upload date:
  • Size: 64.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for macloop-0.3.0.tar.gz
Algorithm Hash digest
SHA256 8816791b5dcfe357a93a84bdd1b052807fdde8637b6a19a05769f49cab1f9f5c
MD5 1eaf3df562cdae596ee1b500db711208
BLAKE2b-256 3cdd3aad5f20503faabd7212829af11268da9e64098b6fb4a589f8fe148e57ae

See more details on using hashes here.

Provenance

The following attestation bundles were made for macloop-0.3.0.tar.gz:

Publisher: publish.yml on kemsta/macloop

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

File details

Details for the file macloop-0.3.0-cp39-abi3-macosx_11_0_x86_64.whl.

File metadata

File hashes

Hashes for macloop-0.3.0-cp39-abi3-macosx_11_0_x86_64.whl
Algorithm Hash digest
SHA256 fb262b1576d98ed2fe1288a8bc59b2f88c80f6532fc8c2f18c4da1f661bad578
MD5 4292867344f31ab32afd7d45ddfb68f3
BLAKE2b-256 a22eb39e625f0ae060ed94fabb3c37ef2e5fc78a0aaabf97d5b8158eee9cffd8

See more details on using hashes here.

Provenance

The following attestation bundles were made for macloop-0.3.0-cp39-abi3-macosx_11_0_x86_64.whl:

Publisher: publish.yml on kemsta/macloop

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

File details

Details for the file macloop-0.3.0-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for macloop-0.3.0-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 f846312b850ec62f64de6ebde75693cc08293eaf56333e9876f37351509f40ca
MD5 411cd975b8ccabd33327c387fe0e4596
BLAKE2b-256 2ddc0fef51a3ea0efcd6505030812c2abd156d30690fe85a58b1551611d8396c

See more details on using hashes here.

Provenance

The following attestation bundles were made for macloop-0.3.0-cp39-abi3-macosx_11_0_arm64.whl:

Publisher: publish.yml on kemsta/macloop

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