Package: pulsectl-asyncio
pulsectl-asyncioThis library provides an Python 3 asyncio interface on top of thepulsectllibrary for monitoring and controlling the PulseAudio sound server.pulsectlis a Python ctypes wrapper of the PulseAudio client C librarylibpulse, 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 thepulsemixercommand line application.Although libpulse provides a callback-based asynchronous C API for the communication with the PulseAudio server,pulsectlonly exposes a blocking Python interface, letting libpulse's internal event loop spin until a response is received for each request.
In theREADME fileandIssue #11ofpulsectl, 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-asynciouses a ctypes-based Python implementation of themain_loop_apiof libpulse to use a Python asyncio event loop for libpulse's asynchronous event handling.
With this event handling in place, no blocking calls intolibpulseare required, so an asynchronous version for the high-level API ofpulsectlcan be provided:
ThePulseAsyncclass, provided bypulsectl-asyncio, exactly mimics thePulseclass frompulsectl, except that all methods are declaredasyncand 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-asynciodepends onpulsectlto reuse its ctype wrappers oflibpulseas well as thePulseObjectclasses, which are used for modelling the PulseAudio action result structures as Python objects.
The high-level API classPulseAsynchas been copied frompulsectland modified for asynchronous control flow.
Thus, its architecture and major parts of its code are still similar topulsectl's code.For more info about the API, the returned PulseObject objects and other value types, volume specification, etc., please refer topulsectl's README file.Usage Examples(heavily inspired bypulsectl'sREADME file)Simple example:importasyncioimportpulsectl_asyncioasyncdefmain():asyncwithpulsectl_asyncio.PulseAsync('volume-increaser')aspulse:forsinkinawaitpulse.sink_list():awaitpulse.volume_change_all_chans(sink,0.1)loop=asyncio.get_event_loop()loop.run_until_complete(main())Listening for server state change events:importasyncioimportsignalfromcontextlibimportsuppressimportpulsectl_asyncio# import pulsectl# print('Event types:', pulsectl.PulseEventTypeEnum)# print('Event facilities:', pulsectl.PulseEventFacilityEnum)# print('Event masks:', pulsectl.PulseEventMaskEnum)asyncdeflisten():asyncwithpulsectl_asyncio.PulseAsync('event-printer')aspulse:asyncforeventinpulse.subscribe_events('all'):print('Pulse event:',event)asyncdefmain():# Run listen() coroutine in task to allow cancelling itlisten_task=asyncio.create_task(listen())# register signal handlers to cancel listener when program is asked to terminateforsigin(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` loopwithsuppress(asyncio.CancelledError):awaitlisten_task# Run event loop until main_task finishesloop=asyncio.get_event_loop()loop.run_until_complete(main())Misc other tinkering:importasyncioimportpulsectl_asyncioasyncdefmain():pulse=pulsectl_asyncio.PulseAsync('my-client-name')awaitpulse.connect()print(awaitpulse.sink_list())# [<PulseSinkInfo at 7f85cfd053d0 - desc='Built-in Audio', index=0L, mute=0, name='alsa-speakers', channels=2, volumes='44.0%, 44.0%'>]print(awaitpulse.sink_input_list())# [<PulseSinkInputInfo at 7fa06562d3d0 - index=181L, mute=0, name='mpv Media Player', channels=2, volumes='25.0%, 25.0%'>]print((awaitpulse.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(awaitpulse.source_list())# [<PulseSourceInfo at 7fcb0615d8d0 - desc='Monitor of Built-in Audio', index=0L, mute=0, name='alsa-speakers.monitor', channels=2, volumes='100.0%, 100.0%'>,#  <PulseSourceInfo at 7fcb0615da10 - desc='Built-in Audio', index=1L, mute=0, name='alsa-mic', channels=2, volumes='100.0%, 100.0%'>]sink=(awaitpulse.sink_list())[0]awaitpulse.volume_change_all_chans(sink,-0.1)awaitpulse.volume_set_all_chans(sink,0.5)print((awaitpulse.server_info()).default_sink_name)# 'alsa_output.pci-0000_00_14.2.analog-stereo'awaitpulse.default_set(sink)card=(awaitpulse.card_list())[0]print(card.profile_list)# [<PulseCardProfileInfo at 7f02e7e88ac8 - description='Analog Stereo Input', n_sinks=0, n_sources=1, name='input:analog-stereo', priority=60>,#  <PulseCardProfileInfo at 7f02e7e88b70 - description='Analog Stereo Output', n_sinks=1, n_sources=0, name='output:analog-stereo', priority=6000>,#  ...#  <PulseCardProfileInfo at 7f02e7e9a4e0 - description='Off', n_sinks=0, n_sources=0, name='off', priority=0>]awaitpulse.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:importasyncioimportsignalfromcontextlibimportsuppressimportpulsectl_asyncioasyncdeflisten(pulse:pulsectl_asyncio.PulseAsync,source_name:str):asyncforlevelinpulse.subscribe_peak_sample(source_name,rate=5):print('\x1b[2K\x1b[0E',end='')# return to beginning of linenum_o=round(level*80)print('O'*num_o+'-'*(80-num_o),end='',flush=True)asyncdefmain():asyncwithpulsectl_asyncio.PulseAsync('peak-listener')aspulse:# Get name of monitor_source of default sinkserver_info=awaitpulse.server_info()default_sink_info=awaitpulse.get_sink_by_name(server_info.default_sink_name)source_name=default_sink_info.monitor_source_name# Start listening/monitoring tasklisten_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` loopforsigin(signal.SIGTERM,signal.SIGHUP,signal.SIGINT):loop.add_signal_handler(sig,listen_task.cancel)withsuppress(asyncio.CancelledError):awaitlisten_taskprint()# Run event loop until main_task finishesloop=asyncio.get_event_loop()loop.run_until_complete(main())