https://github.com/mhthies/pulsectl-asyncio
Asyncio frontend for pulsectl, a Python bindings library for PulseAudio (libpulse)
https://github.com/mhthies/pulsectl-asyncio
asyncio pulseaudio python3
Last synced: 12 months ago
JSON representation
Asyncio frontend for pulsectl, a Python bindings library for PulseAudio (libpulse)
- Host: GitHub
- URL: https://github.com/mhthies/pulsectl-asyncio
- Owner: mhthies
- License: mit
- Created: 2021-02-22T19:01:31.000Z (over 5 years ago)
- Default Branch: main
- Last Pushed: 2024-12-31T15:50:28.000Z (over 1 year ago)
- Last Synced: 2025-05-10T09:49:06.762Z (about 1 year ago)
- Topics: asyncio, pulseaudio, python3
- Language: Python
- Homepage: https://pypi.org/project/pulsectl-asyncio/
- Size: 155 KB
- Stars: 32
- Watchers: 3
- Forks: 4
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# pulsectl-asyncio
This library provides an Python 3 asyncio interface on top of the [pulsectl](https://github.com/mk-fg/python-pulse-control) library for monitoring and controlling the PulseAudio sound server.
*pulsectl* is a Python ctypes wrapper of the PulseAudio client C library `libpulse`, providing a high-level interface to PulseAudio's source/sink/stream handling and volume mixing.
It has originally been forked from the internal code of the [pulsemixer](https://github.com/GeorgeFilipkin/pulsemixer/) command line application.
Although libpulse provides a callback-based asynchronous C API for the communication with the PulseAudio server, *pulsectl* only exposes a blocking Python interface, letting libpulse's internal event loop spin until a response is received for each request.
In the [README file](https://github.com/mk-fg/python-pulse-control/blob/master/README.rst#event-handling-code-threads) and [Issue #11](https://github.com/mk-fg/python-pulse-control/issues/11#issuecomment-259560564) of *pulsectl*, different ways of integrating the library into asynchronous Python applications are discussed.
However, none of these ways provides seamless integration into Python's asyncio event loop framework.
*pulsectl-asyncio* uses a ctypes-based Python implementation of the `main_loop_api` of libpulse to use a Python asyncio event loop for libpulse's asynchronous event handling.
With this event handling in place, no blocking calls into *libpulse* are required, so an asynchronous version for the high-level API of *pulsectl* can be provided:
The `PulseAsync` class, provided by *pulsectl-asyncio*, exactly mimics the `Pulse` class from *pulsectl*, except that all methods are declared `async` and asynchronously await the actions' results.
Additionally, the API for subscribing to PulseAudio server events has been changed from a callback-based interface (`event_callback_set()` etc.) to a more asnycio-nic interface using an async generator.
*pulsectl-asyncio* depends on *pulsectl* to reuse its ctype wrappers of *libpulse* as well as the `PulseObject` classes, which are used for modelling the PulseAudio action result structures as Python objects.
The high-level API class `PulseAsync` has been copied from *pulsectl* and modified for asynchronous control flow.
Thus, its architecture and major parts of its code are still similar to *pulsectl*'s code.
For more info about the API, the returned PulseObject objects and other value types, volume specification, etc., please refer to [*pulsectl*'s README file](https://github.com/mk-fg/python-pulse-control/blob/master/README.rst#notes).
## Usage Examples
(heavily inspired by *pulsectl*'s [README file](https://github.com/mk-fg/python-pulse-control/blob/master/README.rst#usage))
Simple example:
```python
import asyncio
import pulsectl_asyncio
async def main():
async with pulsectl_asyncio.PulseAsync('volume-increaser') as pulse:
for sink in await pulse.sink_list():
await pulse.volume_change_all_chans(sink, 0.1)
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
```
Listening for server state change events:
```python
import asyncio
import signal
from contextlib import suppress
import pulsectl_asyncio
# import pulsectl
# print('Event types:', pulsectl.PulseEventTypeEnum)
# print('Event facilities:', pulsectl.PulseEventFacilityEnum)
# print('Event masks:', pulsectl.PulseEventMaskEnum)
async def listen():
async with pulsectl_asyncio.PulseAsync('event-printer') as pulse:
async for event in pulse.subscribe_events('all'):
print('Pulse event:', event)
async def main():
# Run listen() coroutine in task to allow cancelling it
listen_task = asyncio.create_task(listen())
# register signal handlers to cancel listener when program is asked to terminate
for sig in (signal.SIGTERM, signal.SIGHUP, signal.SIGINT):
loop.add_signal_handler(sig, listen_task.cancel)
# Alternatively, the PulseAudio event subscription can be ended by breaking/returning from the `async for` loop
with suppress(asyncio.CancelledError):
await listen_task
# Run event loop until main_task finishes
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
```
Misc other tinkering:
```python
import asyncio
import pulsectl_asyncio
async def main():
pulse = pulsectl_asyncio.PulseAsync('my-client-name')
await pulse.connect()
print(await pulse.sink_list())
# []
print(await pulse.sink_input_list())
# []
print((await pulse.sink_input_list())[0].proplist) # Note the parentheses around `await` and the method call
# {'application.icon_name': 'mpv',
# 'application.language': 'C',
# 'application.name': 'mpv Media Player',
# ...
# 'native-protocol.version': '30',
# 'window.x11.display': ':1.0'}
print(await pulse.source_list())
# [,
# ]
sink = (await pulse.sink_list())[0]
await pulse.volume_change_all_chans(sink, -0.1)
await pulse.volume_set_all_chans(sink, 0.5)
print((await pulse.server_info()).default_sink_name)
# 'alsa_output.pci-0000_00_14.2.analog-stereo'
await pulse.default_set(sink)
card = (await pulse.card_list())[0]
print(card.profile_list)
# [,
# ,
# ...
# ]
await pulse.card_profile_set(card, 'output:hdmi-stereo')
pulse.close() # No await here!
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
```
Monitor output level of default sink on the command line:
```python
import asyncio
import signal
from contextlib import suppress
import pulsectl_asyncio
async def listen(pulse: pulsectl_asyncio.PulseAsync, source_name: str):
async for level in pulse.subscribe_peak_sample(source_name, rate=5):
print('\x1b[2K\x1b[0E', end='') # return to beginning of line
num_o = round(level * 80)
print('O' * num_o + '-' * (80-num_o), end='', flush=True)
async def main():
async with pulsectl_asyncio.PulseAsync('peak-listener') as pulse:
# Get name of monitor_source of default sink
server_info = await pulse.server_info()
default_sink_info = await pulse.get_sink_by_name(server_info.default_sink_name)
source_name = default_sink_info.monitor_source_name
# Start listening/monitoring task
listen_task = loop.create_task(listen(pulse, source_name))
# register signal handlers to cancel listener when program is asked to terminate
# Alternatively, the PulseAudio event subscription can be ended by breaking/returning from the `async for` loop
for sig in (signal.SIGTERM, signal.SIGHUP, signal.SIGINT):
loop.add_signal_handler(sig, listen_task.cancel)
with suppress(asyncio.CancelledError):
await listen_task
print()
# Run event loop until main_task finishes
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
```