PyAudioCast
Cross-platform audio output library for Python, powered by Rust and CPAL.
Stream audio to any output device — including PipeWire/PulseAudio virtual sinks on Linux — with a simple Python API backed by a high-performance Rust core.
Features
- Cross-platform: Linux (ALSA/PipeWire/PulseAudio), Windows (WASAPI), macOS (CoreAudio)
- Auto-detect: Sample rate and channels are detected from the device — zero config needed
- Auto-negotiate: If the device can't take your sample rate or channel count, audio is resampled and remixed for it (e.g. 22050Hz mono TTS → 48000Hz stereo device)
- Device selection: Select output devices by id or name, including PipeWire virtual sinks
- Streaming audio: Write audio in chunks into a ring buffer drained by a lock-free audio callback — ideal for real-time TTS, generative audio, live effects, etc.
- Unified
write(): Acceptsbytes,numpyarrays (int16/int32/float32/float64), orlist[float]— format is detected automatically - Thread-safe interruption:
clear()from any thread instantly discards buffered audio (including awrite()in progress) and unblocksdrain()— perfect for voice assistant barge-in - Context manager: Clean resource management with
withstatement - GIL-friendly: Blocking writes,
drain()and device setup release the Python GIL (except for the ~20 ms it takes to open a PipeWire/PulseAudio sink); Ctrl+C interrupts blocking writes anddrain() - Typed: Ships type stubs
- Debug logging: Enable detailed logs with
PYAUDIOCAST_LOG=debug
Installation
From PyPI
pip install pyaudiocast
Wheels are built for Python 3.9+ (one abi3 wheel per platform) on Linux x86_64, macOS (Intel and Apple Silicon) and Windows x86_64. numpy is optional.
Note (Linux): Pre-built wheels require ALSA (
libasound.so.2). Install it if not already present:sudo apt install libasound2t64 # Debian 13/Ubuntu 24.04+ (libasound2 on older releases) sudo dnf install alsa-lib # Fedora
From source (requires Rust 1.88+)
# Install Rust if needed
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Linux: install ALSA development headers
sudo apt install libasound2-dev # Debian/Ubuntu
sudo dnf install alsa-lib-devel # Fedora
# Clone and install into a virtual environment
git clone https://github.com/nicokim/PyAudioCast.git
cd PyAudioCast
python -m venv .venv && source .venv/bin/activate
pip install .
Development setup (with uv)
uv venv && source .venv/bin/activate
uv pip install maturin numpy pytest
maturin develop
Quick Start
import pyaudiocast
# List all output devices
for dev in pyaudiocast.list_output_devices():
print(f"[{dev['index']}] {dev['name']} ({dev['type']}, id={dev['id']})")
# Stream audio — sample rate and channels auto-detected from device
with pyaudiocast.AudioPlayer() as player:
print(f"Using: {player.sample_rate}Hz, {player.channels}ch")
player.write(audio_bytes) # bytes, numpy array, or list[float]
player.drain() # wait for playback to finish
# Use your source's format; it is converted for the device if needed
with pyaudiocast.AudioPlayer(sample_rate=22050, channels=1) as player:
player.write(tts_audio)
player.drain()
# Stream to a specific device
with pyaudiocast.AudioPlayer(device="Virtual-Mic") as player:
player.write(samples)
player.drain()
# One-shot WAV playback
pyaudiocast.play_file("audio.wav", device="pulse")
Streaming with Interruption
For real-time applications like voice assistants, stream from one thread and interrupt from another:
import threading
import pyaudiocast
player = pyaudiocast.AudioPlayer(sample_rate=22050, channels=1)
interrupted = threading.Event()
def speak(tts_stream):
interrupted.clear()
for chunk in tts_stream:
if interrupted.is_set():
break
player.write(chunk) # blocks while the buffer is full
player.drain() # wait for playback to finish
# From another thread, e.g. when the user starts speaking:
def on_user_speech_detected():
interrupted.set() # stop producing this response...
player.clear() # ...and silence what's already buffered
clear() discards all buffered audio immediately. A write() in progress in another thread drops the rest of its data and returns, and a blocked drain() returns right away. Later write() calls start a new stream — so stop your producer loop too, as above, or it will keep feeding the rest of the interrupted response.
Supported Audio Formats
write() auto-detects the input format:
| Input type | Format | Conversion |
|---|---|---|
bytes / bytearray |
int16 little-endian PCM | Converted to float32 |
list[float] |
float32 samples (-1.0 to 1.0) | Direct |
numpy.ndarray (int16) |
int16 samples | Converted to float32 |
numpy.ndarray (int32) |
int32 samples | Converted to float32 |
numpy.ndarray (float32) |
float32 samples | Direct (no conversion) |
numpy.ndarray (float64) |
float64 samples | Converted to float32 |
Multi-channel audio is interleaved (L R L R ...). numpy arrays can be 1-D interleaved or 2-D shaped (frames, channels) (as returned by e.g. soundfile.read), contiguous or not. Chunks don't need to be aligned: a write may end mid-sample or mid-frame (as streamed HTTP bodies do), and the remainder is completed by the next write. drain() and clear() end the stream: a remainder still pending then (a stream cut off mid-sample) is dropped, so the next stream starts aligned. Float samples are clipped to -1.0..1.0, and NaN/inf become silence.
import numpy as np
with pyaudiocast.AudioPlayer(channels=1) as player:
# All of these work with the same write() method
player.write(b"\x00\x00" * 100) # bytes (int16 LE)
player.write(np.zeros(100, dtype=np.int16)) # numpy int16
player.write(np.zeros(100, dtype=np.float32)) # numpy float32 (fastest)
player.write(np.zeros(100, dtype=np.float64)) # numpy float64
player.write(np.zeros(100, dtype=np.int32)) # numpy int32
player.write([0.0] * 100) # list[float]
API Reference
pyaudiocast.list_output_devices() -> list[dict]
Returns a list of available output devices. Each dict contains:
name(str): Human-readable device name (not unique: one sound card can have several devices)id(str): Unique device id, e.g."pulse"or"hw:CARD=Audio,DEV=0"on ALSA,"pipewire:Virtual-Mic"for a sink — pass it asdevice=to select exactly this deviceindex(int): Position in the listtype(str):"pipewire"for PipeWire/PulseAudio sinks, otherwise the audio host:"alsa","wasapi"or"coreaudio"
pyaudiocast.AudioPlayer(device=None, sample_rate=None, channels=None)
Streaming audio player with ring buffer.
| Parameter | Type | Default | Description |
|---|---|---|---|
device |
str | None |
None |
Device id or name, PipeWire sink name, or None for the default |
sample_rate |
int | None |
None |
Sample rate of your audio in Hz, or None to use the device's |
channels |
int | None |
None |
Channel count of your audio, or None to use the device's |
If sample_rate or channels don't match what the device supports, pyaudiocast converts to the device's configuration. Invalid values raise ValueError.
Methods (all safe to call from any thread, or from a signal handler):
| Method | Description |
|---|---|
write(data) |
Queue audio data (bytes, numpy array, or list[float]). Blocks while the ~4 s buffer is full |
drain() |
Block until the audio queued so far has been played, device latency included (audio written meanwhile isn't waited for). Returns early on clear()/stop() |
clear() |
Discard buffered audio (with a 5 ms fade-out, so it doesn't click), abort an in-progress write(), unblock drain() |
stop() |
Stop playback and release the device (idempotent) |
write() and drain() raise RuntimeError if the device fails or disappears (for example when unplugged), and KeyboardInterrupt on Ctrl+C.
Properties:
| Property | Type | Description |
|---|---|---|
sample_rate |
int |
Requested sample rate (what you send) |
channels |
int |
Requested channel count (what you send) |
device_sample_rate |
int |
Actual device sample rate (what plays) |
device_channels |
int |
Actual device channel count (what plays) |
is_active |
bool |
Whether the player is open and its stream healthy |
Context manager: Supports with statement (calls stop() on exit).
pyaudiocast.play_file(path, device=None)
Play a WAV file (str or path-like) to completion, streaming it from disk. Blocks until playback is done.
Device Selection
device is matched in this order: exact id (pipewire:<sink> for a sink), exact name, exact PipeWire/PulseAudio sink name, then a case-insensitive substring of a sink name, device id or device name.
Default device
player = pyaudiocast.AudioPlayer() # uses system default
By id or name
player = pyaudiocast.AudioPlayer(device="pulse") # ALSA device id
player = pyaudiocast.AudioPlayer(device="hw:CARD=Audio,DEV=0") # a specific sound card
PipeWire/PulseAudio virtual sinks (Linux)
# Virtual sinks are auto-detected via pactl
player = pyaudiocast.AudioPlayer(device="Virtual-Mic") # sink name
player = pyaudiocast.AudioPlayer(device="pipewire:Virtual-Mic") # its id
PipeWire sinks are routed through the pulse (or pipewire) ALSA device: PULSE_SINK (or PIPEWIRE_NODE) is set only while the stream is opened (holding the GIL, and with other pyaudiocast device lookups and opens locked out), then restored. On a PipeWire desktop, prefer sinks over hw: devices: the sound server keeps the hardware busy.
Environment Variables
| Variable | Description | Example |
|---|---|---|
PYAUDIOCAST_LOG |
Enable logging. Uses env_logger filter syntax. |
PYAUDIOCAST_LOG=debug |
Logging
# Show everything
PYAUDIOCAST_LOG=debug python my_script.py
# Show info and above
PYAUDIOCAST_LOG=info python my_script.py
# Default (no env var): warnings and errors only
python my_script.py
Cross-Platform Support
| Platform | Backend | Device listing | Virtual sinks |
|---|---|---|---|
| Linux | ALSA + PipeWire/Pulse | Full | Yes (via pactl) |
| Windows | WASAPI | cpal devices | N/A |
| macOS | CoreAudio | cpal devices | N/A |
The audio engine (cpal) is fully cross-platform. PipeWire/PulseAudio virtual sink detection uses pactl and is automatically compiled out on non-Linux systems via #[cfg(target_os = "linux")]. On macOS, streams open at the device's current sample rate and channel count (changing them would affect every app), and pyaudiocast converts; cpal may still switch the hardware's sample format to 32-bit float.
Architecture
Python (pyaudiocast)
|
+- write(data) -> auto-detect format -> convert to f32
| |
| v
| [resample + remix channels if needed]
| |
| v
| Lock-free SPSC ring buffer (ringbuf crate, ~4 s of device audio)
| |
| v
| cpal audio callback (OS audio thread)
| | ^
| v |
+- Speaker / Virtual Sink clear() -> discard watermark
- Lock-free audio path: The audio callback only touches the ring buffer and atomic counters: no locks, no allocation, no syscalls.
- GIL release: Device setup (except opening a PipeWire/PulseAudio sink), blocking
write()s anddrain()release the Python GIL. Blocking calls wake every few milliseconds to checkclear()/stop(), device failure and Ctrl+C. - Sample conversion: All input formats are converted to float32 in Rust before entering the ring buffer, then to the device's sample format (f32, i16, i24, i32, ...) in the callback.
- Auto-resample: If the device doesn't support the requested sample rate, a stateful cubic (Catmull-Rom) resampler converts it, seamlessly across
write()calls. - Channel mapping: Mono feeds the front left/right speakers; a mono device gets the average of all channels.
- Interruption:
clear()records how much audio has been written; the callback fades out over 5 ms and discards up to that point, so awrite()right afterclear()plays only new audio. - Drain:
drain()waits until the callback has consumed everything written before the call, plus the device's output latency, so astop()right after doesn't cut the tail.
Running Tests
# Python tests (audio tests are skipped without an output device)
pytest tests/ -v
# Rust tests (link against libpython: needs the Python development package)
cargo test
License
MIT
Metadata
Release files for pyaudiocast 0.2.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 | |
|---|---|---|---|
| pyaudiocast-0.2.0.tar.gz | 48.9 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| pyaudiocast-0.2.0-cp39-abi3-win_amd64.whl | CPython 3.9 | abi3 | Windows x86-64 | Details |
| pyaudiocast-0.2.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl | CPython 3.9 | abi3 | Linux glibc 2.17+ x86-64 | Details |
| pyaudiocast-0.2.0-cp39-abi3-macosx_11_0_arm64.whl | CPython 3.9 | abi3 | macOS 11.0+ ARM64 | Details |
| pyaudiocast-0.2.0-cp39-abi3-macosx_10_12_x86_64.whl | CPython 3.9 | abi3 | macOS 10.12+ x86-64 | Details |
Total release size: 1.6 MB
Release files / pyaudiocast-0.2.0.tar.gz
| Download URL | pyaudiocast-0.2.0.tar.gz |
|---|---|
| Size | 48.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fc872bfff7ed09534110dd7bc99e98c842bf6d96aa1d68bb8b562c0ac9c8d7ba
|
|
BLAKE2b-256 checksum How to use checksums |
fc76b88156153214bf50539066b19e70a049ff7bdf42cec289b9f81a13122636
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.
Transparency logRelease files / pyaudiocast-0.2.0-cp39-abi3-win_amd64.whl
| Download URL | pyaudiocast-0.2.0-cp39-abi3-win_amd64.whl |
|---|---|
| Size | 353.3 kB |
| Tags | CPython 3.9 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
e6c447c9814e5e534095433ad51675f31b65528ef5d44f57d2b98ac1d60f1f64
|
|
BLAKE2b-256 checksum How to use checksums |
d70e46486c3990cf4746fe5b0e2f89b87446d993c82fb8c672f1f778a59525c8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.
Transparency logRelease files / pyaudiocast-0.2.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
| Download URL | pyaudiocast-0.2.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl |
|---|---|
| Size | 411.2 kB |
| Tags | CPython 3.9 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
cc7e0d42597f844212dd6fbaaf52a704c18bb7613fb92067ac6c8cdc0683d5ea
|
|
BLAKE2b-256 checksum How to use checksums |
439b549b9b77cabbf4fa1c68edfb873ff87a64596d5ea5d8e63d9bae9d84738f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.
Transparency logRelease files / pyaudiocast-0.2.0-cp39-abi3-macosx_11_0_arm64.whl
| Download URL | pyaudiocast-0.2.0-cp39-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 374.9 kB |
| Tags | CPython 3.9 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
7d0930547be9f176e517f4ea9e0df8fb33519d6a555ab32769e76efadf0f0c41
|
|
BLAKE2b-256 checksum How to use checksums |
312e9e184f2946984227303d2251a5f0e4d76cfc366ef46097ad80116149c57c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.
Transparency logRelease files / pyaudiocast-0.2.0-cp39-abi3-macosx_10_12_x86_64.whl
| Download URL | pyaudiocast-0.2.0-cp39-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 386.3 kB |
| Tags | CPython 3.9 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
65bdbff7f616dee3fa205191ebe464013534dc61e2a591fbf7dc3c7674ceaec9
|
|
BLAKE2b-256 checksum How to use checksums |
80b1cd0dedd91100d149616ff40d0f7178df6e8218e2a522c23c44b0c44f7430
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.
Transparency log