Skip to main content

listentome

listentome is an async-first audio I/O library for Python, built on PortAudio.

The key features are:

  • Async native: streams are async context managers and async iterators. No callbacks, no queues, no call_soon_threadsafe.
  • Bytes in, bytes out: audio blocks are plain bytes. No NumPy requirement, no raw/array duality.
  • No global state: no mutable module defaults. You pass the device, samplerate, and dtype explicitly.
  • Explicit backpressure: you decide what happens when the consumer falls behind - drop or raise.
  • Testable without hardware: every stream accepts a Backend protocol, so tests inject a fake and never touch a device.
  • Sync facade: play() and record() for scripts, built on the same async core.

Requirements

The PortAudio system library:

brew install portaudio        # macOS
apt install libportaudio2     # Debian/Ubuntu

Installation

uv add listentome

Example

Record

Create a file main.py with:

import anyio

import listentome as ltm


async def main() -> None:
    async with ltm.InputStream(samplerate=48_000, channels=1) as stream:
        async for block in stream:
            print(f"got {len(block)} bytes")


anyio.run(main)

Run it:

uv run main.py

That's it. The stream opens the default microphone, and each iteration gives you one block of raw float32 samples as bytes. Pass dtype="int16" (or int32, int8, uint8) for a different sample format, and blocksize to control how many frames each block carries.

Play

write() returns once the device has consumed the audio:

import anyio

import listentome as ltm


async def main() -> None:
    tone = bytes(48_000 * 4)  # one second of float32 silence
    async with ltm.OutputStream(samplerate=48_000, channels=1) as stream:
        await stream.write(tone)


anyio.run(main)

Duplex

DuplexStream captures and plays through the same device pair - read() (or async for) for capture, write() for playback:

import anyio

import listentome as ltm


async def main() -> None:
    async with ltm.DuplexStream(samplerate=48_000, channels=1) as stream:
        async for block in stream:
            await stream.write(block)


anyio.run(main)

Use with Pydantic AI

This is what listentome is built for: realtime speech-to-speech agents without audio plumbing.

With callback-based libraries, wiring a microphone to a realtime session takes a PortAudio callback, a thread-safe queue, loop.call_soon_threadsafe, and a hand-rolled drop-oldest policy. With listentome, the microphone is an async iterator and the speaker is an awaitable - the plumbing disappears into the library:

import anyio

import listentome as ltm
from pydantic_ai import Agent, PartDeltaEvent, SpeechPartDelta

# OpenAI's realtime models speak and listen in 24 kHz mono PCM16 audio.
SAMPLE_RATE = 24_000
BLOCK_SIZE = 2_400  # 100 ms per audio block

agent = Agent(
    instructions="You are a friendly voice assistant. Keep your replies short and conversational."
)


@agent.tool_plain
def get_weather(city: str) -> str:
    """Look up the current weather in a city."""
    return f"It is currently 21 degrees and sunny in {city}."


async def main() -> None:
    mic = ltm.InputStream(samplerate=SAMPLE_RATE, channels=1, dtype="int16", blocksize=BLOCK_SIZE)
    speaker = ltm.OutputStream(samplerate=SAMPLE_RATE, channels=1, dtype="int16", blocksize=BLOCK_SIZE)

    async with (
        agent.realtime("openai:gpt-realtime").session() as session,
        mic,
        speaker,
        anyio.create_task_group() as tg,
    ):

        async def stream_mic() -> None:
            async for block in mic:
                await session.send_audio(block)

        tg.start_soon(stream_mic)
        print("Listening - start talking (Ctrl-C to quit).")

        async for event in session:
            match event:
                case PartDeltaEvent(delta=SpeechPartDelta(audio_chunk=chunk)) if chunk:
                    await speaker.write(chunk)

        tg.cancel_scope.cancel()


anyio.run(main)

The mic loop is async for block in mic - the drop-oldest queue is the stream's own overflow policy. The speaker is await speaker.write(chunk) - write() suspends until the device consumed the audio, so the model's audio never runs unboundedly ahead of what the user hears.

Devices

There is no global default state. Query devices explicitly and pass an index to the stream:

import listentome as ltm

for device in ltm.devices():
    print(device.index, device.name, device.max_input_channels, device.max_output_channels)

ltm.default_input() and ltm.default_output() return the system defaults, or None when no device exists. Device is a frozen dataclass - there is nothing to mutate.

Sync facade

For scripts that do not need an event loop:

import listentome as ltm

data = ltm.record(2.0, samplerate=48_000, channels=1)
ltm.play(data, samplerate=48_000, channels=1)

These run the async streams on an internal anyio portal. There is a single implementation underneath - the sync functions are conveniences, not a parallel API.

Testing without hardware

Every stream and query function accepts a backend argument satisfying the Backend protocol:

class Backend(Protocol):
    def devices(self) -> list[Device]: ...
    def default_input(self) -> Device | None: ...
    def default_output(self) -> Device | None: ...
    def open(self, ...) -> RawStream: ...

Inject a fake backend in tests to drive streams without any audio device - including simulated device failures your real hardware would never produce. See tests/fake_backend.py for a reference implementation; listentome's own suite runs at 100% coverage without touching a device.

License

MIT

Metadata

Release files for listentome 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for listentome 0.1.0
File Size Uploaded Provenance
listentome-0.1.0.tar.gz 67.5 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for listentome 0.1.0
File Interpreter ABI Platform
listentome-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 78.1 kB

Release files / listentome-0.1.0.tar.gz

Download URL listentome-0.1.0.tar.gz
Size 67.5 kB
Tags Source
SHA-256 checksum
How to use checksums
0dfb5db8da7d1999c019785c0c3b81d3ad4949d925214772c6601f3a167acb7c
BLAKE2b-256 checksum
How to use checksums
af50b7041034d6c6af1e1373f1ea3505ddfa500b62dc63f147fed8dfcbb0970b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.9.12 {"installer":{"name":"uv","version":"0.9.12"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / listentome-0.1.0-py3-none-any.whl

Download URL listentome-0.1.0-py3-none-any.whl
Size 10.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b656247c13c408d05db8051e8dcea4ba4caf96cb11f392aab5fda0fe186c0619
BLAKE2b-256 checksum
How to use checksums
06f98cf1fd057313335d946e7755dd29b2c22bc98df5c3313652c443b4886149
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.9.12 {"installer":{"name":"uv","version":"0.9.12"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for listentome 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page