{"id":16243717,"url":"https://github.com/mhthies/pulsectl-asyncio","last_synced_at":"2025-07-12T03:09:42.453Z","repository":{"id":46336543,"uuid":"341306306","full_name":"mhthies/pulsectl-asyncio","owner":"mhthies","description":"Asyncio frontend for pulsectl, a Python bindings library for PulseAudio (libpulse)","archived":false,"fork":false,"pushed_at":"2024-12-31T15:50:28.000Z","size":159,"stargazers_count":32,"open_issues_count":1,"forks_count":4,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-05-10T09:49:06.762Z","etag":null,"topics":["asyncio","pulseaudio","python3"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/pulsectl-asyncio/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mhthies.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-02-22T19:01:31.000Z","updated_at":"2025-02-16T21:26:50.000Z","dependencies_parsed_at":"2024-04-08T20:00:49.299Z","dependency_job_id":"79cdef80-536a-4987-8ea9-be330d2df59e","html_url":"https://github.com/mhthies/pulsectl-asyncio","commit_stats":{"total_commits":101,"total_committers":2,"mean_commits":50.5,"dds":0.02970297029702973,"last_synced_commit":"6d38c886dbcec75f8a60b07252080d2292c5063a"},"previous_names":[],"tags_count":21,"template":false,"template_full_name":null,"purl":"pkg:github/mhthies/pulsectl-asyncio","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mhthies%2Fpulsectl-asyncio","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mhthies%2Fpulsectl-asyncio/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mhthies%2Fpulsectl-asyncio/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mhthies%2Fpulsectl-asyncio/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mhthies","download_url":"https://codeload.github.com/mhthies/pulsectl-asyncio/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mhthies%2Fpulsectl-asyncio/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259368018,"owners_count":22846822,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["asyncio","pulseaudio","python3"],"created_at":"2024-10-10T14:16:02.807Z","updated_at":"2025-06-12T00:04:52.776Z","avatar_url":"https://github.com/mhthies.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# pulsectl-asyncio\n\nThis 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.\n\n*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. \nIt has originally been forked from the internal code of the [pulsemixer](https://github.com/GeorgeFilipkin/pulsemixer/) command line application.\n\nAlthough 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.\nIn 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.\nHowever, none of these ways provides seamless integration into Python's asyncio event loop framework.\n\n*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.\nWith 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:\nThe `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.\nAdditionally, 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.\n\n*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.\nThe high-level API class `PulseAsync` has been copied from *pulsectl* and modified for asynchronous control flow.\nThus, its architecture and major parts of its code are still similar to *pulsectl*'s code.\n\nFor 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).  \n\n\n## Usage Examples\n\n(heavily inspired by *pulsectl*'s [README file](https://github.com/mk-fg/python-pulse-control/blob/master/README.rst#usage))\n\nSimple example:\n\n```python\nimport asyncio\nimport pulsectl_asyncio\n\nasync def main():\n    async with pulsectl_asyncio.PulseAsync('volume-increaser') as pulse:\n        for sink in await pulse.sink_list():\n            await pulse.volume_change_all_chans(sink, 0.1)\n\nloop = asyncio.get_event_loop()\nloop.run_until_complete(main())\n``` \n\nListening for server state change events:\n\n```python\nimport asyncio\nimport signal\nfrom contextlib import suppress\nimport pulsectl_asyncio\n\n# import pulsectl\n# print('Event types:', pulsectl.PulseEventTypeEnum)\n# print('Event facilities:', pulsectl.PulseEventFacilityEnum)\n# print('Event masks:', pulsectl.PulseEventMaskEnum)\n\nasync def listen():\n    async with pulsectl_asyncio.PulseAsync('event-printer') as pulse:\n        async for event in pulse.subscribe_events('all'):\n            print('Pulse event:', event)\n\nasync def main():\n    # Run listen() coroutine in task to allow cancelling it\n    listen_task = asyncio.create_task(listen())\n\n    # register signal handlers to cancel listener when program is asked to terminate\n    for sig in (signal.SIGTERM, signal.SIGHUP, signal.SIGINT):\n        loop.add_signal_handler(sig, listen_task.cancel)\n    # Alternatively, the PulseAudio event subscription can be ended by breaking/returning from the `async for` loop\n\n    with suppress(asyncio.CancelledError):\n        await listen_task\n\n# Run event loop until main_task finishes\nloop = asyncio.get_event_loop()\nloop.run_until_complete(main())\n```\n\nMisc other tinkering:\n\n```python\nimport asyncio\nimport pulsectl_asyncio\n\nasync def main():\n    pulse = pulsectl_asyncio.PulseAsync('my-client-name')\n    await pulse.connect()\n\n    print(await pulse.sink_list())\n    # [\u003cPulseSinkInfo at 7f85cfd053d0 - desc='Built-in Audio', index=0L, mute=0, name='alsa-speakers', channels=2, volumes='44.0%, 44.0%'\u003e]\n    print(await pulse.sink_input_list())\n    # [\u003cPulseSinkInputInfo at 7fa06562d3d0 - index=181L, mute=0, name='mpv Media Player', channels=2, volumes='25.0%, 25.0%'\u003e]\n\n    print((await pulse.sink_input_list())[0].proplist)  # Note the parentheses around `await` and the method call\n    # {'application.icon_name': 'mpv',\n    #  'application.language': 'C',\n    #  'application.name': 'mpv Media Player',\n    #  ...\n    #  'native-protocol.version': '30',\n    #  'window.x11.display': ':1.0'}\n\n    print(await pulse.source_list())\n    # [\u003cPulseSourceInfo at 7fcb0615d8d0 - desc='Monitor of Built-in Audio', index=0L, mute=0, name='alsa-speakers.monitor', channels=2, volumes='100.0%, 100.0%'\u003e,\n    #  \u003cPulseSourceInfo at 7fcb0615da10 - desc='Built-in Audio', index=1L, mute=0, name='alsa-mic', channels=2, volumes='100.0%, 100.0%'\u003e]\n\n    sink = (await pulse.sink_list())[0]\n    await pulse.volume_change_all_chans(sink, -0.1)\n    await pulse.volume_set_all_chans(sink, 0.5)\n\n    print((await pulse.server_info()).default_sink_name)\n    # 'alsa_output.pci-0000_00_14.2.analog-stereo'\n    await pulse.default_set(sink)\n\n    card = (await pulse.card_list())[0]\n    print(card.profile_list)\n    # [\u003cPulseCardProfileInfo at 7f02e7e88ac8 - description='Analog Stereo Input', n_sinks=0, n_sources=1, name='input:analog-stereo', priority=60\u003e,\n    #  \u003cPulseCardProfileInfo at 7f02e7e88b70 - description='Analog Stereo Output', n_sinks=1, n_sources=0, name='output:analog-stereo', priority=6000\u003e,\n    #  ...\n    #  \u003cPulseCardProfileInfo at 7f02e7e9a4e0 - description='Off', n_sinks=0, n_sources=0, name='off', priority=0\u003e]\n\n    await pulse.card_profile_set(card, 'output:hdmi-stereo')\n\n    pulse.close()  # No await here!\n\nloop = asyncio.get_event_loop()\nloop.run_until_complete(main())\n```\n\nMonitor output level of default sink on the command line:\n\n```python\nimport asyncio\nimport signal\nfrom contextlib import suppress\n\nimport pulsectl_asyncio\n\nasync def listen(pulse: pulsectl_asyncio.PulseAsync, source_name: str):\n    async for level in pulse.subscribe_peak_sample(source_name, rate=5):\n        print('\\x1b[2K\\x1b[0E', end='')  # return to beginning of line\n        num_o = round(level * 80)\n        print('O' * num_o + '-' * (80-num_o), end='', flush=True)\n\n\nasync def main():\n    async with pulsectl_asyncio.PulseAsync('peak-listener') as pulse:\n        # Get name of monitor_source of default sink\n        server_info = await pulse.server_info()\n        default_sink_info = await pulse.get_sink_by_name(server_info.default_sink_name)\n        source_name = default_sink_info.monitor_source_name\n\n        # Start listening/monitoring task\n        listen_task = loop.create_task(listen(pulse, source_name))\n\n        # register signal handlers to cancel listener when program is asked to terminate\n        # Alternatively, the PulseAudio event subscription can be ended by breaking/returning from the `async for` loop\n        for sig in (signal.SIGTERM, signal.SIGHUP, signal.SIGINT):\n            loop.add_signal_handler(sig, listen_task.cancel)\n\n        with suppress(asyncio.CancelledError):\n            await listen_task\n            print()\n\n\n# Run event loop until main_task finishes\nloop = asyncio.get_event_loop()\nloop.run_until_complete(main())\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmhthies%2Fpulsectl-asyncio","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmhthies%2Fpulsectl-asyncio","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmhthies%2Fpulsectl-asyncio/lists"}