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
Backendprotocol, so tests inject a fake and never touch a device. - Sync facade:
play()andrecord()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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| listentome-0.1.0.tar.gz | 67.5 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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.