././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1677078815.4854946 libpulse-0.7/.coveragerc0000644000000000000000000000013614375430437012260 0ustar00[run] omit = */tests/* tools/* [report] exclude_lines = raise NotImplementedError ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1734105152.180176 libpulse-0.7/.gitignore0000644000000000000000000000022314727054100012112 0ustar00/dist/ /docs/build/ /.coverage .emacs.desktop* images/coverage.svg docs/source/README.rst *.pyc libpulse.cpp ext_device_restore.cpp libpulse.cache ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716641548.6308718 libpulse-0.7/.gitlab-ci.yml0000644000000000000000000000341214624357415012574 0ustar00# Built from template located at: # https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Python.gitlab-ci.yml variables: # Change pip's cache directory to be inside the project directory. PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" default: cache: paths: - .cache/pip .pulseaudio: &pulseaudio - apt-get update -yq && apt-get install -yq pulseaudio - pulseaudio --verbose --daemonize=yes .pipewire: &pipewire - apt-get update -yq && apt-get install -yq pulseaudio-utils xvfb pipewire wireplumber pipewire-pulse - | export 'XDG_RUNTIME_DIR=/tmp' export 'DISPLAY=:0.0' Xvfb -screen $DISPLAY 1920x1080x24 & pipewire & pipewire-pulse & wireplumber & .unittest-scripts: &unittest-scripts - python -m unittest --verbose --failfast .coverage-scripts: &coverage-scripts - PIP_BREAK_SYSTEM_PACKAGES=1 python -m pip install coverage - python -m coverage run -m unittest - python -m coverage report --show-missing pipwire-tests: image: python:3.11 stage: test script: - *pipewire - sleep 1 - pactl info - *unittest-scripts py38: image: python:3.8 stage: test script: - *pulseaudio - *unittest-scripts py39: image: python:3.9 stage: test script: - *pulseaudio - *unittest-scripts py310: image: python:3.10 stage: test script: - *pulseaudio - *unittest-scripts py311: image: python:3.11 stage: test script: - *pulseaudio - *unittest-scripts py312: image: python:3.12 stage: test script: - *pulseaudio - *unittest-scripts - *coverage-scripts coverage: '/TOTAL.* (100\%|\d?\d\%)$/' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1716640796.5455117 libpulse-0.7/.gitlab/issue_templates/Default.md0000644000000000000000000000073614624356035016577 0ustar00#### Bug report. ### Your environment. - Pulseaudio version: - Pipewire version: ### Steps to reproduce. ### Relevant logs or configuration. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1734105272.788994 libpulse-0.7/.readthedocs.yaml0000644000000000000000000000121714727054271013366 0ustar00# .readthedocs.yaml # Read the Docs configuration file. # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details. version: 2 build: os: ubuntu-22.04 tools: python: "3.12" jobs: post_build: - echo "Create the READTHEDOCS_OUTPUT/html/index.html symlink at $READTHEDOCS_OUTPUT/html" - cd "$READTHEDOCS_OUTPUT/html"; if [ -f README.html ]; then cp README.html index.html; else echo "README.html not found"; fi sphinx: configuration: docs/source/conf.py # Build PDF formats: - pdf # Declare the Python requirements required to build your docs. python: install: - requirements: docs/requirements.txt ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1646844371.9250107 libpulse-0.7/LICENSE0000644000000000000000000000207114212154724011134 0ustar00The MIT License (MIT) Copyright (c) 2022 Xavier de Gaye Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740231301.4977436 libpulse-0.7/README.rst0000644000000000000000000000751714756351205011636 0ustar00.. image:: images/coverage.png :alt: [libpulse test coverage] Asyncio interface to the Pulseaudio and Pipewire pulse library. Overview -------- `libpulse`_ is a Python package based on `asyncio`_, that uses `ctypes`_ to interface with the ``pulse`` library of the PulseAudio and PipeWire sound servers. The interface is meant to be complete. That is, all the constants, structures, plain functions and async functions are made available by importing the libpulse module of the libpulse package. Async functions are those ``pulse`` functions that return results through a callback. They are implemented as asyncio coroutines that return the callback results. They have the same name as the corresponding pulse async function. Non-async ``pulse`` functions have their corresponding ctypes foreign functions defined in the libpulse module namespace under the same name as the corresponding pulse function. They may be called directly. Calling an async function or a plain function is simple: .. code-block:: python import asyncio import libpulse.libpulse as libpulse async def main(): async with libpulse.LibPulse('my libpulse') as lp_instance: # A plain function. server = libpulse.pa_context_get_server(lp_instance.c_context) print('server:', server.decode()) # An async function. sink = await lp_instance.pa_context_get_sink_info_by_index(0) print('sample_spec rate:', sink.sample_spec.rate) print('proplist names:', list(sink.proplist.keys())) asyncio.run(main()) Another example processing ``pulse`` events: .. code-block:: python import asyncio import libpulse.libpulse as libpulse async def main(): async with libpulse.LibPulse('my libpulse') as lp_instance: await lp_instance.pa_context_subscribe( libpulse.PA_SUBSCRIPTION_MASK_ALL) iterator = lp_instance.get_events_iterator() async for event in iterator: # Start playing some sound to print the events. # 'event' is an instance of the PulseEvent class. print(event.__dict__) asyncio.run(main()) The libpulse package also includes the ``pactl-py`` command, which is a Python implementation of the ``pactl`` command running on Pulseaudio and Pipewire. The output of most ``pactl-py`` subcommands can be parsed by Python. When this output is redirected to a file, the file can be imported as a Python module. For example start an interactive Python session and inspect the ``cards`` object with all its nested sructures and dereferenced pointers with: .. code-block:: shell $ pactl-py list cards > cards.py && python -i cards.py Requirements ------------ Python version 3.8 or more recent. Documentation ------------- The libpulse documentation is hosted at `Read the Docs`_: - The `stable documentation`_ of the last released version. - The `latest documentation`_ of the current GitLab development version. To access the documentation as a pdf document one must click on the icon at the down-right corner of any page. It allows to switch between stable and latest versions and to select the corresponding pdf document. The documentation describing the C language API of the ``pulse`` library is at `PulseAudio Documentation`_. Installation ------------ Install ``libpulse`` with pip:: $ python -m pip install libpulse .. _libpulse: https://gitlab.com/xdegaye/libpulse .. _asyncio: https://docs.python.org/3/library/asyncio.html .. _ctypes: https://docs.python.org/3/library/ctypes.html .. _Read the Docs: https://about.readthedocs.com/ .. _stable documentation: https://libpulse.readthedocs.io/en/stable/ .. _latest documentation: https://libpulse.readthedocs.io/en/latest/ .. _`PulseAudio Documentation`: https://freedesktop.org/software/pulseaudio/doxygen/index.html ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1734105694.9349527 libpulse-0.7/docs/Makefile0000644000000000000000000000157414727055137012537 0ustar00# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = source BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). .ONESHELL: %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) case html in $@) echo "Makefile: symlink index.html to README.html"; cd "$(BUILDDIR)/html"; if [ -f README.html ]; then ln -sf README.html index.html; else echo "Makefile: *** error: README.html not found"; fi esac ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1672657372.279913 libpulse-0.7/docs/requirements.txt0000644000000000000000000000004614354534734014355 0ustar00sphinx >= 5.3 sphinx_rtd_theme >= 1.1 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740239017.2285228 libpulse-0.7/docs/source/classes.rst0000644000000000000000000000762714756370251014572 0ustar00.. _`Ancillary-classes`: Ancillary classes ================= Exceptions ---------- - `LibPulseError(Exception)` - `LibPulseClosedError(LibPulseError)` - `LibPulseStateError(LibPulseError)` - `LibPulseOperationError(LibPulseError)` - `LibPulseClosedIteratorError(LibPulseError)` - `LibPulseInstanceExistsError(LibPulseError)` - `LibPulseArgumentError(LibPulseError)` PulseEvent ---------- An instance of `PulseEvent` is returned by the async iterator returned by the :ref:`get_events_iterator` method of a LibPulse instance. Its attributes are: - `facility` `str` - name of the facility, for example ``sink``. - `index` `int` - index of the facility. - `type` `str` - type of the event, either ``new``, ``change`` or ``remove``. .. _`PulseStructure`: PulseStructure -------------- `class PulseStructure(c_struct, c_structure_type)` - `c_struct` ctypes structure such as a ctypes pointer dereferenced using its `contents` attribute. - `c_structure_type` subclass of ctypes `Structure`_ corresponding to the type of the `c_struct` structure. It is one of the values of the structures defined in the `struct_ctypes` dictionary, attribute of the libpulse module. An instance of this class is the representation of a pulse ctypes structure. Instantiating a `PulseStructure` class constructs a pure Python object as a deep copy of a ctypes structure using the `_fields_` class attribute of the corresponding subclass of the ctypes `Structure`_ class (one of the values of the `struct_ctypes` dictionary, see the :ref:`pulse_structs` module and the :ref:`ctypes-pulse-structures` section). A deep copy of the pulse structure pointed to by one of the parameters of a callback (one of the results of the corresponding `LibPulse` method) is needed because the memory pointed to by the pointer is short-lived, only valid during the execution of the callback. The `PulseStructure` instance embeds `PulseStructure` instances for those of its members that are nested pulse structures or pointers to other pulse structures (recursively). The attributes names of the PulseStructure instance are the names of the members of the pulse structure as listed in the :ref:`pulse_structs` module. Their values are of type `int`, `float`, `bytes`, :ref:`PropList` or `PulseStructure`. This class is used internally by callbacks of the `LibPulse` class and by the `to_pulse_structure()` method of the :ref:`CtypesPulseStructure` class. .. _`PropList`: PropList -------- When the type of the member of a pulse structure is `proplist *`, the corresponding `PulseStructure` attribute is set to an instance of the `PropList` class. The `PropList` class is a subclass of `dict` and the elements of an instance can be accessed as the elements of a dictionary. Instantiation of this class skips all the elements of the proplist that are not of the `bytes` type. The keys and values of the dictionary are strings (`bytes` are decoded to `str`). .. _AsyncioTasks: AsyncioTasks ------------ An instance of `AsyncioTasks` keeps track of the active asyncio tasks that have been created using its `create_task()` method. It is also an iterator that can iterate over these tasks. See the :ref:`session-management` section for how to use it. .. _CtypesPulseStructure: CtypesPulseStructure -------------------- An abstract class whose subclasses provide the reverse of what does the `PulseStructure` class by building a ctypes structure whose pointer can be used as the parameter of one of the pulse functions. Those subclasses are: - `Pa_buffer_attr` - `Pa_cvolume` - `Pa_channel_map` - `Pa_format_info` - `Pa_sample_spec` Instantiate one of these subclasses with a list of the pulse structure values and use `byref()` to get the pointer. For example: .. code-block:: python values = [3, 44100, 2] ptr = libpulse.Pa_sample_spec(*values).byref() See also :ref:`ctypes-pulse-structures`. .. _`Structure`: https://docs.python.org/3/library/ctypes.html#ctypes.Structure ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1739957855.5478668 libpulse-0.7/docs/source/conf.py0000644000000000000000000000343514755323140013665 0ustar00# Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information from pathlib import Path import sys __version__ = 'Unknown version' def conf_py_setup(): global __version__ cur_dir = Path(__file__).parent root_dir = cur_dir.parent.parent try: sys.path.append(str(root_dir)) from libpulse import __version__ finally: sys.path.pop() with open(cur_dir / 'README.rst', 'w') as fdest: fdest.write('libpulse |version|\n') fdest.write('==================\n\n') with open(root_dir / 'README.rst') as fsrc: content = fsrc.read() fdest.write(content) conf_py_setup() project = 'libpulse' copyright = '2025, Xavier de Gaye' author = '' # The short X.Y version version = __version__ # The full version, including alpha/beta/rc tags release = __version__ # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [] templates_path = ['_templates'] exclude_patterns = [] # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = 'sphinx_rtd_theme' html_static_path = ['images'] # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740296960.1118476 libpulse-0.7/docs/source/development.rst0000644000000000000000000001105114756551400015436 0ustar00.. _Development: Development =========== .. _Callbacks: Callbacks --------- All the LibPulse async methods ultimately call either `_pa_get()` or `_pa_get_list()`. Both coroutines follow the same design: - Define a nested function as the callback. - Create the ctypes function pointer for this callback. - Create an asyncio future. - Call the ``pulse`` ctypes foreign function and wait on the future that is set by the callback upon invocation. - Return the result. *So what happens when two asyncio tasks running concurrently are both waiting on their own future for the completion of the same callback ?* There is no concurrency issue. The ``ctypes`` pakage creates the ctypes function pointer for the callback by calling `PyCFuncPtr_new()`_ which in turn calls `_ctypes_alloc_callback()`_. This last function uses ``libffi`` to allocate a closure (quoting the ``libffi`` documentation [#]_: `closures work by assembling a tiny function at runtime`). So each one of the two callbacks is allocated its own closure and gets a different function pointer. Requirements ------------ **Development** * GNU ``gcc`` and `pyclibrary`_ are used to parse the libpulse headers and create the ``pulse_types``, ``pulse_enums``, ``pulse_structs`` and ``pulse_functions`` modules of the libpulse package. To re-create those modules using the current libpulse headers run [#]_:: $ python -m tools.libpulse_parser libpulse * The ``coverage`` Python package is used to get the test suite coverage. * `python-packaging`_ is used to set the development version name as conform to PEP 440. * `flit`_ is used to publish libpulse to PyPi and may be used to install libpulse locally. At the root of the libpulse git repository, use the following command to install libpulse locally:: $ flit install --symlink [--python path/to/python] This symlinks libpulse into site-packages rather than copying it, so that you can test changes. **Documentation** * `Sphinx`_ [#]_. * `Read the Docs theme`_. * Building the pdf documentation: - The latex texlive package group. - Imagemagick version 7 or more recent. Documentation ------------- To build locally the documentation follow these steps: - Fetch the GitLab test coverage badge:: $ curl -o images/coverage.svg "https://gitlab.com/xdegaye/libpulse/badges/master/coverage.svg?min_medium=85&min_acceptable=90&min_good=90" $ magick images/coverage.svg images/coverage.png - Build the html and pdf documentation:: $ make -C docs clean html latexpdf Updating the development version -------------------------------- Run the following commands to update the version name at `latest documentation`_ after a bug fix or a change in the features:: $ python -m tools.set_devpt_version_name $ make -C docs clean html latexpdf $ git commit -m "Update development version name" $ git push Releasing --------- * Run the test suite from the root of the project [#]_:: $ python -m unittest --verbose --catch --failfast * Get the test suite coverage:: $ coverage run --include="./*" -m unittest $ coverage report -m * Update ``__version__`` in libpulse/__init__.py. * Update docs/source/history.rst if needed. * Build locally the documentation, see the previous section. * Commit the changes:: $ git commit -m 'Version 0.n' $ git push * Tag the release and push:: $ git tag -a 0.n -m 'Version 0.n' $ git push --tags * Publish the new version to PyPi:: $ flit publish .. _PyCFuncPtr_new(): https://github.com/python/cpython/blob/38a25e9560cf0ff0b80d9e90bce793ff24c6e027/Modules/_ctypes/_ctypes.c#L3826 .. _`_ctypes_alloc_callback()`: https://github.com/python/cpython/blob/38a25e9560cf0ff0b80d9e90bce793ff24c6e027/Modules/_ctypes/callbacks.c#L348 .. _Read the Docs theme: https://docs.readthedocs.io/en/stable/faq.html#i-want-to-use-the-read-the-docs-theme-locally .. _Sphinx: https://www.sphinx-doc.org/ .. _flit: https://pypi.org/project/flit/ .. _unittest command line options: https://docs.python.org/3/library/unittest.html#command-line-options .. _latest documentation: https://libpulse.readthedocs.io/en/latest/ .. _pyclibrary: https://pypi.org/project/pyclibrary/ .. _python-packaging: https://github.com/pypa/packaging .. rubric:: Footnotes .. [#] The ``libffi`` documentation is included in the ``libffi`` package as a texinfo document to be browsed by the ``ìnfo`` utility or by ``emacs``. .. [#] The shell commands in this section are all run from the root of the repository. .. [#] Required versions at ``docs/requirements.txt``. .. [#] See `unittest command line options`_. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740222748.974506 libpulse-0.7/docs/source/history.rst0000644000000000000000000000531614756330435014627 0ustar00Release history =============== Version 0.7 - The documentation has been entirely rewritten. - Error handling is documented in a new section of the libpulse documentation. - The `LibPulseOperationError` exception is raised when a method listed in `LibPulse.context_success_methods` does not have `PA_OPERATION_DONE` set by the corresponding pulseaudio callback. In other words these methods always return `PA_OPERATION_DONE`. - ``pactl-py load-module`` prints an error message upon failure instead of the value of `PA_INVALID_INDEX`. - A pdf document is part of the libpulse documentation. To access the documentation as a pdf document one must click on the icon at the down-right corner of any page of the documentation on the web. It allows to switch between stable and latest versions and to select the corresponding pdf document. - The development version name is PEP 440 conformant. Version 0.6 - The ``pactl-py`` command runs the pactl Python module. It is created by the libpulse installation process (issue #1). Version 0.5 - libpulse versioning conforms to PEP 440. - The LibPulse methods listed by `LibPulse.context_list_methods` and whose name ends with ``_info`` or ``_formats`` return now a single element instead of a list with one element. Version 0.4 - Rename `LibPulse.get_events()` to `get_events_iterator()`. - Replace the *Interface* section by *Libpulse API* in the documentation. - Add the `CtypesPulseStructure` subclasses `Pa_buffer_attr`, `Pa_channel_map`, `Pa_cvolume`, `Pa_format_info` and `Pa_sample_spec`. These classes are used to build ctypes ``Structure`` instances and pass their pointer to some of the pulse functions. - Add the static coroutine `LibPulse.get_current_instance()`. Version 0.3 - The ``pactl`` module is a Python implementation of the ``pactl`` command that runs both on Pulseaudio and Pipewire. - `server` and `flags` are optional parameters of the LibPulse constructor used by `pa_context_connect()` when connecting to the server. - Exceptions in callbacks and asyncio tasks are propagated to the async methods. - Use `pa_strerror()` to log errors. - Drain the pulse context before disconnecting. Version 0.2 - Add the `pa_context_subscribe.py` example. - Add the `pa_context_load_module.py` example. - Unreference stream pointer upon exit in `pa_stream_new.py` example. - Raise `LibPulseArgumentError` when args do not match the signature. - Document callback concurrent access. - `stream_success_methods` require a `pa_stream *` ctypes pointer as first argument. Version 0.1 - Publish the project on PyPi. - Raise an exception upon instantiation of more than one LibPulse instance. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1734106442.5114582 libpulse-0.7/docs/source/images/coverage.png0000755000000000000000000000000014727056513022535 2../../../images/coverage.pngustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740133662.9191651 libpulse-0.7/docs/source/index.rst0000644000000000000000000000040714756052437014234 0ustar00.. libpulse documentation master file. libpulse ======== .. toctree:: :hidden: :maxdepth: 2 :caption: Table of Contents README usage modules libpulse classes development history Repository ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740297742.9278755 libpulse-0.7/docs/source/libpulse.rst0000644000000000000000000001445714756553017014756 0ustar00.. _libpulse-class: LibPulse class ============== `class LibPulse(name, server=None, flags=PA_CONTEXT_NOAUTOSPAWN)` `name` Application name that will be passed to `pa_context_new()` upon instantiation. `server` and `flags` They are passed to `pa_context_connect()` when called by the async context manager. See the `pa_context_connect() pulse documentation`_. **LibPulse must not be instantiated directly**. It is instatiated by the async context manager statement that connects to the PulseAudio or Pipewire server when it is entered, see :ref:`context-manager-connection`. It must be instanciated this way: .. code-block:: python import asyncio import libpulse.libpulse as libpulse async def main(): async with libpulse.LibPulse('my libpulse') as lp_instance: ... asyncio.run(main()) Class attributes ---------------- The pulse async functions are implemented as LibPulse methods that are asyncio coroutines. Those methods are grouped in four lists according to their signature and the signature of their callback: `context_methods`: These methods return an object that should be checked against `None` or `PA_INVALID_INDEX` when the callback of the async function sets an index. An example of a method that returns an index is `pa_context_load_module()`. The `pa_context_send_message_to_object()` method is special in that it returns a list of two elements. The first one is an `int` of type `pa_operation_state` that is `PA_OPERATION_DONE` in case of success, and the second one is the response as a `bytes` object. `context_success_methods`: These methods always return `PA_OPERATION_DONE` or raise `LibPulseOperationError` upon failure. `context_list_methods`: These methods return a list of objects of the same type. Method names in this list that end with ``info_by_name``, ``info_by_index``, ``_info`` or ``_formats`` return a single object instead of a list. `stream_success_methods`: One must refer to the PulseAudio documentation of the function. The type of the object (or of the objects in a list) returned by these methods is `int`, `float`, `bytes` or :ref:`PulseStructure`. See also :ref:`error-handling`. Instance attributes ------------------- `c_context` ctypes object corresponding to the pulse `pa_context *` opaque pointer used as the first parameter of pulse functions whose name starts with ``pa_context``. It is used when calling a non-async function that needs it. It is not used when waiting on a LibPulse coroutine method when the C pulse async function does, as the LibPulse instance does set it instead. See :ref:`Pulse-methods-parameters`. `libpulse_tasks` An instance of :ref:`AsyncioTasks`. The :ref:`session-management` section explains how to use this object for the creation of asyncio tasks. `loop` The asyncio loop of this `LibPulse` instance. `state` The pulse context state. A tuple whose first element is one of the constants of the `pa_context_state` enum as a string. The second element is one of the constants of the `pa_error_code` enum as a string. Public methods -------------- `async def get_current_instance()` A static method. There may be only one `LibPulse` instance per asyncio loop and one asyncio loop per thread. The libpulse implementation supports multiple threads with one `LibPulse` instance per thread. Return the current `LibPulse` instance, `None` if the async context manager has exited. Raise `LibPulseStateError` if the instance is not in the `PA_CONTEXT_READY` state. This is used by the `LibPulse` instance callbacks that are static methods to get the instance they are running on. .. _`get_events_iterator`: `get_events_iterator()` Return an Asynchronous Iterator of libpulse events. There can only be one such iterator at any given time. Use the iterator in an async for loop to loop over `PulseEvent` instances whose types have been selected by a previous call to the `pa_context_subscribe()` coroutine. `pa_context_subscribe()` may be called while the loop on the iterator is running to change the kind of events one is interested in. The async for loop may be terminated by invoking the `close()` method of the iterator from within the loop or from another asyncio task. .. _`Pulse-methods-parameters`: Pulse methods parameters ------------------------ Pulse methods are those coroutines that are listed in one of the `Class attributes`_ and whose return values are also described there. Some parameters of the Pulse methods are omitted upon invocation: `pa_context * c` The type of the first parameter of the pulse async functions whose name starts with ``pa_context`` is `pa_context *`. This parameter is **omitted** upon invocation of the corresponding LibPulse method (the Libpulse instance already knows it as one of its attributes named `c_context`). `pa_*_cb_t cb` One of the parameters of the pulse async functions is the type of the callback. This parameter is **omitted** upon invocation of the corresponding LibPulse method as the Libpulse instance already knows this type from the signature of the function in the :ref:`pulse_functions` module. `void * userdata` The type of the last parameter of the pulse async functions is `void *`. The parameter is meant to be used to match the callback invocation with the pulse function that triggered it when the implementation is done in C language. This last parameter is not needed and **omitted** upon invocation of the corresponding LibPulse method (the callback is implemented as a nested function in the method definition, more details at :ref:`Callbacks`). For example `pa_context_get_server_info()` is invoked as: .. code-block:: python server_info = await lp_instance.pa_context_get_server_info() Not implemented --------------- The following pulse async functions are not implemented as LibPulse methods: `pa_signal_new()` and `pa_signal_set_destroy()`: Signals are handled by asyncio and the hook signal support built into the pulse main loop is not needed. For the following async functions, the callback has to be implemented by the user of the libpulse API: - `pa_context_rttime_new()` - `pa_stream_write()` - `pa_stream_write_ext_free()` .. _`pa_context_connect() pulse documentation`: https://freedesktop.org/software/pulseaudio/doxygen/context_8h.html#a983ce13d45c5f4b0db8e1a34e21f9fce ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740235899.9941804 libpulse-0.7/docs/source/modules.rst0000644000000000000000000000726514756362174014610 0ustar00Modules ======= libpulse -------- List of the main objects defined by the libpulse module, excluding the :ref:`libpulse-class` and :ref:`Ancillary-classes`: - The constants that are defined by enums in the pulse headers and that are listed by :ref:`pulse_enums`. - The following constants that are defined by the pulse headers as macros: + `PA_INVALID_INDEX` + `PA_VOLUME_NORM` + `PA_VOLUME_MUTED` + `PA_VOLUME_MAX` + `PA_VOLUME_INVALID` + `PA_CHANNELS_MAX` - The ctypes foreign functions corresponding to (and having the same name as) the non-async pulse functions. That is, all the keys in the `signatures` dictionary of the :ref:`pulse_functions` module whose signature does not have a callback as one of its parameters. - `CTX_STATES`, `OPERATION_STATES`, `EVENT_FACILITIES` and `EVENT_TYPES` dictionaries that map constant values to their symbolic names. - `struct_ctypes` a dictionary mapping the name of each pulse structure defined by the pulse headers (see the :ref:`pulse_structs` module) to the corresponding subclass of the ctypes `Structure`_ class. libpulse_ctypes --------------- The libpulse_ctypes module is executed when its `PulseCTypes` class is instantiated by the mainloop module. This occurs when the libpulse module imports the mainloop module on startup. The libpulse_ctypes module uses the `pulse_types`, :ref:`pulse_structs` and :ref:`pulse_functions` modules to build the following ctypes objects: - The ctypes foreign functions corresponding to the pulse functions. - The subclasses of the ctypes `Structure`_ class corresponding to the pulse structures. The libpulse_ctypes module uses the :ref:`pulse_enums` module to set variables corresponding to the constants of the enums of the pulse library. These four ``pulse_*`` modules are generated from the headers of the pulse library and may be re-generated using ``gcc`` and the ``pyclibrary`` package as explained in the :ref:`Development` section although this is not necessary, the ABI of the pulse library being pretty much stable. Using recent versions of Pulseaudio and Pipewire generates the same modules. mainloop -------- The mainloop module implements the pulse Main Loop using the asyncio event loop. The implementation supports multiple threads with one asyncio loop per thread using a dictionary to map the asyncio `loop` instance to the libpulse `MainLoop` instance. .. _pulse_enums: pulse_enums ----------- The `pulse_enums` dictionary holds all the pulse enum types whose values are themselves a dictionary of the enum constant names and their values. All constant names are different making it possible to have each defined as a constant in the libpulse module. .. _pulse_functions: pulse_functions --------------- The `pulse_functions['signatures']` dictionary holds the signatures of all the pulse functions that are not callbacks. Async functions are those functions whose signature has a callback as one of its parameter, the callback signature being one of the values of the `pulse_functions['callbacks']` dictionary. The signatures are used to build ctypes `Function prototypes`_ that are instantiated to create the foreign functions to be used as the corresponding non-async functions, `LibPulse` coroutines or callbacks. Foreign functions are Python callables. .. _pulse_structs: pulse_structs ------------- The `pulse_structs` dictionary holds the definitions of the pulse structures used to build the ctypes `Structure`_ subclasses that are available in the `struct_ctypes` dictionary which is an attribute of the libpulse module. .. _`Structure`: https://docs.python.org/3/library/ctypes.html#ctypes.Structure .. _`Function prototypes`: https://docs.python.org/3/library/ctypes.html#function-prototypes ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740232841.4448977 libpulse-0.7/docs/source/usage.rst0000644000000000000000000001336014756354211014226 0ustar00Usage ===== .. _`context-manager-connection`: Connecting to the sound server ------------------------------ Instantiation of the :ref:`libpulse-class` by the async context manager with the `async with` statement does the following: - The `LibPulse` instance is created and assigned as the target of the context manager. - The instance connects to the sound server. - The instance monitors the state of the connection by subscribing a callback to the sound server. - The `c_context` instance attribute is available as parameter to non-async functions. - The `state` instance attribute is updated by the state monitor callback whenever the callback is invoked by the sound server. The context manager may raise `LibPulseStateError` while attempting the connection. .. _session-management: Session management ------------------ When the connection fails for any reason, the state monitor callback cancels the asyncio task of the async context manager. This causes the async context manager to terminate gracefully. When the async context manager terminates because it got a CancelledError exception or because it terminates normally or because it got any other exception, all the asyncio tasks listed by `libpulse_tasks` are also cancelled allowing those tasks to do some clean up on exit. `libpulse_tasks` is an attribute of the `LibPulse` instance and an instance of :ref:`AsyncioTasks`. The tasks listed by `libpulse_tasks` are the still active asyncio tasks that have been created by its `create_task()` method. So `libpulse_tasks` can also be used by any application built upon libpulse to have tasks do some clean up upon termination of the async context manager. As an example, in the following code the task of the *user_task* coroutine is cancelled upon termination of the async context manager and *"user_task got CancelledError"* is printed: .. code-block:: python import asyncio import libpulse.libpulse as libpulse async def user_task(ready, cancelled): try: ready.set_result(True) # Run an infinite loop. while True: await asyncio.sleep(0.1) except asyncio.CancelledError: print('user_task got CancelledError') cancelled.set_result(True) pass async def main(): async with libpulse.LibPulse('my libpulse') as lp_instance: ready = lp_instance.loop.create_future() cancelled = lp_instance.loop.create_future() lp_instance.libpulse_tasks.create_task(user_task(ready, cancelled)) # Wait for 'user_task' to be ready. await ready await cancelled asyncio.run(main()) .. _error-handling: Error handling -------------- The return value of a libpulse function corresponding to a non-async ``pulse`` function should be checked against `None` or `PA_INVALID_INDEX` when the function returns an index. All the LibPulse methods that are asyncio coroutines corresponding to ``pulse`` async functions **may raise** `LibPulseOperationError`. See also the `Error Handling`_ section of the PulseAudio documentation. .. _`ctypes-pulse-structures`: ctypes pulse structures ----------------------- The parameters of some pulse functions are pointers to pulse structures. Here is an example showing how to build a ctypes pointer to the `pa_sample_spec` structure: .. code-block:: python import ctypes as ct import libpulse.libpulse as libpulse # The 'pa_sample_spec' ctypes subclass of ct.Structure. ct_struct_sample_spec = libpulse.struct_ctypes['pa_sample_spec'] # Instantiate ct_struct_sample_spec with (3, 44100, 2) sample_spec = {'format': 3, 'rate': 44100, 'channels': 2} ct_sample_spec = ct_struct_sample_spec(*sample_spec.values()) # 'ptr' may be used as a parameter of type 'pa_sample_spec *' of a ctypes # foreign function. # Using ctypes pointer() here to be able to print the pointer contents # below, but lightweight byref() is sufficient if only passing the pointer # as a function parameter. ptr = ct.pointer(ct_sample_spec) # Dereference the pointer. contents = ptr.contents # This will print 'format: 3, rate: 44100, channels: 2'. print(f'format: {contents.format}, rate: {contents.rate},' f' channels: {contents.channels}') # This will print '176400'. bps = libpulse.pa_bytes_per_second(ptr) print(bps) # Using ct.byref() instead of ct.pointer(). # This will print '176400'. bps = libpulse.pa_bytes_per_second(ct.byref(ct_sample_spec)) print(bps) A simpler way is to instantiate one of the convenience classes `Pa_buffer_attr`, `Pa_cvolume`, `Pa_channel_map`, `Pa_format_info` or `Pa_sample_spec` and call its `byref()` method. See the :ref:`CtypesPulseStructure` section. In that case the above example becomes: .. code-block:: python ptr = libpulse.Pa_sample_spec(*sample_spec.values()).byref() `examples/pa_stream_new.py`_ shows how to create instances of two structures and pass their pointers to `pa_stream_new()`. The example shows also how to build a `PulseStructure` from a pointer returned by `pa_stream_get_sample_spec()`. See the :ref:`PulseStructure` section. The implementation of the ``pactl`` module uses the `Pa_cvolume` and `Pa_channel_map` classes to build ctypes `Structure`_ instances and pass their pointer to some of the `pactl.py non-async functions`_. .. _`Error Handling`: https://freedesktop.org/software/pulseaudio/doxygen/index.html#error_sec .. _examples/pa_stream_new.py: https://gitlab.com/xdegaye/libpulse/-/blob/master/examples/pa_stream_new.py?ref_type=heads#L1 .. _`pactl.py non-async functions`: https://gitlab.com/xdegaye/libpulse/-/blob/master/libpulse/pactl.py?ref_type=heads#L30 .. _`Structure`: https://docs.python.org/3/library/ctypes.html#ctypes.Structure ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1718265550.993823 libpulse-0.7/examples/pa_context_load_module.py0000644000000000000000000000576614632523317017051 0ustar00"""Example using LibPulse async methods. The 'LoadModule' async context manager is used to ensure that the module is unloaded before exiting. A null-sink is created by loading the 'module-null-sink' module and different async methods are used to get the 'pa_sink_info' structure of the newly created null-sink. Note the restrictions in the naming of the null-sink and of its description (see the comments). """ import sys import asyncio from libpulse.libpulse import (LibPulse, PA_INVALID_INDEX, pa_context_load_module, pa_context_unload_module, pa_context_get_sink_info_by_name, pa_context_get_sink_info_by_index, pa_context_get_sink_info_list, ) # NOTE: Space characters are NOT ALLOWED in the sink name. SINK_NAME = 'my-null-sink' # NOTE: Space characters in the value of a property MUST be escaped with a # backslash. MODULE_ARG = (f'sink_name="{SINK_NAME}" ' r'sink_properties=device.description="my\ description"') class LoadModule: def __init__(self, lib_pulse, name, argument): self.lib_pulse = lib_pulse self.name = name self.argument = argument self.index = PA_INVALID_INDEX async def __aenter__(self): self.index = await self.lib_pulse.pa_context_load_module( self.name, self.argument) if self.index == PA_INVALID_INDEX: print(f'Error: cannot load module {self.name}', file=sys.stderr) sys.exit(1) return self async def __aexit__(self, exc_type, exc_value, traceback): if self.index != PA_INVALID_INDEX: await self.lib_pulse.pa_context_unload_module(self.index) async def main(): async with LibPulse('my libpulse') as lib_pulse: # Create a null sink. async with LoadModule(lib_pulse, 'module-null-sink', MODULE_ARG): # Get the pa_sink_info structure by name. sink_info = (await lib_pulse.pa_context_get_sink_info_by_name(SINK_NAME)) # Get the pa_sink_info structure by index. index = sink_info.index print(f"sink '{sink_info.name}' at index {index}") sink_info = (await lib_pulse.pa_context_get_sink_info_by_index(index)) # 'proplist' is a dict. description = sink_info.proplist['device.description'] print(f"device.description: '{description}'") # Get the pa_sink_info structure as element of the list. sink_infos = await lib_pulse.pa_context_get_sink_info_list() for sink_info in sink_infos: if sink_info.index == index: print('sink_info:\n', sink_info) break else: assert False, 'Cannot find our null sink in the list !' if __name__ == '__main__': asyncio.run(main()) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1723753957.8907092 libpulse-0.7/examples/pa_context_subscribe.py0000644000000000000000000000446714657462746016562 0ustar00"""Example processing pulse events. An asyncio task is started that processes all pulse events and put them into an asyncio queue. The main() function loads a null-sink module and processes the events received from the queue until it receives the event signaling the creation of the module. """ import asyncio from libpulse.libpulse import (LibPulse, PA_SUBSCRIPTION_MASK_ALL, pa_context_subscribe, PulseEvent, ) from pa_context_load_module import LoadModule, MODULE_ARG async def get_events(lib_pulse, evt_queue, evt_ready): try: await lib_pulse.pa_context_subscribe(PA_SUBSCRIPTION_MASK_ALL) iterator = lib_pulse.get_events_iterator() # Signal main() that we are ready and processing events. evt_ready.set_result(True) async for event in iterator: await evt_queue.put(event) # Upon receiving CancelledError, the iterator raises StopAsyncIteration # to end the iteration. print('get_events(): asyncio task has been cancelled by main().') except Exception as e: await evt_queue.put(e) async def main(): evt_queue = asyncio.Queue() async with LibPulse('my libpulse') as lib_pulse: evt_ready = lib_pulse.loop.create_future() evt_task = asyncio.create_task(get_events(lib_pulse, evt_queue, evt_ready)) # Wait for the task to be ready. await evt_ready # Load the 'module-null-sink' module and process all pulse events # until we receive the event signaling the creation of this module. async with LoadModule(lib_pulse, 'module-null-sink', MODULE_ARG) as loaded_module: while True: event = await evt_queue.get() if isinstance(event, Exception): raise event assert isinstance(event, PulseEvent) print('event:', event.facility, event.type, event.index) if (event.facility == 'module' and event.type == 'new' and event.index == loaded_module.index): evt_task.cancel() print('Got the event triggered by loading the module.') break asyncio.run(main()) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1723473829.5733526 libpulse-0.7/examples/pa_stream_new.py0000644000000000000000000000455514656417646015175 0ustar00"""Example using ctypes pulse structures. 1) Two structures are built from scratch using their ctypes types. 2) pa_stream_new() is called using pointers to these structures and returns an opaque pointer. 3) pa_stream_get_sample_spec() returns a ctypes pointer that is used to build a PulseStructure instance. The type of a PulseStructure instance is a mapping type and printing its content shows that it matches the content of the pa_sample_spec structure used to create the stream. Note: ----- pa_stream_get_sample_spec() is a plain function (not a coroutine method of the LibPulse instance) and the PulseStructure instantiation must be done manually. This is not needed for the methods of the LibPulse instance whose async functions return a structure or a list of structures. """ import sys import asyncio import ctypes as ct from libpulse.libpulse import (LibPulse, PulseStructure, struct_ctypes, pa_stream_new, pa_stream_unref, pa_stream_get_sample_spec, Pa_sample_spec, Pa_channel_map, ) async def main(): async with LibPulse('my libpulse') as lib_pulse: # Build the pa_sample_spec structure. sample_spec = Pa_sample_spec(3, 44100, 2) # Build the pa_channel_map structure. channel_map = Pa_channel_map(2, [1, 2]) # Create the stream. ct_pa_stream = pa_stream_new(lib_pulse.c_context, b'some name', sample_spec.byref(), channel_map.byref()) # From the ctypes documentation: "NULL pointers have a False # boolean value". if not ct_pa_stream: print('Error: cannot create a new stream', file=sys.stderr) sys.exit(1) try: # Get the pa_sample_spec structure as a PulseStructure instance. ct_sample_spec = pa_stream_get_sample_spec(ct_pa_stream) sample_spec = PulseStructure(ct_sample_spec.contents, struct_ctypes['pa_sample_spec']) # Print the attributes of sample_spec. # This will print: # {'format': 3, 'rate': 44100, 'channels': 2} print(sample_spec.__dict__) finally: pa_stream_unref(ct_pa_stream) asyncio.run(main()) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740302046.4888985 libpulse-0.7/images/coverage.png0000644000000000000000000000502314756563336013715 0ustar00PNG  IHDRt\n} cHRMz&u0`:pQ<bKGD pHYs``kBIDAThL_sswqwpwbyf%-0ZtVF댍ghkVSך)Ҳ:DӪM׬Kd312Z%=B\.~?~OI4QVV{+ڥNkbvOqeF)++㭷ZB,u _4-/zYP,ǚ:%r4Te9,׺N `h(zȪ Xg091c5DZVΫ: b>&*C_>/7 M#o#Йj "s$nŞI2k;pW%̽>B~y X,f|>ߴ&v4M/bt:EfCUYl6t]gttt8v}y 3T"n~N%KVvkiQrfJ6_Itٓ 1(~"Ccn!j;Njxp8 :>,6'x"<ҥK=z طo_8ơCx׸|2{/YYYt:y&// @gF#PIqq1===HĮ]HLLdxxχm۶aÆƑ#GZԕ2 FtE"8'LoHȴ5IbfDgyDN JxnB{o>$<NCoNkk+%%%ȲvillY)))aTWW*j*En Ã>@nn.?&#<.~qnl+BVVVzݻwp8BvZV^fǎ/Px/;ٜ_6چ1:S"Ѣ΍BQdSBusK |T:IjV8|lF"BIQQn;!锔r8wiiiP^^Nnn.'N 77rE!33]ٵkפnŋF&æMp:L>99ZN'ϟ'%%! 0McJ2߿g-oV;1߳+~ܜFA1@K>HPI\y=rf/3}*d2M+|Tޯ( `!gϞȑ#:u/ 蠦&"OӴpx~aիWYn;wDA b ϛMrцlZ/1G>1k isrc}u ub&0;c`t !DD.e ]CxL1i.U #D70}f婦^ؼysx`0 7egg8yt q hmmexx!҂rڵYrq]wznvf3$qwxhmm`/Jz,I ,N|=9!q9(Q+B6q)[@Rq)?y9!FqעQQ I7* #c9s?~zÇ3448ɓRTTjZL&,S^^9p,JKKpzyW%N{{{4˻s=.\ -- !~!UUUKtvvbijjرcZBtp f{:uU/ Ʈ" qwӋLvY?jJ"rS5*{U.n\w0^;b/ (Auw`p4M#!!>t]n322@tttSsd0 _GFFȲ<ĉ4k̆H_[:'v'wxgxe^+ k@hn-H xVHIW;YaT% Nפ4Y=wi֭u]^/IRFfBȲghLiUf4/7YJJ3DbaʴM5kodY^м(,,$99@ @ss3e јnYǔ8U̯V!| $aZg=ghYrwo,u. $I,GCPv3gV꿋dђ;e;+o?\$%tEXtdate:create2025-02-23T09:13:53+00:00q$%tEXtdate:modify2025-02-23T09:13:53+00:00k,q(tEXtdate:timestamp2025-02-23T09:14:06+00:00=jIENDB`././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1740302004.405901 libpulse-0.7/libpulse/__init__.py0000644000000000000000000000050314756563264014075 0ustar00"""Asyncio interface to the Pulseaudio and Pipewire pulse library.""" import sys __version__ = '0.7' MIN_PYTHON_VERSION = (3, 8) _version = sys.version_info[:2] if _version < MIN_PYTHON_VERSION: print(f'error: the python version must be at least' f' {MIN_PYTHON_VERSION}', file=sys.stderr) sys.exit(1) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740169856.4856384 libpulse-0.7/libpulse/libpulse.py0000644000000000000000000011155214756161200014144 0ustar00"""ctypes interface to the pulse library based on asyncio.""" import sys import asyncio import logging import re import pprint import ctypes as ct from abc import ABC from functools import partialmethod from . import __version__ from .libpulse_ctypes import PulseCTypes from .mainloop import MainLoop, pulse_ctypes, callback_func_ptr from .pulse_functions import pulse_functions from .pulse_enums import pulse_enums struct_ctypes = pulse_ctypes.struct_ctypes logger = logging.getLogger('libpuls') def _add_pulse_to_namespace(): def add_obj(name, obj): assert getattr(module, name, None) is None, f'{name} is duplicated' setattr(module, name, obj) # Add the pulse constants and functions to the module namespace. module = sys.modules[__name__] for name in pulse_functions['signatures']: func = pulse_ctypes.get_prototype(name) add_obj(name, func) for enum, constants in pulse_enums.items(): for name, value in constants.items(): add_obj(name, value) _add_pulse_to_namespace() del _add_pulse_to_namespace # /usr/include/pulse/def.h: # #define PA_INVALID_INDEX ((uint32_t) -1) PA_INVALID_INDEX = ct.c_uint32(-1).value # 'pa_volume_t' values defined in volume.h. PA_VOLUME_NORM = 0x10000 # Normal volume (100%, 0 dB). PA_VOLUME_MUTED = 0 # Muted (minimal valid) volume (0%, -inf dB). PA_VOLUME_MAX = 0xffffffff # UINT32_MAX/2 Maximum volume we can store. PA_VOLUME_INVALID = 0xffffffff # Special 'invalid' volume. def PA_VOLUME_IS_VALID(v): return v <= PA_VOLUME_MAX PA_CHANNELS_MAX = 32 # Defined in sample.h C_UINT_ARRAY_32 = ct.c_uint * PA_CHANNELS_MAX C_INT_ARRAY_32 = ct.c_int * PA_CHANNELS_MAX # Map values to their name. CTX_STATES = dict((eval(state), state) for state in ('PA_CONTEXT_UNCONNECTED', 'PA_CONTEXT_CONNECTING', 'PA_CONTEXT_AUTHORIZING', 'PA_CONTEXT_SETTING_NAME', 'PA_CONTEXT_READY', 'PA_CONTEXT_FAILED', 'PA_CONTEXT_TERMINATED')) OPERATION_STATES = dict((eval(state), state) for state in ('PA_OPERATION_CANCELLED', 'PA_OPERATION_DONE', 'PA_OPERATION_RUNNING')) def event_codes_to_names(): def build_events_dict(mask): for fac in globals(): if fac.startswith(prefix): val = eval(fac) if (val & mask) and val != mask: yield val, fac[prefix_len:].lower() prefix = 'PA_SUBSCRIPTION_EVENT_' prefix_len = len(prefix) facilities = {0 : 'sink'} facilities.update(build_events_dict(PA_SUBSCRIPTION_EVENT_FACILITY_MASK)) event_types = {0: 'new'} event_types.update(build_events_dict(PA_SUBSCRIPTION_EVENT_TYPE_MASK)) return facilities, event_types # Dictionaries mapping libpulse events values to their names. EVENT_FACILITIES, EVENT_TYPES = event_codes_to_names() def run_in_task(coro): """Decorator to wrap a coroutine in a task of AsyncioTasks instance.""" async def wrapper(*args, **kwargs): def get_coro_arg(): length = len(args) coro_arg = '' if length >=2: coro_arg += f'{args[1].__qualname__}(' if length >= 3: coro_arg += f'{args[2]})' else: coro_arg += ')' return coro_arg if 0: # When enabled while running the test suite, will print all the # pulseaudio coroutines that are being tested. print(f'{coro.__qualname__}({get_coro_arg()})', file=sys.stderr) lib_pulse = LibPulse._get_instance() if lib_pulse is None: raise LibPulseClosedError try: return await lib_pulse.libpulse_tasks.create_task( coro(*args, **kwargs)) except asyncio.CancelledError: logger.warning(f'{coro.__qualname__}({get_coro_arg()})' ' has been cancelled') raise return wrapper class LibPulseError(Exception): pass class LibPulseClosedError(LibPulseError): pass class LibPulseStateError(LibPulseError): pass class LibPulseOperationError(LibPulseError): pass class LibPulseClosedIteratorError(LibPulseError): pass class LibPulseInstanceExistsError(LibPulseError): pass class LibPulseArgumentError(LibPulseError): pass class EventIterator: """Pulse events asynchronous iterator.""" QUEUE_CLOSED = object() def __init__(self): self.event_queue = asyncio.Queue() self.closed = False # Public methods. def close(self): self.closed = True # Private methods. def abort(self): while True: try: self.event_queue.get_nowait() except asyncio.QueueEmpty: break self.put_nowait(self.QUEUE_CLOSED) def put_nowait(self, obj): if not self.closed: self.event_queue.put_nowait(obj) def __aiter__(self): return self async def __anext__(self): if self.closed: logger.info('Events Asynchronous Iterator is closed') raise StopAsyncIteration try: event = await self.event_queue.get() except asyncio.CancelledError: self.close() raise StopAsyncIteration if event is not self.QUEUE_CLOSED: return event self.close() raise LibPulseClosedIteratorError('Got QUEUE_CLOSED') class AsyncioTasks: def __init__(self): self._tasks = set() def create_task(self, coro): task = asyncio.create_task(coro) self._tasks.add(task) task.add_done_callback(lambda t: self._tasks.remove(t)) return task def __iter__(self): for t in self._tasks: yield t class PulseEvent: """A libpulse event. Use the event_facilities() and event_types() static methods to get all the values currently defined by the libpulse library for 'facility' and 'type'. They correspond to some of the variables defined in the pulse_enums module under the pa_subscription_event_type Enum. attributes: facility: str - name of the facility, for example 'sink'. index: int - index of the facility. type: str - type of event, normaly 'new', 'change' or 'remove'. """ def __init__(self, event_type, index): fac = event_type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK assert fac in EVENT_FACILITIES self.facility = EVENT_FACILITIES[fac] type = event_type & PA_SUBSCRIPTION_EVENT_TYPE_MASK assert type in EVENT_TYPES self.type = EVENT_TYPES[type] self.index = index @staticmethod def event_facilities(): return list(EVENT_FACILITIES.values()) @staticmethod def event_types(): return list(EVENT_TYPES.values()) class PropList(dict): """Dictionary of the elements of a proplist whose value is a string.""" def __init__(self, c_pa_proplist): super().__init__() null_ptr = ct.POINTER(ct.c_void_p)() null_ptr_ptr = ct.pointer(null_ptr) while True: key = pa_proplist_iterate(c_pa_proplist, null_ptr_ptr) if not key: break elif isinstance(key, bytes): val = pa_proplist_gets(c_pa_proplist, key) if val: self[key.decode()] = val.decode() class PulseStructure: """The representation of a ctypes Structure. When returned by a callback as a pointer to a structure, one must make a deep copy of the elements of the structure as they are only temporarily available. """ ignored_pointer_names = set() array_sizes = { 'pa_card_port_info': 'n_ports', 'pa_source_port_info': 'n_ports', 'pa_sink_port_info': 'n_ports', 'pa_card_profile_info': 'n_profiles', 'pa_card_profile_info2': 'n_profiles', 'pa_format_info': 'n_formats', } def __init__(self, c_struct, c_structure_type): for name, c_type in c_structure_type._fields_: fq_name = f'{c_structure_type.__name__}.{name}: {c_type.__name__}' if fq_name in self.ignored_pointer_names: continue try: c_struct_val = getattr(c_struct, name) except AttributeError: assert False, (f'{fq_name} not found while instantiating' f' a PulseStructure') if c_type in PulseCTypes.numeric_types.values(): setattr(self, name, c_struct_val) # A NULL pointer. elif not c_struct_val: setattr(self, name, None) elif c_type is ct.c_char_p: setattr(self, name, c_struct_val.decode()) # A proplist pointer. elif (isinstance(c_struct_val, ct._Pointer) and c_struct_val._type_.__name__ == 'pa_proplist'): setattr(self, name, PropList(c_struct_val)) # An array. elif isinstance(c_struct_val, ct.Array): # All libpulse arrays have a numeric type. setattr(self, name, c_struct_val[:]) # An array of pointers. elif (isinstance(c_struct_val, ct._Pointer) and isinstance(c_struct_val.contents, ct._Pointer)): ctype = c_struct_val.contents._type_ if ctype.__name__ in struct_ctypes: val = [] ptr = c_struct_val size_attr = self.array_sizes[ctype.__name__] array_size = getattr(self, size_attr) for i in range(array_size): if not ptr[i]: break val.append(PulseStructure(ptr[i].contents, ctype)) setattr(self, name, val) else: self.ignore_member(fq_name) # A pointer. elif isinstance(c_struct_val, ct._Pointer): if c_struct_val._type_.__name__ in struct_ctypes: setattr(self, name, PulseStructure(c_struct_val.contents, c_struct_val._type_)) else: self.ignore_member(fq_name) # A structure. else: if c_type.__name__ in struct_ctypes: setattr(self, name, PulseStructure(c_struct_val, c_type)) else: self.ignore_member(fq_name) def ignore_member(self, name): self.ignored_pointer_names.add(name) logger.debug(f"Ignoring '{name}' structure member") def __repr__(self): return pprint.pformat(self.__dict__, sort_dicts=False) class CtypesPulseStructure(ABC): """Container for an instance of a subclass of ctypes.Structure. Scalar attributes of the instance may be updated after instanciation as well as the scalar elements of an array, but not the array itself or the other aggregate attributes. """ @staticmethod def get_fields_names(struct_name): return [field[0] for field in struct_ctypes[struct_name]._fields_] def __getattr__(self, name): if name in self.fields_names: return getattr(self.ct_struct, name) else: raise AttributeError( f"'{self.__class__.__name__}' object has no attribute '{name}'") def __setattr__(self, name, val): if name in self.fields_names: setattr(self.c_obj, name, val) else: object.__setattr__(self, name, val) def byref(self): return ct.byref(self.ct_struct) def to_pulse_structure(self): return PulseStructure(self.ct_struct, struct_ctypes[self.struct_name]) class Pa_buffer_attr(CtypesPulseStructure): struct_name = 'pa_buffer_attr' fields_names = CtypesPulseStructure.get_fields_names(struct_name) def __init__(self, maxlength, tlength, prebuf, minreq, fragsize): self.ct_struct = struct_ctypes[self.struct_name]( maxlength, tlength, prebuf, minreq, fragsize) class Pa_cvolume(CtypesPulseStructure): struct_name = 'pa_cvolume' fields_names = CtypesPulseStructure.get_fields_names(struct_name) def __init__(self, channels, values): length = len(values) assert length <= PA_CHANNELS_MAX values += [0 for i in range(PA_CHANNELS_MAX - length)] self.ct_struct = struct_ctypes[self.struct_name]( channels, C_UINT_ARRAY_32(*values)) class Pa_channel_map(CtypesPulseStructure): struct_name = 'pa_channel_map' fields_names = CtypesPulseStructure.get_fields_names(struct_name) def __init__(self, channels, map): length = len(map) assert length <= PA_CHANNELS_MAX map += [0 for i in range(PA_CHANNELS_MAX - length)] self.ct_struct = struct_ctypes[self.struct_name]( channels, C_INT_ARRAY_32(*map)) class Pa_format_info(CtypesPulseStructure): struct_name = 'pa_format_info' fields_names = CtypesPulseStructure.get_fields_names(struct_name) def __init__(self, encoding, plist): # 'plist' is an instance of ctypes._Pointer that has been built using # some of the pa_proplist_ functions. self.ct_struct = struct_ctypes[self.struct_name](encoding, plist) class Pa_sample_spec(CtypesPulseStructure): struct_name = 'pa_sample_spec' fields_names = CtypesPulseStructure.get_fields_names(struct_name) def __init__(self, format, rate, channels): self.ct_struct = struct_ctypes[self.struct_name]( format, rate, channels) class LibPulse: """Interface to libpulse library as an asynchronous context manager.""" ASYNCIO_LOOPS = dict() # {asyncio loop: LibPulse instance} # Function signature: (pa_operation *, # [pa_context *, [args...], cb_t, void *]) # Callback signature: (void, [pa_context *, [objs...], void *]) context_methods = ( 'pa_context_add_autoload', 'pa_context_drain', 'pa_context_get_server_info', 'pa_context_load_module', 'pa_context_play_sample_with_proplist', # pa_context_send_message_to_object() is the only method that is not # in the 'LibPulse.context_list_methods' list and that returns a list # of objects. See the 'pa_context_string_cb_t' callback signature in # the pulse_functions module. 'pa_context_send_message_to_object', # The context state is monitored by the LibPulse instance. # 'pa_context_set_state_callback', # Reception of events is monitored by the LibPulse instance. # 'pa_context_set_subscribe_callback', 'pa_context_stat', 'pa_ext_device_restore_test', ) # Function signature: (pa_operation *, # [pa_context *, [args...], cb_t, void *]) # Callback signature: (void, [pa_context *, int, void *]) context_success_methods = ( 'pa_context_exit_daemon', 'pa_context_kill_client', 'pa_context_kill_sink_input', 'pa_context_kill_source_output', 'pa_context_move_sink_input_by_index', 'pa_context_move_sink_input_by_name', 'pa_context_move_source_output_by_index', 'pa_context_move_source_output_by_name', 'pa_context_play_sample', 'pa_context_proplist_remove', 'pa_context_proplist_update', 'pa_context_remove_autoload_by_index', 'pa_context_remove_autoload_by_name', 'pa_context_remove_sample', 'pa_context_set_card_profile_by_index', 'pa_context_set_card_profile_by_name', 'pa_context_set_default_sink', 'pa_context_set_default_source', 'pa_context_set_name', 'pa_context_set_port_latency_offset', 'pa_context_set_sink_input_mute', 'pa_context_set_sink_input_volume', 'pa_context_set_sink_mute_by_index', 'pa_context_set_sink_mute_by_name', 'pa_context_set_sink_port_by_index', 'pa_context_set_sink_port_by_name', 'pa_context_set_sink_volume_by_index', 'pa_context_set_sink_volume_by_name', 'pa_context_set_source_mute_by_index', 'pa_context_set_source_mute_by_name', 'pa_context_set_source_output_mute', 'pa_context_set_source_output_volume', 'pa_context_set_source_port_by_index', 'pa_context_set_source_port_by_name', 'pa_context_set_source_volume_by_index', 'pa_context_set_source_volume_by_name', 'pa_context_subscribe', 'pa_context_suspend_sink_by_index', 'pa_context_suspend_sink_by_name', 'pa_context_suspend_source_by_index', 'pa_context_suspend_source_by_name', 'pa_context_unload_module', 'pa_ext_device_restore_save_formats', 'pa_ext_device_restore_subscribe', ) # Function signature: (pa_operation *, [pa_context *, cb_t, void *]) # Callback signature: (void, [pa_context *, struct *, int, void *]) context_list_methods = ( 'pa_context_get_autoload_info_by_index', 'pa_context_get_autoload_info_by_name', 'pa_context_get_autoload_info_list', 'pa_context_get_card_info_by_index', 'pa_context_get_card_info_by_name', 'pa_context_get_card_info_list', 'pa_context_get_client_info', 'pa_context_get_client_info_list', 'pa_context_get_module_info', 'pa_context_get_module_info_list', 'pa_context_get_sample_info_by_index', 'pa_context_get_sample_info_by_name', 'pa_context_get_sample_info_list', 'pa_context_get_sink_info_by_index', 'pa_context_get_sink_info_by_name', 'pa_context_get_sink_info_list', 'pa_context_get_sink_input_info', 'pa_context_get_sink_input_info_list', 'pa_context_get_source_info_by_index', 'pa_context_get_source_info_by_name', 'pa_context_get_source_info_list', 'pa_context_get_source_output_info', 'pa_context_get_source_output_info_list', 'pa_ext_device_restore_read_formats', 'pa_ext_device_restore_read_formats_all', ) # Function signature: (pa_operation *, # [pa_stream *, [args...], cb_t, void *]) # Callback signature: (void, [pa_stream *, int, void *]) stream_success_methods = ( 'pa_stream_cork', 'pa_stream_drain', 'pa_stream_flush', 'pa_stream_prebuf', 'pa_stream_proplist_remove', 'pa_stream_proplist_update', 'pa_stream_set_buffer_attr', 'pa_stream_set_name', 'pa_stream_trigger', 'pa_stream_update_sample_rate', 'pa_stream_update_timing_info', ) def __init__(self, name, server=None, flags=PA_CONTEXT_NOAUTOSPAWN): """ Constructor arguments: - 'name' Name of the application. - 'server' Server name, if 'server' is None, connect to the default server. - 'flags' 'flags' and 'server' are arguments of pa_context_connect() used to connect to the server. """ logger.info(f'Python libpulse version {__version__}') assert isinstance(name, str) if server is not None: assert isinstance(server, str) server = server.encode() self.server = server self.flags = flags self.loop = asyncio.get_running_loop() if self.loop in self.ASYNCIO_LOOPS: raise LibPulseInstanceExistsError self.c_context = pa_context_new(MainLoop.C_MAINLOOP_API, name.encode()) # From the ctypes documentation: "NULL pointers have a False # boolean value". if not self.c_context: raise RuntimeError('Cannot get context from libpulse library') self.closed = False self.state = ('PA_CONTEXT_UNCONNECTED', 'PA_OK') self.main_task = asyncio.current_task(self.loop) self.libpulse_tasks = AsyncioTasks() self.state_notification = self.loop.create_future() self.event_iterator = None self.ASYNCIO_LOOPS[self.loop] = self LibPulse.add_async_methods() # Keep a reference to prevent garbage collection. self.c_context_state_callback = callback_func_ptr( 'pa_context_notify_cb_t', LibPulse.context_state_callback) self.c_context_subscribe_callback = callback_func_ptr( 'pa_context_subscribe_cb_t', LibPulse.context_subscribe_callback) # Initialisation. @staticmethod def add_async_methods(): # Register the partial methods. method_types = { 'context_methods': LibPulse._pa_context_get, 'context_success_methods': LibPulse._pa_context_op_success, 'context_list_methods': LibPulse._pa_context_get_list, 'stream_success_methods': LibPulse._pa_stream_op_success, } this_module = sys.modules[__name__] for method_type, libpulse_method in method_types.items(): func_names = getattr(LibPulse, method_type) for func_name in func_names: setattr(LibPulse, func_name, partialmethod(libpulse_method, func_name)) if hasattr(this_module, func_name): delattr(this_module, func_name) @staticmethod def _get_instance(): """Get the LibPulse instance running on the asyncio loop. The instance may not be yet connected or be in the closing state. Prefer using get_current_instance(). """ loop = asyncio.get_running_loop() try: return LibPulse.ASYNCIO_LOOPS[loop] except KeyError: return None @staticmethod def context_state_callback(c_context, c_userdata): """Call back that monitors the connection state.""" lib_pulse = LibPulse._get_instance() if lib_pulse is None: return st = pa_context_get_state(c_context) st = CTX_STATES[st] if st in ('PA_CONTEXT_READY', 'PA_CONTEXT_FAILED', 'PA_CONTEXT_TERMINATED'): error = pa_strerror(pa_context_errno(c_context)) state = (st, error.decode()) logger.info(f'LibPulse connection: {state}') state_notification = lib_pulse.state_notification lib_pulse.state = state if not state_notification.done(): state_notification.set_result(state) elif not lib_pulse.closed and st != 'PA_CONTEXT_READY': # A task is used here instead of calling directly abort() so # that pa_context_connect() has the time to handle a # previous PA_CONTEXT_READY state. asyncio.create_task(lib_pulse.abort(state)) else: logger.debug(f'LibPulse connection: {st}') @run_in_task async def _pa_context_connect(self): """Connect the context to the default server.""" pa_context_set_state_callback(self.c_context, self.c_context_state_callback, None) rc = pa_context_connect(self.c_context, self.server, self.flags, None) logger.debug(f'pa_context_connect return code: {rc}') await self.state_notification if self.state[0] != 'PA_CONTEXT_READY': raise LibPulseStateError(self.state) @staticmethod def context_subscribe_callback(c_context, event_type, index, c_userdata): """Call back to handle pulseaudio events.""" lib_pulse = LibPulse._get_instance() if lib_pulse is None: return if lib_pulse.event_iterator is not None: lib_pulse.event_iterator.put_nowait(PulseEvent(event_type, index)) # Libpulse async methods workers. @staticmethod def get_callback_data(func_name): # Get name and signature of the callback argument of 'func_name'. func_sig = pulse_functions['signatures'][func_name] args = func_sig[1] for arg in args: if arg in pulse_functions['callbacks']: callback_name = arg callback_sig = pulse_functions['callbacks'][arg] assert len(args) >= 3 and arg == args[-2] return callback_name, callback_sig def call_ctypes_func(self, func_name, operation_type, cb_func_ptr, *func_args): # Call the 'func_name' ctypes function. args = [] for arg in func_args: arg = arg.encode() if isinstance(arg, str) else arg args.append(arg) func_proto = pulse_ctypes.get_prototype(func_name) try: c_operation = func_proto(operation_type, *args, cb_func_ptr, None) except ct.ArgumentError as e: first_arg = ('c_context' if operation_type == self.c_context else 'pa_stream') raise LibPulseArgumentError( f"\nException reported by ctypes:\n" f" {e!r}" f"\nFunction arguments:\n" f" {func_name}{(first_arg, *args, cb_func_ptr, None)}\n" ) return c_operation @staticmethod async def handle_operation(c_operation, future): # From the ctypes documentation: "NULL pointers have a False # boolean value". if not c_operation: future.cancel() error = "NULL 'pa_operation' pointer" lib_pulse = LibPulse._get_instance() if lib_pulse is not None: errmsg = pa_strerror(pa_context_errno(lib_pulse.c_context)) error += f': {errmsg.decode()}' raise LibPulseOperationError(error) try: await future except asyncio.CancelledError: pa_operation_cancel(c_operation) raise finally: pa_operation_unref(c_operation) async def _pa_get(self, func_name, operation_type, *func_args): """Call an asynchronous pulse function that does not return a list. 'func_args' is the sequence of the arguments of the function preceding the callback in the function signature. The last argument (i.e. 'userdata') is set to None by call_ctypes_func(). """ def callback_func(c_operation_type, *c_results): results = [] try: for arg, c_result in zip(callback_sig[1][1:-1], c_results[:-1]): arg_list = arg.split() if arg_list[-1] == '*' and arg_list[0] in struct_ctypes: struct_name = arg_list[0] if not c_result: results.append(None) else: results.append(PulseStructure(c_result.contents, struct_ctypes[struct_name])) else: if arg == 'char *': c_result = c_result.decode() results.append(c_result) except Exception as e: results = e finally: if not notification.done(): notification.set_result(results) callback_data = self.get_callback_data(func_name) assert callback_data, f'{func_name} signature without a callback' callback_name, callback_sig = callback_data notification = self.loop.create_future() # Await on the future. cb_func_ptr = callback_func_ptr(callback_name, callback_func) c_operation = self.call_ctypes_func(func_name, operation_type, cb_func_ptr, *func_args) await LibPulse.handle_operation(c_operation, notification) results = notification.result() if isinstance(results, Exception): raise results for result in results: if result is None: raise LibPulseOperationError( 'NULL pointer result returned by the callback') if len(results) == 1: return results[0] return results async def _pa_get_list(self, func_name, operation_type, *func_args): """Call an asynchronous pulse function that returns a list. 'func_args' is the sequence of the arguments of the function preceding the callback in the function signature. The last argument (i.e. 'userdata') is set to None by call_ctypes_func(). """ def info_callback(c_operation_type, c_info, eol, c_userdata): # From the ctypes documentation: "NULL pointers have a False # boolean value". if not c_info: if not notification.done(): notification.set_result(eol) else: try: arg = callback_sig[1][1] arg_list = arg.split() assert arg_list[-1] == '*' assert arg_list[0] in struct_ctypes struct_name = arg_list[0] infos.append(PulseStructure(c_info.contents, struct_ctypes[struct_name])) except Exception as e: if not notification.done(): notification.set_result(e) callback_data = self.get_callback_data(func_name) assert callback_data, f'{func_name} signature without a callback' callback_name, callback_sig = callback_data infos = [] notification = self.loop.create_future() # Await on the future. cb_func_ptr = callback_func_ptr(callback_name, info_callback) c_operation = self.call_ctypes_func(func_name, operation_type, cb_func_ptr, *func_args) await LibPulse.handle_operation(c_operation, notification) eol = notification.result() if isinstance(eol, Exception): raise eol if eol < 0: error = "'eol' set to a negative value by the callback" errmsg = pa_strerror(pa_context_errno(self.c_context)) error += f': {errmsg.decode()}' raise LibPulseOperationError(error) if func_name.endswith(('_by_name', '_by_index', '_info', '_formats')): assert len(infos) == 1 return infos[0] return infos @run_in_task async def _run_in_task(self, method, func_name, operation_type, *func_args): try: return await method(func_name, operation_type, *func_args) except (Exception, asyncio.CancelledError) as e: return e async def _pa_context_get(self, func_name, *func_args): result = await self._run_in_task(self._pa_get, func_name, self.c_context, *func_args) if isinstance(result, (Exception, asyncio.CancelledError)): raise result return result async def _pa_success(self, func_name, operation_type, *func_args): success = await self._run_in_task(self._pa_get, func_name, operation_type, *func_args) if isinstance(success, (Exception, asyncio.CancelledError)): raise success return success async def _pa_context_op_success(self, func_name, *func_args): result = await self._pa_success(func_name, self.c_context, *func_args) if result != PA_OPERATION_DONE: error = pa_strerror(pa_context_errno(self.c_context)) raise LibPulseOperationError(f'Failure: {error.decode()}: ' f'{OPERATION_STATES[result]!r}') return result async def _pa_stream_op_success(self, func_name, pa_stream, *func_args): return await self._pa_success(func_name, pa_stream, *func_args) async def _pa_context_get_list(self, func_name, *func_args): result = await self._run_in_task(self._pa_get_list, func_name, self.c_context, *func_args) if isinstance(result, (Exception, asyncio.CancelledError)): raise result else: return result # Context manager. async def abort(self, state): # Cancelling the main task does close the LibPulse context manager. logger.error(f'The LibPulse instance has been aborted: {state}') self.main_task.cancel() async def close(self): if self.closed: return self.closed = True try: for task in self.libpulse_tasks: task.cancel() if self.event_iterator is not None: self.event_iterator.abort() pa_context_set_state_callback(self.c_context, None, None) pa_context_set_subscribe_callback(self.c_context, None, None) if self.state[0] == 'PA_CONTEXT_READY': try: await self.pa_context_drain() except (Exception, asyncio.CancelledError): # Quoting pulse documentation: # If there is nothing to drain, the function returns NULL. pass pa_context_disconnect(self.c_context) logger.info('Disconnected from libpulse context') finally: pa_context_unref(self.c_context) for loop, lib_pulse in list(self.ASYNCIO_LOOPS.items()): if lib_pulse is self: del self.ASYNCIO_LOOPS[loop] break else: logger.error('Cannot remove LibPulse instance upon closing') MainLoop.close() logger.debug('LibPulse instance closed') async def __aenter__(self): try: # Set up the two callbacks that live until this instance is # closed. self.main_task = asyncio.current_task(self.loop) await self._pa_context_connect() pa_context_set_subscribe_callback(self.c_context, self.c_context_subscribe_callback, None) return self except asyncio.CancelledError: await self.close() if self.state[0] != 'PA_CONTEXT_READY': raise LibPulseStateError(self.state) except Exception: await self.close() raise async def __aexit__(self, exc_type, exc_value, traceback): await self.close() if exc_type is asyncio.CancelledError: if self.state[0] != 'PA_CONTEXT_READY': raise LibPulseStateError(self.state) # Public methods. @staticmethod async def get_current_instance(): """Get the LibPulse running instance. Raises LibPulseStateError if the instance is not in the PA_CONTEXT_READY state. """ lib_pulse = LibPulse._get_instance() if lib_pulse is not None: await lib_pulse.state_notification if lib_pulse.state[0] != 'PA_CONTEXT_READY': raise LibPulseStateError(lib_pulse.state) return lib_pulse def get_events_iterator(self): """Return an Asynchronous Iterator of libpulse events. The iterator is used to run an async for loop over the PulseEvent instances. The async for loop can be terminated by invoking the close() method of the iterator from within the loop or from another task. """ if self.closed: raise LibPulseOperationError('The LibPulse instance is closed') if self.event_iterator is not None and not self.event_iterator.closed: raise LibPulseError('Not allowed: the current Asynchronous' ' Iterator must be closed first') self.event_iterator = EventIterator() return self.event_iterator async def log_server_info(self): if self.state[0] != 'PA_CONTEXT_READY': raise LibPulseStateError(self.state) server_info = await self.pa_context_get_server_info() server_name = server_info.server_name if re.match(r'.*\d+\.\d', server_name): # Pipewire includes the server version in the server name. logger.info(f'Server: {server_name}') else: logger.info(f'Server: {server_name} {server_info.server_version}') version = pa_context_get_protocol_version(self.c_context) server_ver = pa_context_get_server_protocol_version(self.c_context) logger.debug(f'libpulse library/server versions: ' f'{version}/{server_ver}') # 'server' is the name of the socket libpulse is connected to. server = pa_context_get_server(self.c_context) logger.debug(f'{server_name} connected to {server.decode()}') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1740045194.1564405 libpulse-0.7/libpulse/libpulse_ctypes.py0000644000000000000000000002367214755575612015557 0ustar00"""All the ctypes of the pulse library.""" import sys import sysconfig import pprint import functools import ctypes as ct from ctypes.util import find_library from .pulse_types import pulse_types from .pulse_enums import pulse_enums from .pulse_structs import pulse_structs from .pulse_functions import pulse_functions class PulseCTypesError(Exception): pass class PulseCTypesLibError(PulseCTypesError): pass class PulseCTypesNameError(PulseCTypesError): pass class PulseCTypesSignatureError(PulseCTypesError): pass class PulseCTypesCallbackError(PulseCTypesError): pass def _time_t(): """Return a ctypes type for time_t.""" # The size of 'time_t' depends on the platform. # 'SIZEOF_TIME_T' is computed by configure when building Python. sizeof_time_t = sysconfig.get_config_var('SIZEOF_TIME_T') types = (ct.c_longlong, ct.c_long, ct.c_int) sizes = [ct.sizeof(t) for t in types] assert sizeof_time_t in sizes, 'Cannot find a ctypes match for time_t.' return types[sizes.index(sizeof_time_t)] class timeval(ct.Structure): _fields_ = [ ('tv_sec', _time_t()), ('tv_usec', ct.c_long), ] class PulseCTypes: numeric_types = { 'int': ct.c_int, 'int64_t': ct.c_int64, 'unsigned': ct.c_uint, 'unsigned int': ct.c_uint, 'unsigned long':ct.c_ulong, 'uint8_t': ct.c_uint8, 'uint32_t': ct.c_uint32, 'uint64_t': ct.c_uint64, 'size_t': ct.c_size_t, 'float': ct.c_float, 'double': ct.c_double, } standard_ctypes = { 'void': None, 'char *': ct.c_char_p, 'void *': ct.c_void_p, } standard_ctypes.update(numeric_types) def __init__(self): self.known_ctypes = {} self.struct_ctypes = {} self.cb_types_params = None path = find_library('pulse') if path is None: raise PulseCTypesLibError('Cannot find the pulse library') self.clib = ct.CDLL(path) self.update_known_ctypes() self.update_struct_ctypes() def update_known_ctypes(self): for item in pulse_types: types = pulse_types[item].split() length = len(types) if length == 1: self.known_ctypes[item] = self.get_ctype(types[0]) continue elif length == 2: if types[0] == 'enum': self.known_ctypes[item] = ct.c_int continue elif types[0] == 'struct' and item == types[1]: continue raise PulseCTypesError(f'Unknown type: {item}: {types}') def get_member_ctype(self, types): # ctype_struct_class() helper. if types[0] in pulse_structs or types[0] == 'timeval': # Recursive call. struct_nested = self.ctype_struct_class(types[0]) if types[-1] == '*': ctype = ct.POINTER(struct_nested) else: ctype = struct_nested else: ctype = self.get_ctype(' '.join(types)) return ctype def ctype_struct_class(self, struct_name): """Build a ctypes Structure class.""" if struct_name in self.struct_ctypes: return self.struct_ctypes[struct_name] _fields_ = [] for member in pulse_structs[struct_name]: member_name = member[0] member_type = member[1] types = member_type.split() if types[0] == 'struct': types = types[1:] # An array of ctypes. if len(types) == 3 and types[1] == '*' and types[2] != '*': ctype = self.get_member_ctype(types[:1]) try: _fields_.append((member_name, (ctype * int(types[2])))) except ValueError: assert False, f'{struct_name}.{member_name}' # A pointer to a pointer. elif ''.join(types).endswith('**'): ctype = self.get_member_ctype(types[:1]) _fields_.append((member_name, ct.POINTER(ct.POINTER(ctype)))) else: ctype = self.get_member_ctype(types) _fields_.append((member_name, ctype)) # Create the Structure subclass. struct_class = type(struct_name, (ct.Structure, ), {'_fields_': tuple(_fields_)}) if len(struct_class._fields_) != 0: self.struct_ctypes[struct_name] = struct_class self.struct_ctypes[struct_name + ' *'] = ct.POINTER(struct_class) return struct_class def update_struct_ctypes(self): self.struct_ctypes['timeval'] = timeval self.struct_ctypes['timeval *'] = ct.POINTER(timeval) for struct_name in pulse_structs: self.ctype_struct_class(struct_name) def get_ctype(self, type_name): if type_name.startswith('struct '): type_name = type_name[7:] if type_name in self.standard_ctypes: return self.standard_ctypes[type_name] elif type_name in self.known_ctypes: return self.known_ctypes[type_name] elif type_name in self.struct_ctypes: return self.struct_ctypes[type_name] elif type_name.endswith('*'): return ct.c_void_p else: raise PulseCTypesError(f'Cannot convert to ctypes: {type_name}') @functools.lru_cache def get_callback(self, callback_name): try: val = pulse_functions['callbacks'][callback_name] except KeyError: raise PulseCTypesCallbackError( f"'{callback_name}' not a known callback") types = [] restype = self.get_ctype(val[0]) # The return type. types.append(restype) for arg in val[1]: # The args types. try: argtype = self.get_ctype(arg) except PulseCTypesError: # Not a known data type. So it must be a function pointer # to a callback. Call get_callback() recursively. assert arg in pulse_functions['callbacks'], ( f'{callback_name}: {val} - Error: {arg}') argtype = self.get_callback(arg) types.append(argtype) if self.cb_types_params is not None: self.cb_types_params[callback_name] = types return ct.CFUNCTYPE(*types) @functools.lru_cache def get_prototype(self, func_name): """Set the restype and argtypes of a 'clib' function name.""" # Ctypes does not allow None as a NULL callback function pointer. # Overriding _CFuncPtr.from_param() allows it. This is a hack as # _CFuncPtr is private. # See https://ctypes-users.narkive.com/wmJNDPu2/optional-callbacks- # passing-null-for-function-pointers. def from_param(cls, obj): if obj is None: return None # Return a NULL pointer. return ct._CFuncPtr.from_param(obj) try: func = getattr(self.clib, func_name) except AttributeError: raise PulseCTypesNameError( f"'{func_name}' is not a function of the pulse library") try: val = pulse_functions['signatures'][func_name] except KeyError: raise PulseCTypesSignatureError( f"'{func_name}' not a known signature") func.restype = self.get_ctype(val[0]) # The return type. argtypes = [] for arg in val[1]: # The args types. if arg == 'void': break # A function signature nested in this signature. if isinstance(arg, tuple): types = [] restype = self.get_ctype(arg[0]) # The return type. types.append(restype) for argument in arg[1]: # The args types. ctype = self.get_ctype(argument) types.append(ctype) argtype = ct.CFUNCTYPE(*types) argtype.from_param = classmethod(from_param) argtypes.append(argtype) continue try: argtype = self.get_ctype(arg) except PulseCTypesError: # Not a known data type. So it must be a function pointer to a # callback. argtype = self.get_callback(arg) argtype.from_param = classmethod(from_param) argtypes.append(argtype) func.argtypes = argtypes return func def python_object(ctypes_object, cls=None): obj = ct.cast(ctypes_object, ct.POINTER(ct.py_object)).contents.value if cls is not None: assert type(obj) is cls return obj def print_types(sections): types = PulseCTypes() for section in sections: if section == 'types': pprint.pprint(types.known_ctypes) elif section == 'structs': # Structures excluding pointer types. pprint.pprint(dict((t.__name__, t._fields_) for t in types.struct_ctypes.values() if hasattr(t, '_fields_'))) elif section == 'callbacks': types.cb_types_params = {} for callback_name in pulse_functions['callbacks']: types.get_callback(callback_name) pprint.pprint(types.cb_types_params) elif section == 'prototypes': for func_name in pulse_functions['signatures']: func = types.get_prototype(func_name) pprint.pprint(f'{func.__name__}: ' f'({func.restype}, {func.argtypes})') print('get_callback: ', types.get_callback.cache_info()) else: print(f"Error: '{section}' is not a valid section name") def main(): print_types(sys.argv[1:]) if __name__ == '__main__': main() ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1716476301.917826 libpulse-0.7/libpulse/mainloop.py0000644000000000000000000002310214623654616014147 0ustar00"""An implementation of the libpulse Main Loop based on asyncio.""" import sys import asyncio import logging import time import gc import ctypes as ct from .libpulse_ctypes import PulseCTypes, python_object, PulseCTypesLibError from .pulse_enums import pulse_enums logger = logging.getLogger('libpuls') try: pulse_ctypes = PulseCTypes() except PulseCTypesLibError as e: sys.exit(f'{e!r}') pa_io_event_flags = pulse_enums['pa_io_event_flags'] def callback_func_ptr(name, python_function): callback = pulse_ctypes.get_callback(name) return callback(python_function) def build_mainloop_api(): """Build an instance of the libpulse Main Loop API.""" api = {'io_new': IoEvent.io_new, 'io_enable': IoEvent.io_enable, 'io_free': IoEvent.io_free, 'io_set_destroy': PulseEvent.set_destroy, 'time_new': TimeEvent.time_new, 'time_restart': TimeEvent.time_restart, 'time_free': TimeEvent.time_free, 'time_set_destroy': PulseEvent.set_destroy, 'defer_new': DeferEvent.defer_new, 'defer_enable': DeferEvent.defer_enable, 'defer_free': DeferEvent.defer_free, 'defer_set_destroy': PulseEvent.set_destroy, 'quit': quit } class Mainloop_api(ct.Structure): _fields_ = [('userdata', ct.c_void_p)] for name in api: _fields_.append((name, pulse_ctypes.get_callback(name))) # Instantiate Mainloop_api. args = [callback_func_ptr(name, api[name]) for name in api] return Mainloop_api(None, *args) # Main Loop API functions. # There is only one asyncio loop and therefore only one MainLoop instance per # thread. The MainLoop instance referenced by any callback of the API is # obtained by calling MainLoop.get_instance(). class PulseEvent: DEBUG = False HASHES = [] def __init__(self, c_callback, c_userdata): self.mainloop = MainLoop.get_instance() self.c_callback = c_callback self.c_userdata = c_userdata self.c_destroy_cb = None self.c_self = ct.cast(ct.pointer(ct.py_object(self)), ct.c_void_p) PulseEvent.HASHES.append(hash(self)) self.debug(f'__init__ {self.__class__.__name__}') def debug(self, msg): if PulseEvent.DEBUG: index = PulseEvent.HASHES.index(hash(self)) + 1 logger.debug(f'{index}: {msg}') def destroy(self): self.debug(f'destroy-0 {self.__class__.__name__}') if self.c_destroy_cb: self.debug(f'destroy-1 {self.__class__.__name__}') self.c_destroy_cb(self.mainloop.C_MAINLOOP_API, self.c_self, self.c_userdata) def __del__(self): try: index = PulseEvent.HASHES.index(hash(self)) except ValueError: return PulseEvent.HASHES.pop(index) @staticmethod def set_destroy(c_event, c_callback): event = python_object(c_event) event.debug(f'set_destroy {event.__class__.__name__}') event.c_destroy_cb = c_callback @classmethod def cleanup(cls, mainloop): for event in cls.EVENTS: event.debug(f'cleanup {cls.__name__}') if event.mainloop is mainloop: event.destroy() class IoEvent(PulseEvent): EVENTS = set() def __init__(self, fd, c_callback, c_userdata): super().__init__(c_callback, c_userdata) self.fd = fd self.flags = pa_io_event_flags['PA_IO_EVENT_NULL'] def read_callback(self): self.debug('read_callback IoEvent') self.c_callback(self.mainloop.C_MAINLOOP_API, self.c_self, self.fd, pa_io_event_flags['PA_IO_EVENT_INPUT'], self.c_userdata) def write_callback(self): self.debug('write_callback IoEvent') self.c_callback(self.mainloop.C_MAINLOOP_API, self.c_self, self.fd, pa_io_event_flags['PA_IO_EVENT_OUTPUT'], self.c_userdata) def enable(self, flags): self.debug(f'enable IoEvent: {flags}') aio_loop = self.mainloop.aio_loop pa_io_event_input = pa_io_event_flags['PA_IO_EVENT_INPUT'] pa_io_event_output = pa_io_event_flags['PA_IO_EVENT_OUTPUT'] if flags & pa_io_event_input and not (self.flags & pa_io_event_input): aio_loop.add_reader(self.fd, self.read_callback) if not (flags & pa_io_event_input) and self.flags & pa_io_event_input: aio_loop.remove_reader(self.fd) if (flags & pa_io_event_output and not (self.flags & pa_io_event_output)): aio_loop.add_writer(self.fd, self.write_callback) if (not (flags & pa_io_event_output) and self.flags & pa_io_event_output): aio_loop.remove_writer(self.fd) self.flags = flags @staticmethod def io_new(c_mainloop_api, fd, flags, c_callback, c_userdata): event = IoEvent(fd, c_callback, c_userdata) event.enable(flags) IoEvent.EVENTS.add(event) return event.c_self.value @staticmethod def io_enable(c_io_event, flags): event = python_object(c_io_event, cls=IoEvent) event.debug(f'io_enable {flags}') event.enable(flags) @staticmethod def io_free(c_io_event): event = python_object(c_io_event, cls=IoEvent) event.debug('io_free') event.enable(pa_io_event_flags['PA_IO_EVENT_NULL']) IoEvent.EVENTS.remove(event) class TimeEvent(PulseEvent): EVENTS = set() def __init__(self, c_callback, c_userdata): super().__init__(c_callback, c_userdata) self.timer_handle = None def restart(self, c_time): if self.timer_handle is not None: self.debug('restart TimeEvent - cancel') self.timer_handle.cancel() self.timer_handle = None if c_time is not None: timeval = c_time.contents delay = timeval.tv_sec + timeval.tv_usec / 10**6 - time.time() self.debug(f'restart TimeEvent - delay: {delay}') self.timer_handle = self.mainloop.aio_loop.call_later( delay, self.c_callback, self.mainloop.C_MAINLOOP_API, self.c_self, c_time, self.c_userdata) @staticmethod def time_new(c_mainloop_api, c_time, c_callback, c_userdata): event = TimeEvent(c_callback, c_userdata) event.restart(c_time) TimeEvent.EVENTS.add(event) return event.c_self.value @staticmethod def time_restart(c_time_event, c_time): event = python_object(c_time_event, cls=TimeEvent) event.debug('time_restart') event.restart(c_time) @staticmethod def time_free(c_time_event): event = python_object(c_time_event, cls=TimeEvent) event.debug('time_free') event.restart(None) TimeEvent.EVENTS.remove(event) class DeferEvent(PulseEvent): EVENTS = set() def __init__(self, c_callback, c_userdata): super().__init__(c_callback, c_userdata) self.handle = None def enable(self, enable): self.debug(f'enable DeferEvent: {enable}') if self.handle is None and enable: self.handle = self.mainloop.aio_loop.call_soon(self.callback) elif self.handle is not None and not enable: self.handle.cancel() self.handle = None self.enabled = True if enable else False def callback(self): self.debug('callback DeferEvent') self.handle = None self.c_callback(self.mainloop.C_MAINLOOP_API, self.c_self, self.c_userdata) if self.enabled: self.handle = self.mainloop.aio_loop.call_soon(self.callback) @staticmethod def defer_new(c_mainloop_api, c_callback, c_userdata): event = DeferEvent(c_callback, c_userdata) event.enable(True) DeferEvent.EVENTS.add(event) return event.c_self.value @staticmethod def defer_enable(c_defer_event, enable): event = python_object(c_defer_event, cls=DeferEvent) event.debug(f'defer_enable {enable}') event.enable(enable) @staticmethod def defer_free(c_defer_event): event = python_object(c_defer_event, cls=DeferEvent) event.debug('defer_free') event.enable(False) DeferEvent.EVENTS.remove(event) def quit(c_mainloop_api, retval): logger.debug(f'quit() of the mainloop API called with retval={retval}') # Keep a reference to the Python Main loop API so that it is not garbage # collected. _mainloop_api = build_mainloop_api() class MainLoop: """An implementation of the libpulse Main Loop based on asyncio.""" ASYNCIO_LOOPS = dict() # {asyncio loop: MainLoop instance} C_MAINLOOP_API = ct.cast(ct.pointer(_mainloop_api), ct.c_void_p) def __init__(self, aio_loop): assert aio_loop not in MainLoop.ASYNCIO_LOOPS, ( 'There is already a MainLoop instance on this asyncio loop') self.aio_loop = aio_loop MainLoop.ASYNCIO_LOOPS[aio_loop] = self @staticmethod def get_instance(): aio_loop = asyncio.get_running_loop() mloop = MainLoop.ASYNCIO_LOOPS.get(aio_loop) return mloop if mloop is not None else MainLoop(aio_loop) @staticmethod def close(): mloop = MainLoop.get_instance() for cls in (IoEvent, TimeEvent, DeferEvent): cls.cleanup(mloop) gc.collect() for aio_loop, loop in list(MainLoop.ASYNCIO_LOOPS.items()): if loop is mloop: del MainLoop.ASYNCIO_LOOPS[aio_loop] break else: assert False, 'Cannot remove MainLoop instance upon closing' logger.info('LibPulse main loop closed') ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1739804177.520919 libpulse-0.7/libpulse/pactl.py0000644000000000000000000007747314754647022013456 0ustar00"""The special names @DEFAULT_SINK@, @DEFAULT_SOURCE@ and @DEFAULT_MONITOR@ can be used to specify the default sink, source and monitor. The output of most subcommands can be parsed by Python: run the exec() Python built-in on the output, or redirect the output to a file and import the file as a Python module. For example start an interactive Python session and inspect the 'cards' object with: $ ./pactl.py list cards > cards.py && python -i cards.py """ import sys import os import argparse import inspect import asyncio import ctypes as ct from textwrap import dedent, wrap from itertools import chain from libpulse.libpulse import ( LibPulse, LibPulseError, LibPulseInstanceExistsError, PulseStructure, struct_ctypes, Pa_cvolume, Pa_channel_map, LibPulseOperationError, PA_VOLUME_NORM, PA_VOLUME_MUTED, PA_VOLUME_IS_VALID, PA_INVALID_INDEX, PA_DEVICE_TYPE_SINK, PA_SUBSCRIPTION_MASK_ALL, PA_CHANNELS_MAX, PA_OPERATION_DONE, # Non-async functions. pa_cvolume_snprint_verbose, pa_cvolume_get_balance, pa_cvolume_set, pa_sw_cvolume_multiply, pa_sw_volume_from_linear, pa_sw_volume_from_dB, pa_format_info_from_string, pa_format_info_free, pa_strerror, pa_context_errno, ) __VERSION__ = '0.1' PGM = os.path.basename(sys.argv[0].rstrip(os.sep)) PA_CVOLUME_SNPRINT_VERBOSE_MAX = 1984 # Defined in volume.h. PA_MAX_FORMATS = 256 # Defined in internal.h. VOL_UINT = 0 VOL_PERCENT = 1 VOL_LINEAR = 2 VOL_DECIBEL = 3 VOL_ABSOLUTE = 0 << 4 VOL_RELATIVE = 1 << 4 VOLUME_DOC = """ VOLUME can be specified as an integer (e.g. 2000, 16384), a linear factor (e.g. 0.4, 1.100), a percentage (e.g. 10%, 100%) or a decibel value (e.g. 0dB, 20dB). If the volume specification start with a + or - the volume adjustment will be relative to the current sink volume. A single volume value affects all channels; if multiple volume values are given their number has to match the sink's number of channels. Note: The subcommand MUST be followed by '--' when entering a negative volume value (otherwise this will be understood as an invalid option). For example: 'pactl.py set-sink-volume -- 0 -1db +1db' """ def print_volume(info): c_volume = Pa_cvolume(info.volume.channels, info.volume.values) c_channel_map = Pa_channel_map(info.channel_map.channels, info.channel_map.map) buff = ct.create_string_buffer(PA_CVOLUME_SNPRINT_VERBOSE_MAX) volume = pa_cvolume_snprint_verbose(buff, PA_CVOLUME_SNPRINT_VERBOSE_MAX, c_volume.byref(), c_channel_map.byref(), 1) balance = pa_cvolume_get_balance( c_volume.byref(), c_channel_map.byref()) print(f'Volume: {volume.decode()}\n' f' balance {balance:.2f}') def volume_relative_adjust(cv, c_volume): sflag = c_volume.flags & 0x0f if sflag in (VOL_UINT, VOL_PERCENT): for i in range(cv.channels): add = cv.values[i] + c_volume.values[i] if add < PA_VOLUME_NORM: c_volume.values[i] = PA_VOLUME_MUTED else: c_volume.values[i] = add - PA_VOLUME_NORM if sflag in (VOL_LINEAR, VOL_DECIBEL): c_cv = Pa_cvolume(cv.channels, cv.values) pa_sw_cvolume_multiply(c_volume.byref(), c_cv.byref(), c_volume.byref()) def fill_volume(info, c_volume): supported = info.channel_map.channels if c_volume.channels == 1: pa_cvolume_set(c_volume.byref(), supported, c_volume.values[0]) elif c_volume.channels != supported: sys.exit(f'Failed to set volume: You tried to set c_volume for' f' {c_volume.channels} channels, whereas channel(s) supported' f' = {supported}') if c_volume.flags & VOL_RELATIVE: volume_relative_adjust(info.volume, c_volume) class PactlSubCommands: """Provide the methods that implement the pactl commands. The 'upload-sample' command is not implemented (requires libsndfile). """ def __init__(self, lib_pulse): self.lib_pulse = lib_pulse def exit_no_info(self, entity): error = pa_strerror(pa_context_errno(self.lib_pulse.c_context)) sys.exit(f'Failed to get {entity} information: {error.decode()}') async def run(self, args): command = args.command command = command.replace('-', '_') method = getattr(self, command) try: await method(args) except LibPulseOperationError as e: sys.exit(e.args[0]) async def cmd_stat(self, args): "Dump few statistics about memory usage of the PulseAudio daemon." lp = self.lib_pulse pa_stat_info = await lp.pa_context_stat() print(f'pa_stat_info = {pa_stat_info}') async def cmd_info(self, args): "Dump some info about the PulseAudio daemon." lp = self.lib_pulse pa_server_info = await lp.pa_context_get_server_info() print(f'pa_server_info = {pa_server_info}') async def cmd_list(self, args): """Dump all currently loaded modules, available sinks, sources, streams, etc. The argument must be set to: modules, sinks, sources, sink-inputs, source-outputs, clients, samples, cards, message-handlers. If not specified, all info is listed with the exception of the message-handlers. """ lp = self.lib_pulse list_methods = { 'modules': lp.pa_context_get_module_info_list, 'sinks': lp.pa_context_get_sink_info_list, 'sources': lp.pa_context_get_source_info_list, 'sink-inputs': lp.pa_context_get_sink_input_info_list, 'source-outputs': lp.pa_context_get_source_output_info_list, 'clients': lp.pa_context_get_client_info_list, 'samples': lp.pa_context_get_sample_info_list, 'cards': lp.pa_context_get_card_info_list, 'message-handlers': lp.pa_context_send_message_to_object, } if not args.type: for type, method in list_methods.items(): if type == 'message-handlers': continue type_list = await method() print(f"{type.replace('-', '_')} = {type_list}") else: assert args.type in list_methods if args.type == 'message-handlers': type_list = await list_methods[args.type]('/core', 'list-handlers', None) else: type_list = await list_methods[args.type]() print(f"{args.type.replace('-', '_')} = {type_list}") async def cmd_exit(self, args): "Ask the PulseAudio server to terminate." lp = self.lib_pulse await lp.pa_context_exit_daemon() print("result = 'PA_OPERATION_DONE'") async def cmd_play_sample(self, args): """Play the specified sample from the sample cache. It is played on the default sink, unless the symbolic name or the numerical index of the sink to play it on is specified. """ lp = self.lib_pulse await lp.pa_context_play_sample(args.name, args.sink, PA_VOLUME_NORM) print("result = 'PA_OPERATION_DONE'") async def cmd_remove_sample(self, args): """Remove the specified sample from the sample cache.""" lp = self.lib_pulse await lp.pa_context_remove_sample(args.name) print("result = 'PA_OPERATION_DONE'") async def cmd_load_module(self, args): """Load a module with the specified arguments into the sound server. Prints the numeric index of the module just loaded. You can use it to unload the module later. """ lp = self.lib_pulse index = await lp.pa_context_load_module(args.name, args.arguments) if index == PA_INVALID_INDEX: error = pa_strerror(pa_context_errno(lp.c_context)) sys.exit(f'Failure: {error.decode()}') print(f'index = {index}') async def cmd_unload_module(self, args): """Unload module(s). Unload the module instance identified by the specified numeric index or unload all modules by the specified name. """ lp = self.lib_pulse try: index = int(args.id_name) except ValueError: name = args.id_name count = 0 for module in await lp.pa_context_get_module_info_list(): if module.name == name: await lp.pa_context_unload_module(module.index) print("result = 'PA_OPERATION_DONE'") count += 1 if count: print("result = 'PA_OPERATION_DONE'") return await lp.pa_context_unload_module(index) print("result = 'PA_OPERATION_DONE'") async def cmd_move_sink_input(self, args): """Move the specified playback stream to the specified sink. Move the specified playback stream (identified by its numerical index) to the specified sink (identified by its symbolic name or numerical index). """ lp = self.lib_pulse await lp.pa_context_move_sink_input_by_name(args.idx, args.sink) print("result = 'PA_OPERATION_DONE'") async def cmd_move_source_output(self, args): lp = self.lib_pulse await lp.pa_context_move_source_output_by_name(args.idx, args.source) print("result = 'PA_OPERATION_DONE'") cmd_move_source_output.__doc__ = cmd_move_sink_input.__doc__.replace( 'sink', 'source').replace('playback', 'recording') async def cmd_suspend_sink(self, args): """Suspend or resume the specified sink. Suspend or resume the specified sink (which may be specified either by its symbolic name or numerical index), depending whether true (suspend) or false (resume) is passed as last argument. Suspending a sink will pause all playback. Depending on the module implementing the sink this might have the effect that the underlying device is closed, making it available for other applications to use. The exact behaviour depends on the module. """ lp = self.lib_pulse if args.sink: await lp.pa_context_suspend_sink_by_name(args.sink, args.bool) else: await lp.pa_context_suspend_sink_by_index( PA_INVALID_INDEX, args.bool) print("result = 'PA_OPERATION_DONE'") async def cmd_suspend_source(self, args): lp = self.lib_pulse if args.source: await lp.pa_context_suspend_source_by_name(args.source, args.bool) else: await lp.pa_context_suspend_source_by_index( PA_INVALID_INDEX, args.bool) print("result = 'PA_OPERATION_DONE'") cmd_suspend_source.__doc__ = cmd_suspend_sink.__doc__.replace( 'sink', 'source').replace('playback', 'capturing') async def cmd_set_card_profile(self, args): """Set the specified card to the specified profile. Set the specified card (identified by its symbolic name or numerical index) to the specified profile (identified by its symbolic name). """ lp = self.lib_pulse await lp.pa_context_set_card_profile_by_name(args.card, args.profile) print("result = 'PA_OPERATION_DONE'") async def cmd_get_default_sink(self, args): """Returns the symbolic name of the default sink.""" lp = self.lib_pulse pa_server_info = await lp.pa_context_get_server_info() default_sink_name = (f"'{pa_server_info.default_sink_name}'" if pa_server_info else None) print(f"default_sink_name = {default_sink_name}") async def cmd_get_default_source(self, args): """Returns the symbolic name of the default source.""" lp = self.lib_pulse pa_server_info = await lp.pa_context_get_server_info() default_source_name = (f"'{pa_server_info.default_source_name}'" if pa_server_info else None) print(f"default_source_name = {default_source_name}") async def cmd_set_default_sink(self, args): """Make the specified sink the default sink. Make the specified sink (identified by its symbolic name or numerical index) the default sink. Use the special name @NONE@ to unset the user defined default sink. This will make pulseaudio return to the default sink selection based on sink priority. """ lp = self.lib_pulse await lp.pa_context_set_default_sink(args.sink) print("result = 'PA_OPERATION_DONE'") async def cmd_set_default_source(self, args): lp = self.lib_pulse await lp.pa_context_set_default_source(args.source) print("result = 'PA_OPERATION_DONE'") cmd_set_default_source.__doc__ = cmd_set_default_sink.__doc__.replace( 'sink', 'source') async def cmd_set_sink_port(self, args): """Set the specified sink to the specified port. Set the specified sink (identified by its symbolic name or numerical index) to the specified port (identified by its symbolic name). """ lp = self.lib_pulse await lp.pa_context_set_sink_port_by_name(args.sink, args.port) print("result = 'PA_OPERATION_DONE'") async def cmd_set_source_port(self, args): lp = self.lib_pulse await lp.pa_context_set_source_port_by_name( args.source, args.port) print("result = 'PA_OPERATION_DONE'") cmd_set_source_port.__doc__ = cmd_set_sink_port.__doc__.replace('sink', 'source') async def cmd_get_sink_volume(self, args): """Get the volume of the specified sink.""" lp = self.lib_pulse sink = await lp.pa_context_get_sink_info_by_name(args.sink) if sink: print_volume(sink) else: self.exit_no_info('sink') async def cmd_get_source_volume(self, args): """Get the volume of the specified source.""" lp = self.lib_pulse source = await lp.pa_context_get_source_info_by_name(args.source) if source: print_volume(source) else: self.exit_no_info('source') async def cmd_get_sink_mute(self, args): """Get the mute status of the specified sink.""" lp = self.lib_pulse sink = await lp.pa_context_get_sink_info_by_name(args.sink) if sink: print(f"mute = {True if sink.mute else False}") else: self.exit_no_info('sink') async def cmd_get_source_mute(self, args): """Get the mute status of the specified source.""" lp = self.lib_pulse source = await lp.pa_context_get_source_info_by_name(args.source) if source: print(f"mute = {True if source.mute else False}") else: self.exit_no_info('source') async def cmd_set_sink_volume(self, args): """Set the volume of the specified sink. Set the volume of the specified sink (identified by its symbolic name or numerical index).""" lp = self.lib_pulse c_volume = parse_volumes(args.volumes) sink = await lp.pa_context_get_sink_info_by_name(args.sink) if not sink: self.exit_no_info('sink') fill_volume(sink, c_volume) await lp.pa_context_set_sink_volume_by_name( args.sink, c_volume.byref()) print("result = 'PA_OPERATION_DONE'") cmd_set_sink_volume.__doc__ += VOLUME_DOC async def cmd_set_source_volume(self, args): lp = self.lib_pulse c_volume = parse_volumes(args.volumes) source = await lp.pa_context_get_source_info_by_name(args.source) if not source: self.exit_no_info('source') fill_volume(source, c_volume) await lp.pa_context_set_source_volume_by_name( args.source, c_volume.byref()) print("result = 'PA_OPERATION_DONE'") cmd_set_source_volume.__doc__ = cmd_set_sink_volume.__doc__.replace( 'sink', 'source') async def cmd_set_sink_input_volume(self, args): """Set the volume of the specified sink input. Set the volume of the specified sink input (identified by its numerical index). """ lp = self.lib_pulse c_volume = parse_volumes(args.volumes) sink_input = await lp.pa_context_get_sink_input_info(args.idx) if not sink_input: self.exit_no_info('sink-input') fill_volume(sink_input, c_volume) await lp.pa_context_set_sink_input_volume(args.idx, c_volume.byref()) print("result = 'PA_OPERATION_DONE'") cmd_set_sink_input_volume.__doc__ += VOLUME_DOC.replace('sink', 'sink-input') async def cmd_set_source_output_volume(self, args): """Set the volume of the specified source output. Set the volume of the specified source output (identified by its numerical index). """ lp = self.lib_pulse c_volume = parse_volumes(args.volumes) source_output = await lp.pa_context_get_source_output_info(args.idx) if not source_output: self.exit_no_info('source-output') fill_volume(source_output, c_volume) await lp.pa_context_set_source_output_volume( args.idx, c_volume.byref()) print("result = 'PA_OPERATION_DONE'") cmd_set_source_output_volume.__doc__ += VOLUME_DOC.replace('sink', 'source-output') async def cmd_set_sink_mute(self, args): """Set the mute status of the specified sink. Set the mute status of the specified sink (identified by its symbolic name or numerical index). """ lp = self.lib_pulse if args.mute == 'toggle': sink = await lp.pa_context_get_sink_info_by_name(args.sink) mute = 0 if sink.mute else 1 await lp.pa_context_set_sink_mute_by_name(args.sink, mute) else: await lp.pa_context_set_sink_mute_by_name(args.sink, args.mute) print("result = 'PA_OPERATION_DONE'") async def cmd_set_source_mute(self, args): """Set the mute status of the specified source. Set the mute status of the specified source (identified by its symbolic name or numerical index). """ lp = self.lib_pulse if args.mute == 'toggle': source = await lp.pa_context_get_source_info_by_name(args.source) mute = 0 if source.mute else 1 await lp.pa_context_set_source_mute_by_name(args.source, mute) else: await lp.pa_context_set_source_mute_by_name(args.source, args.mute) print("result = 'PA_OPERATION_DONE'") async def cmd_set_sink_input_mute(self, args): """Set the mute status of the specified sink input. Set the mute status of the specified sink input (identified by its numerical index). """ lp = self.lib_pulse if args.mute == 'toggle': sink_input = await lp.pa_context_get_sink_input_info(args.idx) mute = 0 if sink_input.mute else 1 await lp.pa_context_set_sink_input_mute(args.idx, mute) else: await lp.pa_context_set_sink_input_mute(args.idx, args.mute) print("result = 'PA_OPERATION_DONE'") async def cmd_set_source_output_mute(self, args): """Set the mute status of the specified source output. Set the mute status of the specified source output (identified by its numerical index). """ lp = self.lib_pulse if args.mute == 'toggle': src_output = await lp.pa_context_get_source_output_info(args.idx) mute = 0 if src_output.mute else 1 await lp.pa_context_set_source_output_mute(args.idx, mute) else: await lp.pa_context_set_source_output_mute(args.idx, args.mute) print("result = 'PA_OPERATION_DONE'") async def cmd_set_sink_formats(self, args): """Set the supported formats of the specified sink. Set the supported formats of the specified sink (identified by its numerical index) if supported by the sink. FORMATS is specified as a semi-colon (;) separated list of formats in the form 'encoding[, key1=value1, key2=value2, ...]' (for example, AC3 at 32000, 44100 and 48000 Hz would be specified as 'ac3-iec61937, format.rate = "[32000, 44100, 48000 ]"'). See https://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/SupportedAudioFormats/ for possible encodings. """ c_format_pointers = [] try: for format in args.formats.split(';'): f = pa_format_info_from_string(format.strip().encode()) if not f: sys.exit( f"Failed to set format: invalid format string '{format}'") c_format_pointers.append(f) lp = self.lib_pulse PA_FORMAT_INFO_PTR = ct.POINTER(struct_ctypes['pa_format_info']) F_ARRAY = PA_FORMAT_INFO_PTR * PA_MAX_FORMATS await lp.pa_ext_device_restore_save_formats( PA_DEVICE_TYPE_SINK, args.idx, len(c_format_pointers), F_ARRAY(*c_format_pointers)) print("result = 'PA_OPERATION_DONE'") finally: for f in c_format_pointers: pa_format_info_free(f) async def cmd_send_message(self, args): """Send a message to the specified recipient object. If applicable an additional string containing message parameters can be specified. A string is returned as a response to the message. For available messages see https://cgit.freedesktop.org/pulseaudio/pulseaudio/tree/doc/messaging_api.txt """ lp = self.lib_pulse result, response = await lp.pa_context_send_message_to_object( args.rcv, args.msg, args.params) if result != PA_OPERATION_DONE: error = pa_strerror(pa_context_errno(lp.c_context)) sys.exit(f'Failure: {error.decode()}') print(f'response = {response}') async def cmd_subscribe(self, args): """Subscribe to events. pactl does not exit by itself, but keeps waiting for new events. The optional EOF parameter allows for conditional exit, mostly for testing. """ lp = self.lib_pulse # All events except autoload events. await lp.pa_context_subscribe(PA_SUBSCRIPTION_MASK_ALL) iterator = lp.get_events_iterator() async for event in iterator: event = f"Event '{event.type}' on {event.facility} #{event.index}" print(event) if args.eof is not None and event.startswith(args.eof): return def dispatch_help(args): """Print help on a subcommand with 'pactl.py help SUBCOMMAND'.""" command = args.subcommand if command is None: command = 'help' args.parsers[command].print_help() command = command.replace('-', '_') cmd_func = getattr(PactlSubCommands, 'cmd_%s' % command, None) if cmd_func: lines = cmd_func.__doc__.splitlines() print('\n%s\n' % lines[0]) paragraph = [] for l in dedent('\n'.join(lines[2:])).splitlines(): if l == '': if paragraph: print('\n'.join(wrap(' '.join(paragraph), width=80))) print() paragraph = [] continue paragraph.append(l) if paragraph: print('\n'.join(wrap(' '.join(paragraph), width=80))) def parse_volume(volume): flags = VOL_RELATIVE if volume.startswith(('+', '-')) else VOL_ABSOLUTE if volume.endswith('%'): flags |= VOL_PERCENT volume = volume[:-1] elif volume.endswith(('db', 'dB')): flags |= VOL_DECIBEL volume = volume[:-2] elif '.' in volume: flags |= VOL_LINEAR try: value = float(volume) except ValueError as e: sys.exit(f'{e!r}') sflag = flags & 0x0f if flags & VOL_RELATIVE: if sflag == VOL_UINT: value += PA_VOLUME_NORM elif sflag == VOL_PERCENT: value += 100.0 elif sflag == VOL_LINEAR: value += 1.0 if sflag == VOL_PERCENT: value = value * PA_VOLUME_NORM / 100 elif sflag == VOL_LINEAR: value = pa_sw_volume_from_linear(value) elif sflag == VOL_DECIBEL: value = pa_sw_volume_from_dB(value) if not PA_VOLUME_IS_VALID(value): sys.exit('Volume outside permissible range') return value, flags def parse_volumes(volumes): length = len(volumes) if length >= PA_CHANNELS_MAX: sys.exit('Invalid number of volume specifications') volume_flags = None values = [] for volume in volumes: value, flags = parse_volume(volume) if volume_flags is None: volume_flags = flags elif flags != volume_flags: sys.exit('Inconsistent volume specification') values.append(int(value)) c_volume = Pa_cvolume(length, values) c_volume.flags = volume_flags return c_volume def parse_boolean(val): true = ('1', 'y', 't', 'yes', 'true', 'on') false = ('0', 'n', 'f', 'no', 'false', 'off') if val in chain(true, (v.upper() for v in true)): return 1 elif val in chain(false, (v.upper() for v in false)): return 0 else: raise argparse.ArgumentTypeError def parse_mute(val): if val == 'toggle': return val return parse_boolean(val) def to_bytes(val): if val is None: return None return val.encode() def parse_args(argv): # Instantiate the main parser. usage= ('pactl.py --version | help | help SUBCOMMAND |' ' SUBCOMMAND [ARGS ...]') main_parser = argparse.ArgumentParser(prog=PGM, usage=usage, formatter_class=argparse.RawDescriptionHelpFormatter, description=__doc__, add_help=False) main_parser.add_argument('--version', '-v', action='version', version='%(prog)s ' + __VERSION__) # The help subparser handles the help for each command and uses the # 'parsers' dict for that purpose. subparsers = main_parser.add_subparsers(title='subcommands', prog='pactl.py', required=True) parsers = { 'help': main_parser } parser = subparsers.add_parser('help', add_help=False, help=dispatch_help.__doc__) parser.add_argument('subcommand', choices=parsers, nargs='?', default=None) parser.set_defaults(command='dispatch_help', parsers=parsers) # Add the subcommands subparsers. d = dict(inspect.getmembers(PactlSubCommands, inspect.isfunction)) for command in sorted(d): if not command.startswith('cmd_'): continue cmd = command[4:] cmd = cmd.replace('_', '-') func = d[command] parser = subparsers.add_parser(cmd, help=func.__doc__.splitlines()[0], add_help=False) parser.set_defaults(command=command) parser.add_argument('--server', '-s', help='name of the server to connect to') parser.add_argument('--client-name', '-n', metavar='NAME', help='how to call this client on the server') if cmd == 'list': parser.add_argument('type', nargs='?', choices=['modules', 'sinks', 'sources', 'sink-inputs', 'source-outputs', 'clients', 'samples', 'cards', 'message-handlers'], metavar='TYPE') if cmd in ('play-sample', 'remove-sample', 'load-module'): parser.add_argument('name', metavar='NAME') if cmd in ('move-sink-input', 'move-source-output', 'set-sink-input-volume', 'set-source-output-volume', 'set-sink-input-mute', 'set-source-output-mute', 'set-sink-formats'): parser.add_argument('idx', metavar='INDEX', type=int) if cmd in ('move-sink-input', 'suspend-sink', 'set-default-sink', 'set-sink-port', 'get-sink-volume', 'get-sink-mute', 'set-sink-volume', 'set-sink-mute'): parser.add_argument('sink', metavar='SINK') if cmd in ('move-source-output', 'suspend-source', 'set-default-source', 'set-source-port', 'get-source-volume', 'get-source-mute', 'set-source-volume', 'set-source-mute'): parser.add_argument('source', metavar='SOURCE') if cmd == 'set-card-profile': parser.add_argument('card', metavar='CARD') if cmd == 'send-message': parser.add_argument('rcv', type=to_bytes, metavar='RECIPIENT') parser.add_argument('msg', type=to_bytes, metavar='MESSAGE') parser.add_argument('params', type=to_bytes, metavar='MESSAGE_PARAMETERS', nargs='?') if cmd == 'set-card-profile': parser.add_argument('profile', metavar='PROFILE') if cmd in ('set-sink-port', 'set-source-port'): parser.add_argument('port', metavar='PORT') if cmd == 'play-sample': parser.add_argument('sink', metavar='SINK', nargs='?') if cmd == 'load-module': parser.add_argument('arguments', metavar='ARGUMENTS', nargs='?') if cmd == 'unload-module': parser.add_argument('id_name', metavar='ID|NAME') if cmd == 'set-sink-formats': parser.add_argument('formats', metavar='FORMATS') if cmd in ('suspend-sink', 'suspend-source'): parser.add_argument('bool', type=parse_boolean, metavar='true|false') if cmd in ('set-sink-mute', 'set-source-mute', 'set-sink-input-mute', 'set-source-output-mute'): parser.add_argument('mute', type=parse_mute, metavar='1|0|toggle') if cmd in ('set-sink-volume', 'set-source-volume', 'set-sink-input-volume', 'set-source-output-volume'): parser.add_argument('volumes', metavar='VOLUME', nargs='+') if cmd in 'subscribe': parser.add_argument('eof', metavar='EOF', nargs='?') parsers[cmd] = parser return main_parser.parse_args(argv[1:]) async def main(argv=sys.argv): args = parse_args(argv) if args.command == 'dispatch_help': dispatch_help(args) else: name = args.client_name if args.client_name else 'pactl.py' server = args.server if args.server else None try: try: async with LibPulse(name, server=server, flags=0) as lib_pulse: await PactlSubCommands(lib_pulse).run(args) except LibPulseInstanceExistsError: lib_pulse = await LibPulse.get_current_instance() await PactlSubCommands(lib_pulse).run(args) except LibPulseError as e: sys.exit(f'{e!r}') def script_main(): asyncio.run(main()) if __name__ == '__main__': asyncio.run(main()) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1721565556.7635758 libpulse-0.7/libpulse/pulse_enums.py0000644000000000000000000003343414647200565014675 0ustar00"""File pulse_enums. This file has been generated by libpulse_parser.py - DO NOT MODIFY. """ pulse_enums = \ {'pa_autoload_type': {'PA_AUTOLOAD_SINK': 0, 'PA_AUTOLOAD_SOURCE': 1}, 'pa_channel_map_def': {'PA_CHANNEL_MAP_AIFF': 0, 'PA_CHANNEL_MAP_ALSA': 1, 'PA_CHANNEL_MAP_AUX': 2, 'PA_CHANNEL_MAP_DEFAULT': 0, 'PA_CHANNEL_MAP_DEF_MAX': 5, 'PA_CHANNEL_MAP_OSS': 4, 'PA_CHANNEL_MAP_WAVEEX': 3}, 'pa_channel_position': {'PA_CHANNEL_POSITION_AUX0': 12, 'PA_CHANNEL_POSITION_AUX1': 13, 'PA_CHANNEL_POSITION_AUX10': 22, 'PA_CHANNEL_POSITION_AUX11': 23, 'PA_CHANNEL_POSITION_AUX12': 24, 'PA_CHANNEL_POSITION_AUX13': 25, 'PA_CHANNEL_POSITION_AUX14': 26, 'PA_CHANNEL_POSITION_AUX15': 27, 'PA_CHANNEL_POSITION_AUX16': 28, 'PA_CHANNEL_POSITION_AUX17': 29, 'PA_CHANNEL_POSITION_AUX18': 30, 'PA_CHANNEL_POSITION_AUX19': 31, 'PA_CHANNEL_POSITION_AUX2': 14, 'PA_CHANNEL_POSITION_AUX20': 32, 'PA_CHANNEL_POSITION_AUX21': 33, 'PA_CHANNEL_POSITION_AUX22': 34, 'PA_CHANNEL_POSITION_AUX23': 35, 'PA_CHANNEL_POSITION_AUX24': 36, 'PA_CHANNEL_POSITION_AUX25': 37, 'PA_CHANNEL_POSITION_AUX26': 38, 'PA_CHANNEL_POSITION_AUX27': 39, 'PA_CHANNEL_POSITION_AUX28': 40, 'PA_CHANNEL_POSITION_AUX29': 41, 'PA_CHANNEL_POSITION_AUX3': 15, 'PA_CHANNEL_POSITION_AUX30': 42, 'PA_CHANNEL_POSITION_AUX31': 43, 'PA_CHANNEL_POSITION_AUX4': 16, 'PA_CHANNEL_POSITION_AUX5': 17, 'PA_CHANNEL_POSITION_AUX6': 18, 'PA_CHANNEL_POSITION_AUX7': 19, 'PA_CHANNEL_POSITION_AUX8': 20, 'PA_CHANNEL_POSITION_AUX9': 21, 'PA_CHANNEL_POSITION_CENTER': 3, 'PA_CHANNEL_POSITION_FRONT_CENTER': 3, 'PA_CHANNEL_POSITION_FRONT_LEFT': 1, 'PA_CHANNEL_POSITION_FRONT_LEFT_OF_CENTER': 8, 'PA_CHANNEL_POSITION_FRONT_RIGHT': 2, 'PA_CHANNEL_POSITION_FRONT_RIGHT_OF_CENTER': 9, 'PA_CHANNEL_POSITION_INVALID': -1, 'PA_CHANNEL_POSITION_LEFT': 1, 'PA_CHANNEL_POSITION_LFE': 7, 'PA_CHANNEL_POSITION_MAX': 51, 'PA_CHANNEL_POSITION_MONO': 0, 'PA_CHANNEL_POSITION_REAR_CENTER': 4, 'PA_CHANNEL_POSITION_REAR_LEFT': 5, 'PA_CHANNEL_POSITION_REAR_RIGHT': 6, 'PA_CHANNEL_POSITION_RIGHT': 2, 'PA_CHANNEL_POSITION_SIDE_LEFT': 10, 'PA_CHANNEL_POSITION_SIDE_RIGHT': 11, 'PA_CHANNEL_POSITION_SUBWOOFER': 7, 'PA_CHANNEL_POSITION_TOP_CENTER': 44, 'PA_CHANNEL_POSITION_TOP_FRONT_CENTER': 47, 'PA_CHANNEL_POSITION_TOP_FRONT_LEFT': 45, 'PA_CHANNEL_POSITION_TOP_FRONT_RIGHT': 46, 'PA_CHANNEL_POSITION_TOP_REAR_CENTER': 50, 'PA_CHANNEL_POSITION_TOP_REAR_LEFT': 48, 'PA_CHANNEL_POSITION_TOP_REAR_RIGHT': 49}, 'pa_context_flags': {'PA_CONTEXT_NOAUTOSPAWN': 1, 'PA_CONTEXT_NOFAIL': 2, 'PA_CONTEXT_NOFLAGS': 0}, 'pa_context_state': {'PA_CONTEXT_AUTHORIZING': 2, 'PA_CONTEXT_CONNECTING': 1, 'PA_CONTEXT_FAILED': 5, 'PA_CONTEXT_READY': 4, 'PA_CONTEXT_SETTING_NAME': 3, 'PA_CONTEXT_TERMINATED': 6, 'PA_CONTEXT_UNCONNECTED': 0}, 'pa_device_port_type': {'PA_DEVICE_PORT_TYPE_ANALOG': 22, 'PA_DEVICE_PORT_TYPE_AUX': 1, 'PA_DEVICE_PORT_TYPE_BLUETOOTH': 15, 'PA_DEVICE_PORT_TYPE_CAR': 18, 'PA_DEVICE_PORT_TYPE_EARPIECE': 8, 'PA_DEVICE_PORT_TYPE_HANDSET': 7, 'PA_DEVICE_PORT_TYPE_HANDSFREE': 17, 'PA_DEVICE_PORT_TYPE_HDMI': 10, 'PA_DEVICE_PORT_TYPE_HEADPHONES': 3, 'PA_DEVICE_PORT_TYPE_HEADSET': 6, 'PA_DEVICE_PORT_TYPE_HIFI': 19, 'PA_DEVICE_PORT_TYPE_LINE': 4, 'PA_DEVICE_PORT_TYPE_MIC': 5, 'PA_DEVICE_PORT_TYPE_NETWORK': 21, 'PA_DEVICE_PORT_TYPE_PHONE': 20, 'PA_DEVICE_PORT_TYPE_PORTABLE': 16, 'PA_DEVICE_PORT_TYPE_RADIO': 12, 'PA_DEVICE_PORT_TYPE_SPDIF': 9, 'PA_DEVICE_PORT_TYPE_SPEAKER': 2, 'PA_DEVICE_PORT_TYPE_TV': 11, 'PA_DEVICE_PORT_TYPE_UNKNOWN': 0, 'PA_DEVICE_PORT_TYPE_USB': 14, 'PA_DEVICE_PORT_TYPE_VIDEO': 13}, 'pa_device_type': {'PA_DEVICE_TYPE_SINK': 0, 'PA_DEVICE_TYPE_SOURCE': 1}, 'pa_direction': {'PA_DIRECTION_INPUT': 2, 'PA_DIRECTION_OUTPUT': 1}, 'pa_encoding': {'PA_ENCODING_AC3_IEC61937': 2, 'PA_ENCODING_ANY': 0, 'PA_ENCODING_DTSHD_IEC61937': 8, 'PA_ENCODING_DTS_IEC61937': 5, 'PA_ENCODING_EAC3_IEC61937': 3, 'PA_ENCODING_INVALID': -1, 'PA_ENCODING_MAX': 9, 'PA_ENCODING_MPEG2_AAC_IEC61937': 6, 'PA_ENCODING_MPEG_IEC61937': 4, 'PA_ENCODING_PCM': 1, 'PA_ENCODING_TRUEHD_IEC61937': 7}, 'pa_error_code': {'PA_ERR_ACCESS': 1, 'PA_ERR_AUTHKEY': 9, 'PA_ERR_BADSTATE': 15, 'PA_ERR_BUSY': 26, 'PA_ERR_COMMAND': 2, 'PA_ERR_CONNECTIONREFUSED': 6, 'PA_ERR_CONNECTIONTERMINATED': 11, 'PA_ERR_EXIST': 4, 'PA_ERR_FORKED': 24, 'PA_ERR_INTERNAL': 10, 'PA_ERR_INVALID': 3, 'PA_ERR_INVALIDSERVER': 13, 'PA_ERR_IO': 25, 'PA_ERR_KILLED': 12, 'PA_ERR_MAX': 27, 'PA_ERR_MODINITFAILED': 14, 'PA_ERR_NODATA': 16, 'PA_ERR_NOENTITY': 5, 'PA_ERR_NOEXTENSION': 21, 'PA_ERR_NOTIMPLEMENTED': 23, 'PA_ERR_NOTSUPPORTED': 19, 'PA_ERR_OBSOLETE': 22, 'PA_ERR_PROTOCOL': 7, 'PA_ERR_TIMEOUT': 8, 'PA_ERR_TOOLARGE': 18, 'PA_ERR_UNKNOWN': 20, 'PA_ERR_VERSION': 17, 'PA_OK': 0}, 'pa_io_event_flags': {'PA_IO_EVENT_ERROR': 8, 'PA_IO_EVENT_HANGUP': 4, 'PA_IO_EVENT_INPUT': 1, 'PA_IO_EVENT_NULL': 0, 'PA_IO_EVENT_OUTPUT': 2}, 'pa_operation_state': {'PA_OPERATION_CANCELLED': 2, 'PA_OPERATION_DONE': 1, 'PA_OPERATION_RUNNING': 0}, 'pa_port_available': {'PA_PORT_AVAILABLE_NO': 1, 'PA_PORT_AVAILABLE_UNKNOWN': 0, 'PA_PORT_AVAILABLE_YES': 2}, 'pa_prop_type_t': {'PA_PROP_TYPE_INT': 0, 'PA_PROP_TYPE_INT_ARRAY': 2, 'PA_PROP_TYPE_INT_RANGE': 1, 'PA_PROP_TYPE_INVALID': -1, 'PA_PROP_TYPE_STRING': 3, 'PA_PROP_TYPE_STRING_ARRAY': 4}, 'pa_sample_format': {'PA_SAMPLE_ALAW': 1, 'PA_SAMPLE_FLOAT32BE': 6, 'PA_SAMPLE_FLOAT32LE': 5, 'PA_SAMPLE_INVALID': -1, 'PA_SAMPLE_MAX': 13, 'PA_SAMPLE_S16BE': 4, 'PA_SAMPLE_S16LE': 3, 'PA_SAMPLE_S24BE': 10, 'PA_SAMPLE_S24LE': 9, 'PA_SAMPLE_S24_32BE': 12, 'PA_SAMPLE_S24_32LE': 11, 'PA_SAMPLE_S32BE': 8, 'PA_SAMPLE_S32LE': 7, 'PA_SAMPLE_U8': 0, 'PA_SAMPLE_ULAW': 2}, 'pa_seek_mode': {'PA_SEEK_ABSOLUTE': 1, 'PA_SEEK_RELATIVE': 0, 'PA_SEEK_RELATIVE_END': 3, 'PA_SEEK_RELATIVE_ON_READ': 2}, 'pa_sink_flags': {'PA_SINK_DECIBEL_VOLUME': 32, 'PA_SINK_DYNAMIC_LATENCY': 128, 'PA_SINK_FLAT_VOLUME': 64, 'PA_SINK_HARDWARE': 4, 'PA_SINK_HW_MUTE_CTRL': 16, 'PA_SINK_HW_VOLUME_CTRL': 1, 'PA_SINK_LATENCY': 2, 'PA_SINK_NETWORK': 8, 'PA_SINK_NOFLAGS': 0, 'PA_SINK_SET_FORMATS': 256}, 'pa_sink_state': {'PA_SINK_IDLE': 1, 'PA_SINK_INIT': -2, 'PA_SINK_INVALID_STATE': -1, 'PA_SINK_RUNNING': 0, 'PA_SINK_SUSPENDED': 2, 'PA_SINK_UNLINKED': -3}, 'pa_source_flags': {'PA_SOURCE_DECIBEL_VOLUME': 32, 'PA_SOURCE_DYNAMIC_LATENCY': 64, 'PA_SOURCE_FLAT_VOLUME': 128, 'PA_SOURCE_HARDWARE': 4, 'PA_SOURCE_HW_MUTE_CTRL': 16, 'PA_SOURCE_HW_VOLUME_CTRL': 1, 'PA_SOURCE_LATENCY': 2, 'PA_SOURCE_NETWORK': 8, 'PA_SOURCE_NOFLAGS': 0}, 'pa_source_state': {'PA_SOURCE_IDLE': 1, 'PA_SOURCE_INIT': -2, 'PA_SOURCE_INVALID_STATE': -1, 'PA_SOURCE_RUNNING': 0, 'PA_SOURCE_SUSPENDED': 2, 'PA_SOURCE_UNLINKED': -3}, 'pa_stream_direction': {'PA_STREAM_NODIRECTION': 0, 'PA_STREAM_PLAYBACK': 1, 'PA_STREAM_RECORD': 2, 'PA_STREAM_UPLOAD': 3}, 'pa_stream_flags': {'PA_STREAM_ADJUST_LATENCY': 8192, 'PA_STREAM_AUTO_TIMING_UPDATE': 8, 'PA_STREAM_DONT_INHIBIT_AUTO_SUSPEND': 32768, 'PA_STREAM_DONT_MOVE': 512, 'PA_STREAM_EARLY_REQUESTS': 16384, 'PA_STREAM_FAIL_ON_SUSPEND': 131072, 'PA_STREAM_FIX_CHANNELS': 256, 'PA_STREAM_FIX_FORMAT': 64, 'PA_STREAM_FIX_RATE': 128, 'PA_STREAM_INTERPOLATE_TIMING': 2, 'PA_STREAM_NOFLAGS': 0, 'PA_STREAM_NOT_MONOTONIC': 4, 'PA_STREAM_NO_REMAP_CHANNELS': 16, 'PA_STREAM_NO_REMIX_CHANNELS': 32, 'PA_STREAM_PASSTHROUGH': 524288, 'PA_STREAM_PEAK_DETECT': 2048, 'PA_STREAM_RELATIVE_VOLUME': 262144, 'PA_STREAM_START_CORKED': 1, 'PA_STREAM_START_MUTED': 4096, 'PA_STREAM_START_UNMUTED': 65536, 'PA_STREAM_VARIABLE_RATE': 1024}, 'pa_stream_state': {'PA_STREAM_CREATING': 1, 'PA_STREAM_FAILED': 3, 'PA_STREAM_READY': 2, 'PA_STREAM_TERMINATED': 4, 'PA_STREAM_UNCONNECTED': 0}, 'pa_subscription_event_type': {'PA_SUBSCRIPTION_EVENT_AUTOLOAD': 8, 'PA_SUBSCRIPTION_EVENT_CARD': 9, 'PA_SUBSCRIPTION_EVENT_CHANGE': 16, 'PA_SUBSCRIPTION_EVENT_CLIENT': 5, 'PA_SUBSCRIPTION_EVENT_FACILITY_MASK': 15, 'PA_SUBSCRIPTION_EVENT_MODULE': 4, 'PA_SUBSCRIPTION_EVENT_NEW': 0, 'PA_SUBSCRIPTION_EVENT_REMOVE': 32, 'PA_SUBSCRIPTION_EVENT_SAMPLE_CACHE': 6, 'PA_SUBSCRIPTION_EVENT_SERVER': 7, 'PA_SUBSCRIPTION_EVENT_SINK': 0, 'PA_SUBSCRIPTION_EVENT_SINK_INPUT': 2, 'PA_SUBSCRIPTION_EVENT_SOURCE': 1, 'PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT': 3, 'PA_SUBSCRIPTION_EVENT_TYPE_MASK': 48}, 'pa_subscription_mask': {'PA_SUBSCRIPTION_MASK_ALL': 767, 'PA_SUBSCRIPTION_MASK_AUTOLOAD': 256, 'PA_SUBSCRIPTION_MASK_CARD': 512, 'PA_SUBSCRIPTION_MASK_CLIENT': 32, 'PA_SUBSCRIPTION_MASK_MODULE': 16, 'PA_SUBSCRIPTION_MASK_NULL': 0, 'PA_SUBSCRIPTION_MASK_SAMPLE_CACHE': 64, 'PA_SUBSCRIPTION_MASK_SERVER': 128, 'PA_SUBSCRIPTION_MASK_SINK': 1, 'PA_SUBSCRIPTION_MASK_SINK_INPUT': 4, 'PA_SUBSCRIPTION_MASK_SOURCE': 2, 'PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT': 8}, 'pa_update_mode': {'PA_UPDATE_MERGE': 1, 'PA_UPDATE_REPLACE': 2, 'PA_UPDATE_SET': 0}} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1721565556.7669091 libpulse-0.7/libpulse/pulse_functions.py0000644000000000000000000021052114647200565015550 0ustar00"""File pulse_functions. This file has been generated by libpulse_parser.py - DO NOT MODIFY. """ pulse_functions = \ {'callbacks': {'defer_enable': ('void', ['pa_defer_event *', 'int']), 'defer_free': ('void', ['pa_defer_event *']), 'defer_new': ('pa_defer_event *', ['pa_mainloop_api *', 'pa_defer_event_cb_t', 'void *']), 'defer_set_destroy': ('void', ['pa_defer_event *', 'pa_defer_event_destroy_cb_t']), 'io_enable': ('void', ['pa_io_event *', 'pa_io_event_flags_t']), 'io_free': ('void', ['pa_io_event *']), 'io_new': ('pa_io_event *', ['pa_mainloop_api *', 'int', 'pa_io_event_flags_t', 'pa_io_event_cb_t', 'void *']), 'io_set_destroy': ('void', ['pa_io_event *', 'pa_io_event_destroy_cb_t']), 'pa_autoload_info_cb_t': ('void', ['pa_context *', 'pa_autoload_info *', 'int', 'void *']), 'pa_card_info_cb_t': ('void', ['pa_context *', 'pa_card_info *', 'int', 'void *']), 'pa_client_info_cb_t': ('void', ['pa_context *', 'pa_client_info *', 'int', 'void *']), 'pa_context_event_cb_t': ('void', ['pa_context *', 'char *', 'pa_proplist *', 'void *']), 'pa_context_index_cb_t': ('void', ['pa_context *', 'uint32_t', 'void *']), 'pa_context_notify_cb_t': ('void', ['pa_context *', 'void *']), 'pa_context_play_sample_cb_t': ('void', ['pa_context *', 'uint32_t', 'void *']), 'pa_context_string_cb_t': ('void', ['pa_context *', 'int', 'char *', 'void *']), 'pa_context_subscribe_cb_t': ('void', ['pa_context *', 'pa_subscription_event_type_t', 'uint32_t', 'void *']), 'pa_context_success_cb_t': ('void', ['pa_context *', 'int', 'void *']), 'pa_defer_event_cb_t': ('void', ['pa_mainloop_api *', 'pa_defer_event *', 'void *']), 'pa_defer_event_destroy_cb_t': ('void', ['pa_mainloop_api *', 'pa_defer_event *', 'void *']), 'pa_ext_device_restore_read_device_formats_cb_t': ('void', ['pa_context *', 'pa_ext_device_restore_info *', 'int', 'void *']), 'pa_ext_device_restore_subscribe_cb_t': ('void', ['pa_context *', 'pa_device_type_t', 'uint32_t', 'void *']), 'pa_ext_device_restore_test_cb_t': ('void', ['pa_context *', 'uint32_t', 'void *']), 'pa_free_cb_t': ('void', ['void *']), 'pa_io_event_cb_t': ('void', ['pa_mainloop_api *', 'pa_io_event *', 'int', 'pa_io_event_flags_t', 'void *']), 'pa_io_event_destroy_cb_t': ('void', ['pa_mainloop_api *', 'pa_io_event *', 'void *']), 'pa_module_info_cb_t': ('void', ['pa_context *', 'pa_module_info *', 'int', 'void *']), 'pa_operation_notify_cb_t': ('void', ['pa_operation *', 'void *']), 'pa_poll_func': ('int', ['struct pollfd *', 'unsigned long', 'int', 'void *']), 'pa_sample_info_cb_t': ('void', ['pa_context *', 'pa_sample_info *', 'int', 'void *']), 'pa_server_info_cb_t': ('void', ['pa_context *', 'pa_server_info *', 'void *']), 'pa_signal_cb_t': ('void', ['pa_mainloop_api *', 'pa_signal_event *', 'int', 'void *']), 'pa_signal_destroy_cb_t': ('void', ['pa_mainloop_api *', 'pa_signal_event *', 'void *']), 'pa_sink_info_cb_t': ('void', ['pa_context *', 'pa_sink_info *', 'int', 'void *']), 'pa_sink_input_info_cb_t': ('void', ['pa_context *', 'pa_sink_input_info *', 'int', 'void *']), 'pa_source_info_cb_t': ('void', ['pa_context *', 'pa_source_info *', 'int', 'void *']), 'pa_source_output_info_cb_t': ('void', ['pa_context *', 'pa_source_output_info *', 'int', 'void *']), 'pa_stat_info_cb_t': ('void', ['pa_context *', 'pa_stat_info *', 'void *']), 'pa_stream_event_cb_t': ('void', ['pa_stream *', 'char *', 'pa_proplist *', 'void *']), 'pa_stream_notify_cb_t': ('void', ['pa_stream *', 'void *']), 'pa_stream_request_cb_t': ('void', ['pa_stream *', 'size_t', 'void *']), 'pa_stream_success_cb_t': ('void', ['pa_stream *', 'int', 'void *']), 'pa_time_event_cb_t': ('void', ['pa_mainloop_api *', 'pa_time_event *', 'struct timeval *', 'void *']), 'pa_time_event_destroy_cb_t': ('void', ['pa_mainloop_api *', 'pa_time_event *', 'void *']), 'quit': ('void', ['pa_mainloop_api *', 'int']), 'time_free': ('void', ['pa_time_event *']), 'time_new': ('pa_time_event *', ['pa_mainloop_api *', 'struct timeval *', 'pa_time_event_cb_t', 'void *']), 'time_restart': ('void', ['pa_time_event *', 'struct timeval *']), 'time_set_destroy': ('void', ['pa_time_event *', 'pa_time_event_destroy_cb_t'])}, 'signatures': {'pa_ascii_filter': ('char *', ['char *']), 'pa_ascii_valid': ('char *', ['char *']), 'pa_bytes_per_second': ('size_t', ['pa_sample_spec *']), 'pa_bytes_snprint': ('char *', ['char *', 'size_t', 'unsigned']), 'pa_bytes_to_usec': ('pa_usec_t', ['uint64_t', 'pa_sample_spec *']), 'pa_channel_map_can_balance': ('int', ['pa_channel_map *']), 'pa_channel_map_can_fade': ('int', ['pa_channel_map *']), 'pa_channel_map_can_lfe_balance': ('int', ['pa_channel_map *']), 'pa_channel_map_compatible': ('int', ['pa_channel_map *', 'pa_sample_spec *']), 'pa_channel_map_equal': ('int', ['pa_channel_map *', 'pa_channel_map *']), 'pa_channel_map_has_position': ('int', ['pa_channel_map *', 'pa_channel_position_t']), 'pa_channel_map_init': ('pa_channel_map *', ['pa_channel_map *']), 'pa_channel_map_init_auto': ('pa_channel_map *', ['pa_channel_map *', 'unsigned', 'pa_channel_map_def_t']), 'pa_channel_map_init_extend': ('pa_channel_map *', ['pa_channel_map *', 'unsigned', 'pa_channel_map_def_t']), 'pa_channel_map_init_mono': ('pa_channel_map *', ['pa_channel_map *']), 'pa_channel_map_init_stereo': ('pa_channel_map *', ['pa_channel_map *']), 'pa_channel_map_mask': ('pa_channel_position_mask_t', ['pa_channel_map *']), 'pa_channel_map_parse': ('pa_channel_map *', ['pa_channel_map *', 'char *']), 'pa_channel_map_snprint': ('char *', ['char *', 'size_t', 'pa_channel_map *']), 'pa_channel_map_superset': ('int', ['pa_channel_map *', 'pa_channel_map *']), 'pa_channel_map_to_name': ('char *', ['pa_channel_map *']), 'pa_channel_map_to_pretty_name': ('char *', ['pa_channel_map *']), 'pa_channel_map_valid': ('int', ['pa_channel_map *']), 'pa_channel_position_from_string': ('pa_channel_position_t', ['char *']), 'pa_channel_position_to_pretty_string': ('char *', ['pa_channel_position_t']), 'pa_channel_position_to_string': ('char *', ['pa_channel_position_t']), 'pa_channels_valid': ('int', ['uint8_t']), 'pa_context_add_autoload': ('pa_operation *', ['pa_context *', 'char *', 'pa_autoload_type_t', 'char *', 'char *', 'pa_context_index_cb_t', 'void *']), 'pa_context_connect': ('int', ['pa_context *', 'char *', 'pa_context_flags_t', 'pa_spawn_api *']), 'pa_context_disconnect': ('void', ['pa_context *']), 'pa_context_drain': ('pa_operation *', ['pa_context *', 'pa_context_notify_cb_t', 'void *']), 'pa_context_errno': ('int', ['pa_context *']), 'pa_context_exit_daemon': ('pa_operation *', ['pa_context *', 'pa_context_success_cb_t', 'void *']), 'pa_context_get_autoload_info_by_index': ('pa_operation *', ['pa_context *', 'uint32_t', 'pa_autoload_info_cb_t', 'void *']), 'pa_context_get_autoload_info_by_name': ('pa_operation *', ['pa_context *', 'char *', 'pa_autoload_type_t', 'pa_autoload_info_cb_t', 'void *']), 'pa_context_get_autoload_info_list': ('pa_operation *', ['pa_context *', 'pa_autoload_info_cb_t', 'void *']), 'pa_context_get_card_info_by_index': ('pa_operation *', ['pa_context *', 'uint32_t', 'pa_card_info_cb_t', 'void *']), 'pa_context_get_card_info_by_name': ('pa_operation *', ['pa_context *', 'char *', 'pa_card_info_cb_t', 'void *']), 'pa_context_get_card_info_list': ('pa_operation *', ['pa_context *', 'pa_card_info_cb_t', 'void *']), 'pa_context_get_client_info': ('pa_operation *', ['pa_context *', 'uint32_t', 'pa_client_info_cb_t', 'void *']), 'pa_context_get_client_info_list': ('pa_operation *', ['pa_context *', 'pa_client_info_cb_t', 'void *']), 'pa_context_get_index': ('uint32_t', ['pa_context *']), 'pa_context_get_module_info': ('pa_operation *', ['pa_context *', 'uint32_t', 'pa_module_info_cb_t', 'void *']), 'pa_context_get_module_info_list': ('pa_operation *', ['pa_context *', 'pa_module_info_cb_t', 'void *']), 'pa_context_get_protocol_version': ('uint32_t', ['pa_context *']), 'pa_context_get_sample_info_by_index': ('pa_operation *', ['pa_context *', 'uint32_t', 'pa_sample_info_cb_t', 'void *']), 'pa_context_get_sample_info_by_name': ('pa_operation *', ['pa_context *', 'char *', 'pa_sample_info_cb_t', 'void *']), 'pa_context_get_sample_info_list': ('pa_operation *', ['pa_context *', 'pa_sample_info_cb_t', 'void *']), 'pa_context_get_server': ('char *', ['pa_context *']), 'pa_context_get_server_info': ('pa_operation *', ['pa_context *', 'pa_server_info_cb_t', 'void *']), 'pa_context_get_server_protocol_version': ('uint32_t', ['pa_context *']), 'pa_context_get_sink_info_by_index': ('pa_operation *', ['pa_context *', 'uint32_t', 'pa_sink_info_cb_t', 'void *']), 'pa_context_get_sink_info_by_name': ('pa_operation *', ['pa_context *', 'char *', 'pa_sink_info_cb_t', 'void *']), 'pa_context_get_sink_info_list': ('pa_operation *', ['pa_context *', 'pa_sink_info_cb_t', 'void *']), 'pa_context_get_sink_input_info': ('pa_operation *', ['pa_context *', 'uint32_t', 'pa_sink_input_info_cb_t', 'void *']), 'pa_context_get_sink_input_info_list': ('pa_operation *', ['pa_context *', 'pa_sink_input_info_cb_t', 'void *']), 'pa_context_get_source_info_by_index': ('pa_operation *', ['pa_context *', 'uint32_t', 'pa_source_info_cb_t', 'void *']), 'pa_context_get_source_info_by_name': ('pa_operation *', ['pa_context *', 'char *', 'pa_source_info_cb_t', 'void *']), 'pa_context_get_source_info_list': ('pa_operation *', ['pa_context *', 'pa_source_info_cb_t', 'void *']), 'pa_context_get_source_output_info': ('pa_operation *', ['pa_context *', 'uint32_t', 'pa_source_output_info_cb_t', 'void *']), 'pa_context_get_source_output_info_list': ('pa_operation *', ['pa_context *', 'pa_source_output_info_cb_t', 'void *']), 'pa_context_get_state': ('pa_context_state_t', ['pa_context *']), 'pa_context_get_tile_size': ('size_t', ['pa_context *', 'pa_sample_spec *']), 'pa_context_is_local': ('int', ['pa_context *']), 'pa_context_is_pending': ('int', ['pa_context *']), 'pa_context_kill_client': ('pa_operation *', ['pa_context *', 'uint32_t', 'pa_context_success_cb_t', 'void *']), 'pa_context_kill_sink_input': ('pa_operation *', ['pa_context *', 'uint32_t', 'pa_context_success_cb_t', 'void *']), 'pa_context_kill_source_output': ('pa_operation *', ['pa_context *', 'uint32_t', 'pa_context_success_cb_t', 'void *']), 'pa_context_load_cookie_from_file': ('int', ['pa_context *', 'char *']), 'pa_context_load_module': ('pa_operation *', ['pa_context *', 'char *', 'char *', 'pa_context_index_cb_t', 'void *']), 'pa_context_move_sink_input_by_index': ('pa_operation *', ['pa_context *', 'uint32_t', 'uint32_t', 'pa_context_success_cb_t', 'void *']), 'pa_context_move_sink_input_by_name': ('pa_operation *', ['pa_context *', 'uint32_t', 'char *', 'pa_context_success_cb_t', 'void *']), 'pa_context_move_source_output_by_index': ('pa_operation *', ['pa_context *', 'uint32_t', 'uint32_t', 'pa_context_success_cb_t', 'void *']), 'pa_context_move_source_output_by_name': ('pa_operation *', ['pa_context *', 'uint32_t', 'char *', 'pa_context_success_cb_t', 'void *']), 'pa_context_new': ('pa_context *', ['pa_mainloop_api *', 'char *']), 'pa_context_new_with_proplist': ('pa_context *', ['pa_mainloop_api *', 'char *', 'pa_proplist *']), 'pa_context_play_sample': ('pa_operation *', ['pa_context *', 'char *', 'char *', 'pa_volume_t', 'pa_context_success_cb_t', 'void *']), 'pa_context_play_sample_with_proplist': ('pa_operation *', ['pa_context *', 'char *', 'char *', 'pa_volume_t', 'pa_proplist *', 'pa_context_play_sample_cb_t', 'void *']), 'pa_context_proplist_remove': ('pa_operation *', ['pa_context *', 'char * *', 'pa_context_success_cb_t', 'void *']), 'pa_context_proplist_update': ('pa_operation *', ['pa_context *', 'pa_update_mode_t', 'pa_proplist *', 'pa_context_success_cb_t', 'void *']), 'pa_context_ref': ('pa_context *', ['pa_context *']), 'pa_context_remove_autoload_by_index': ('pa_operation *', ['pa_context *', 'uint32_t', 'pa_context_success_cb_t', 'void *']), 'pa_context_remove_autoload_by_name': ('pa_operation *', ['pa_context *', 'char *', 'pa_autoload_type_t', 'pa_context_success_cb_t', 'void *']), 'pa_context_remove_sample': ('pa_operation *', ['pa_context *', 'char *', 'pa_context_success_cb_t', 'void *']), 'pa_context_rttime_new': ('pa_time_event *', ['pa_context *', 'pa_usec_t', 'pa_time_event_cb_t', 'void *']), 'pa_context_rttime_restart': ('void', ['pa_context *', 'pa_time_event *', 'pa_usec_t']), 'pa_context_send_message_to_object': ('pa_operation *', ['pa_context *', 'char *', 'char *', 'char *', 'pa_context_string_cb_t', 'void *']), 'pa_context_set_card_profile_by_index': ('pa_operation *', ['pa_context *', 'uint32_t', 'char *', 'pa_context_success_cb_t', 'void *']), 'pa_context_set_card_profile_by_name': ('pa_operation *', ['pa_context *', 'char *', 'char *', 'pa_context_success_cb_t', 'void *']), 'pa_context_set_default_sink': ('pa_operation *', ['pa_context *', 'char *', 'pa_context_success_cb_t', 'void *']), 'pa_context_set_default_source': ('pa_operation *', ['pa_context *', 'char *', 'pa_context_success_cb_t', 'void *']), 'pa_context_set_event_callback': ('void', ['pa_context *', 'pa_context_event_cb_t', 'void *']), 'pa_context_set_name': ('pa_operation *', ['pa_context *', 'char *', 'pa_context_success_cb_t', 'void *']), 'pa_context_set_port_latency_offset': ('pa_operation *', ['pa_context *', 'char *', 'char *', 'int64_t', 'pa_context_success_cb_t', 'void *']), 'pa_context_set_sink_input_mute': ('pa_operation *', ['pa_context *', 'uint32_t', 'int', 'pa_context_success_cb_t', 'void *']), 'pa_context_set_sink_input_volume': ('pa_operation *', ['pa_context *', 'uint32_t', 'pa_cvolume *', 'pa_context_success_cb_t', 'void *']), 'pa_context_set_sink_mute_by_index': ('pa_operation *', ['pa_context *', 'uint32_t', 'int', 'pa_context_success_cb_t', 'void *']), 'pa_context_set_sink_mute_by_name': ('pa_operation *', ['pa_context *', 'char *', 'int', 'pa_context_success_cb_t', 'void *']), 'pa_context_set_sink_port_by_index': ('pa_operation *', ['pa_context *', 'uint32_t', 'char *', 'pa_context_success_cb_t', 'void *']), 'pa_context_set_sink_port_by_name': ('pa_operation *', ['pa_context *', 'char *', 'char *', 'pa_context_success_cb_t', 'void *']), 'pa_context_set_sink_volume_by_index': ('pa_operation *', ['pa_context *', 'uint32_t', 'pa_cvolume *', 'pa_context_success_cb_t', 'void *']), 'pa_context_set_sink_volume_by_name': ('pa_operation *', ['pa_context *', 'char *', 'pa_cvolume *', 'pa_context_success_cb_t', 'void *']), 'pa_context_set_source_mute_by_index': ('pa_operation *', ['pa_context *', 'uint32_t', 'int', 'pa_context_success_cb_t', 'void *']), 'pa_context_set_source_mute_by_name': ('pa_operation *', ['pa_context *', 'char *', 'int', 'pa_context_success_cb_t', 'void *']), 'pa_context_set_source_output_mute': ('pa_operation *', ['pa_context *', 'uint32_t', 'int', 'pa_context_success_cb_t', 'void *']), 'pa_context_set_source_output_volume': ('pa_operation *', ['pa_context *', 'uint32_t', 'pa_cvolume *', 'pa_context_success_cb_t', 'void *']), 'pa_context_set_source_port_by_index': ('pa_operation *', ['pa_context *', 'uint32_t', 'char *', 'pa_context_success_cb_t', 'void *']), 'pa_context_set_source_port_by_name': ('pa_operation *', ['pa_context *', 'char *', 'char *', 'pa_context_success_cb_t', 'void *']), 'pa_context_set_source_volume_by_index': ('pa_operation *', ['pa_context *', 'uint32_t', 'pa_cvolume *', 'pa_context_success_cb_t', 'void *']), 'pa_context_set_source_volume_by_name': ('pa_operation *', ['pa_context *', 'char *', 'pa_cvolume *', 'pa_context_success_cb_t', 'void *']), 'pa_context_set_state_callback': ('void', ['pa_context *', 'pa_context_notify_cb_t', 'void *']), 'pa_context_set_subscribe_callback': ('void', ['pa_context *', 'pa_context_subscribe_cb_t', 'void *']), 'pa_context_stat': ('pa_operation *', ['pa_context *', 'pa_stat_info_cb_t', 'void *']), 'pa_context_subscribe': ('pa_operation *', ['pa_context *', 'pa_subscription_mask_t', 'pa_context_success_cb_t', 'void *']), 'pa_context_suspend_sink_by_index': ('pa_operation *', ['pa_context *', 'uint32_t', 'int', 'pa_context_success_cb_t', 'void *']), 'pa_context_suspend_sink_by_name': ('pa_operation *', ['pa_context *', 'char *', 'int', 'pa_context_success_cb_t', 'void *']), 'pa_context_suspend_source_by_index': ('pa_operation *', ['pa_context *', 'uint32_t', 'int', 'pa_context_success_cb_t', 'void *']), 'pa_context_suspend_source_by_name': ('pa_operation *', ['pa_context *', 'char *', 'int', 'pa_context_success_cb_t', 'void *']), 'pa_context_unload_module': ('pa_operation *', ['pa_context *', 'uint32_t', 'pa_context_success_cb_t', 'void *']), 'pa_context_unref': ('void', ['pa_context *']), 'pa_cvolume_avg': ('pa_volume_t', ['pa_cvolume *']), 'pa_cvolume_avg_mask': ('pa_volume_t', ['pa_cvolume *', 'pa_channel_map *', 'pa_channel_position_mask_t']), 'pa_cvolume_channels_equal_to': ('int', ['pa_cvolume *', 'pa_volume_t']), 'pa_cvolume_compatible': ('int', ['pa_cvolume *', 'pa_sample_spec *']), 'pa_cvolume_compatible_with_channel_map': ('int', ['pa_cvolume *', 'pa_channel_map *']), 'pa_cvolume_dec': ('pa_cvolume *', ['pa_cvolume *', 'pa_volume_t']), 'pa_cvolume_equal': ('int', ['pa_cvolume *', 'pa_cvolume *']), 'pa_cvolume_get_balance': ('float', ['pa_cvolume *', 'pa_channel_map *']), 'pa_cvolume_get_fade': ('float', ['pa_cvolume *', 'pa_channel_map *']), 'pa_cvolume_get_lfe_balance': ('float', ['pa_cvolume *', 'pa_channel_map *']), 'pa_cvolume_get_position': ('pa_volume_t', ['pa_cvolume *', 'pa_channel_map *', 'pa_channel_position_t']), 'pa_cvolume_inc': ('pa_cvolume *', ['pa_cvolume *', 'pa_volume_t']), 'pa_cvolume_inc_clamp': ('pa_cvolume *', ['pa_cvolume *', 'pa_volume_t', 'pa_volume_t']), 'pa_cvolume_init': ('pa_cvolume *', ['pa_cvolume *']), 'pa_cvolume_max': ('pa_volume_t', ['pa_cvolume *']), 'pa_cvolume_max_mask': ('pa_volume_t', ['pa_cvolume *', 'pa_channel_map *', 'pa_channel_position_mask_t']), 'pa_cvolume_merge': ('pa_cvolume *', ['pa_cvolume *', 'pa_cvolume *', 'pa_cvolume *']), 'pa_cvolume_min': ('pa_volume_t', ['pa_cvolume *']), 'pa_cvolume_min_mask': ('pa_volume_t', ['pa_cvolume *', 'pa_channel_map *', 'pa_channel_position_mask_t']), 'pa_cvolume_remap': ('pa_cvolume *', ['pa_cvolume *', 'pa_channel_map *', 'pa_channel_map *']), 'pa_cvolume_scale': ('pa_cvolume *', ['pa_cvolume *', 'pa_volume_t']), 'pa_cvolume_scale_mask': ('pa_cvolume *', ['pa_cvolume *', 'pa_volume_t', 'pa_channel_map *', 'pa_channel_position_mask_t']), 'pa_cvolume_set': ('pa_cvolume *', ['pa_cvolume *', 'unsigned', 'pa_volume_t']), 'pa_cvolume_set_balance': ('pa_cvolume *', ['pa_cvolume *', 'pa_channel_map *', 'float']), 'pa_cvolume_set_fade': ('pa_cvolume *', ['pa_cvolume *', 'pa_channel_map *', 'float']), 'pa_cvolume_set_lfe_balance': ('pa_cvolume *', ['pa_cvolume *', 'pa_channel_map *', 'float']), 'pa_cvolume_set_position': ('pa_cvolume *', ['pa_cvolume *', 'pa_channel_map *', 'pa_channel_position_t', 'pa_volume_t']), 'pa_cvolume_snprint': ('char *', ['char *', 'size_t', 'pa_cvolume *']), 'pa_cvolume_snprint_verbose': ('char *', ['char *', 'size_t', 'pa_cvolume *', 'pa_channel_map *', 'int']), 'pa_cvolume_valid': ('int', ['pa_cvolume *']), 'pa_direction_to_string': ('char *', ['pa_direction_t']), 'pa_direction_valid': ('int', ['pa_direction_t']), 'pa_encoding_from_string': ('pa_encoding_t', ['char *']), 'pa_encoding_to_string': ('char *', ['pa_encoding_t']), 'pa_ext_device_restore_read_formats': ('pa_operation *', ['pa_context *', 'pa_device_type_t', 'uint32_t', 'pa_ext_device_restore_read_device_formats_cb_t', 'void *']), 'pa_ext_device_restore_read_formats_all': ('pa_operation *', ['pa_context *', 'pa_ext_device_restore_read_device_formats_cb_t', 'void *']), 'pa_ext_device_restore_save_formats': ('pa_operation *', ['pa_context *', 'pa_device_type_t', 'uint32_t', 'uint8_t', 'pa_format_info * *', 'pa_context_success_cb_t', 'void *']), 'pa_ext_device_restore_set_subscribe_cb': ('void', ['pa_context *', 'pa_ext_device_restore_subscribe_cb_t', 'void *']), 'pa_ext_device_restore_subscribe': ('pa_operation *', ['pa_context *', 'int', 'pa_context_success_cb_t', 'void *']), 'pa_ext_device_restore_test': ('pa_operation *', ['pa_context *', 'pa_ext_device_restore_test_cb_t', 'void *']), 'pa_format_info_copy': ('pa_format_info *', ['pa_format_info *']), 'pa_format_info_free': ('void', ['pa_format_info *']), 'pa_format_info_free_string_array': ('void', ['char * *', 'int']), 'pa_format_info_from_sample_spec': ('pa_format_info *', ['pa_sample_spec *', 'pa_channel_map *']), 'pa_format_info_from_string': ('pa_format_info *', ['char *']), 'pa_format_info_get_channel_map': ('int', ['pa_format_info *', 'pa_channel_map *']), 'pa_format_info_get_channels': ('int', ['pa_format_info *', 'uint8_t *']), 'pa_format_info_get_prop_int': ('int', ['pa_format_info *', 'char *', 'int *']), 'pa_format_info_get_prop_int_array': ('int', ['pa_format_info *', 'char *', 'int * *', 'int *']), 'pa_format_info_get_prop_int_range': ('int', ['pa_format_info *', 'char *', 'int *', 'int *']), 'pa_format_info_get_prop_string': ('int', ['pa_format_info *', 'char *', 'char * *']), 'pa_format_info_get_prop_string_array': ('int', ['pa_format_info *', 'char *', 'char * * *', 'int *']), 'pa_format_info_get_prop_type': ('pa_prop_type_t', ['pa_format_info *', 'char *']), 'pa_format_info_get_rate': ('int', ['pa_format_info *', 'uint32_t *']), 'pa_format_info_get_sample_format': ('int', ['pa_format_info *', 'pa_sample_format_t *']), 'pa_format_info_is_compatible': ('int', ['pa_format_info *', 'pa_format_info *']), 'pa_format_info_is_pcm': ('int', ['pa_format_info *']), 'pa_format_info_new': ('pa_format_info *', ['void']), 'pa_format_info_set_channel_map': ('void', ['pa_format_info *', 'pa_channel_map *']), 'pa_format_info_set_channels': ('void', ['pa_format_info *', 'int']), 'pa_format_info_set_prop_int': ('void', ['pa_format_info *', 'char *', 'int']), 'pa_format_info_set_prop_int_array': ('void', ['pa_format_info *', 'char *', 'int *', 'int']), 'pa_format_info_set_prop_int_range': ('void', ['pa_format_info *', 'char *', 'int', 'int']), 'pa_format_info_set_prop_string': ('void', ['pa_format_info *', 'char *', 'char *']), 'pa_format_info_set_prop_string_array': ('void', ['pa_format_info *', 'char *', 'char * *', 'int']), 'pa_format_info_set_rate': ('void', ['pa_format_info *', 'int']), 'pa_format_info_set_sample_format': ('void', ['pa_format_info *', 'pa_sample_format_t']), 'pa_format_info_snprint': ('char *', ['char *', 'size_t', 'pa_format_info *']), 'pa_format_info_to_sample_spec': ('int', ['pa_format_info *', 'pa_sample_spec *', 'pa_channel_map *']), 'pa_format_info_valid': ('int', ['pa_format_info *']), 'pa_frame_size': ('size_t', ['pa_sample_spec *']), 'pa_get_binary_name': ('char *', ['char *', 'size_t']), 'pa_get_fqdn': ('char *', ['char *', 'size_t']), 'pa_get_home_dir': ('char *', ['char *', 'size_t']), 'pa_get_host_name': ('char *', ['char *', 'size_t']), 'pa_get_library_version': ('char *', ['void']), 'pa_get_user_name': ('char *', ['char *', 'size_t']), 'pa_gettimeofday': ('struct timeval *', ['struct timeval *']), 'pa_locale_to_utf8': ('char *', ['char *']), 'pa_mainloop_api_once': ('void', ['pa_mainloop_api *', ('void', ['pa_mainloop_api *', 'void *']), 'void *']), 'pa_mainloop_dispatch': ('int', ['pa_mainloop *']), 'pa_mainloop_free': ('void', ['pa_mainloop *']), 'pa_mainloop_get_api': ('pa_mainloop_api *', ['pa_mainloop *']), 'pa_mainloop_get_retval': ('int', ['pa_mainloop *']), 'pa_mainloop_iterate': ('int', ['pa_mainloop *', 'int', 'int *']), 'pa_mainloop_new': ('pa_mainloop *', ['void']), 'pa_mainloop_poll': ('int', ['pa_mainloop *']), 'pa_mainloop_prepare': ('int', ['pa_mainloop *', 'int']), 'pa_mainloop_quit': ('void', ['pa_mainloop *', 'int']), 'pa_mainloop_run': ('int', ['pa_mainloop *', 'int *']), 'pa_mainloop_set_poll_func': ('void', ['pa_mainloop *', 'pa_poll_func', 'void *']), 'pa_mainloop_wakeup': ('void', ['pa_mainloop *']), 'pa_msleep': ('int', ['unsigned long']), 'pa_operation_cancel': ('void', ['pa_operation *']), 'pa_operation_get_state': ('pa_operation_state_t', ['pa_operation *']), 'pa_operation_ref': ('pa_operation *', ['pa_operation *']), 'pa_operation_set_state_callback': ('void', ['pa_operation *', 'pa_operation_notify_cb_t', 'void *']), 'pa_operation_unref': ('void', ['pa_operation *']), 'pa_parse_sample_format': ('pa_sample_format_t', ['char *']), 'pa_path_get_filename': ('char *', ['char *']), 'pa_proplist_clear': ('void', ['pa_proplist *']), 'pa_proplist_contains': ('int', ['pa_proplist *', 'char *']), 'pa_proplist_copy': ('pa_proplist *', ['pa_proplist *']), 'pa_proplist_equal': ('int', ['pa_proplist *', 'pa_proplist *']), 'pa_proplist_free': ('void', ['pa_proplist *']), 'pa_proplist_from_string': ('pa_proplist *', ['char *']), 'pa_proplist_get': ('int', ['pa_proplist *', 'char *', 'void * *', 'size_t *']), 'pa_proplist_gets': ('char *', ['pa_proplist *', 'char *']), 'pa_proplist_isempty': ('int', ['pa_proplist *']), 'pa_proplist_iterate': ('char *', ['pa_proplist *', 'void * *']), 'pa_proplist_key_valid': ('int', ['char *']), 'pa_proplist_new': ('pa_proplist *', ['void']), 'pa_proplist_set': ('int', ['pa_proplist *', 'char *', 'void *', 'size_t']), 'pa_proplist_setp': ('int', ['pa_proplist *', 'char *']), 'pa_proplist_sets': ('int', ['pa_proplist *', 'char *', 'char *']), 'pa_proplist_size': ('unsigned', ['pa_proplist *']), 'pa_proplist_to_string': ('char *', ['pa_proplist *']), 'pa_proplist_to_string_sep': ('char *', ['pa_proplist *', 'char *']), 'pa_proplist_unset': ('int', ['pa_proplist *', 'char *']), 'pa_proplist_unset_many': ('int', ['pa_proplist *', 'char * *']), 'pa_proplist_update': ('void', ['pa_proplist *', 'pa_update_mode_t', 'pa_proplist *']), 'pa_rtclock_now': ('pa_usec_t', ['void']), 'pa_sample_format_is_be': ('int', ['pa_sample_format_t']), 'pa_sample_format_is_le': ('int', ['pa_sample_format_t']), 'pa_sample_format_to_string': ('char *', ['pa_sample_format_t']), 'pa_sample_format_valid': ('int', ['unsigned']), 'pa_sample_rate_valid': ('int', ['uint32_t']), 'pa_sample_size': ('size_t', ['pa_sample_spec *']), 'pa_sample_size_of_format': ('size_t', ['pa_sample_format_t']), 'pa_sample_spec_equal': ('int', ['pa_sample_spec *', 'pa_sample_spec *']), 'pa_sample_spec_init': ('pa_sample_spec *', ['pa_sample_spec *']), 'pa_sample_spec_snprint': ('char *', ['char *', 'size_t', 'pa_sample_spec *']), 'pa_sample_spec_valid': ('int', ['pa_sample_spec *']), 'pa_signal_done': ('void', ['void']), 'pa_signal_free': ('void', ['pa_signal_event *']), 'pa_signal_init': ('int', ['pa_mainloop_api *']), 'pa_signal_new': ('pa_signal_event *', ['int', 'pa_signal_cb_t', 'void *']), 'pa_signal_set_destroy': ('void', ['pa_signal_event *', 'pa_signal_destroy_cb_t']), 'pa_stream_begin_write': ('int', ['pa_stream *', 'void * *', 'size_t *']), 'pa_stream_cancel_write': ('int', ['pa_stream *']), 'pa_stream_connect_playback': ('int', ['pa_stream *', 'char *', 'pa_buffer_attr *', 'pa_stream_flags_t', 'pa_cvolume *', 'pa_stream *']), 'pa_stream_connect_record': ('int', ['pa_stream *', 'char *', 'pa_buffer_attr *', 'pa_stream_flags_t']), 'pa_stream_connect_upload': ('int', ['pa_stream *', 'size_t']), 'pa_stream_cork': ('pa_operation *', ['pa_stream *', 'int', 'pa_stream_success_cb_t', 'void *']), 'pa_stream_disconnect': ('int', ['pa_stream *']), 'pa_stream_drain': ('pa_operation *', ['pa_stream *', 'pa_stream_success_cb_t', 'void *']), 'pa_stream_drop': ('int', ['pa_stream *']), 'pa_stream_finish_upload': ('int', ['pa_stream *']), 'pa_stream_flush': ('pa_operation *', ['pa_stream *', 'pa_stream_success_cb_t', 'void *']), 'pa_stream_get_buffer_attr': ('pa_buffer_attr *', ['pa_stream *']), 'pa_stream_get_channel_map': ('pa_channel_map *', ['pa_stream *']), 'pa_stream_get_context': ('pa_context *', ['pa_stream *']), 'pa_stream_get_device_index': ('uint32_t', ['pa_stream *']), 'pa_stream_get_device_name': ('char *', ['pa_stream *']), 'pa_stream_get_format_info': ('pa_format_info *', ['pa_stream *']), 'pa_stream_get_index': ('uint32_t', ['pa_stream *']), 'pa_stream_get_latency': ('int', ['pa_stream *', 'pa_usec_t *', 'int *']), 'pa_stream_get_monitor_stream': ('uint32_t', ['pa_stream *']), 'pa_stream_get_sample_spec': ('pa_sample_spec *', ['pa_stream *']), 'pa_stream_get_state': ('pa_stream_state_t', ['pa_stream *']), 'pa_stream_get_time': ('int', ['pa_stream *', 'pa_usec_t *']), 'pa_stream_get_timing_info': ('pa_timing_info *', ['pa_stream *']), 'pa_stream_get_underflow_index': ('int64_t', ['pa_stream *']), 'pa_stream_is_corked': ('int', ['pa_stream *']), 'pa_stream_is_suspended': ('int', ['pa_stream *']), 'pa_stream_new': ('pa_stream *', ['pa_context *', 'char *', 'pa_sample_spec *', 'pa_channel_map *']), 'pa_stream_new_extended': ('pa_stream *', ['pa_context *', 'char *', 'pa_format_info * *', 'unsigned int', 'pa_proplist *']), 'pa_stream_new_with_proplist': ('pa_stream *', ['pa_context *', 'char *', 'pa_sample_spec *', 'pa_channel_map *', 'pa_proplist *']), 'pa_stream_peek': ('int', ['pa_stream *', 'void * *', 'size_t *']), 'pa_stream_prebuf': ('pa_operation *', ['pa_stream *', 'pa_stream_success_cb_t', 'void *']), 'pa_stream_proplist_remove': ('pa_operation *', ['pa_stream *', 'char * *', 'pa_stream_success_cb_t', 'void *']), 'pa_stream_proplist_update': ('pa_operation *', ['pa_stream *', 'pa_update_mode_t', 'pa_proplist *', 'pa_stream_success_cb_t', 'void *']), 'pa_stream_readable_size': ('size_t', ['pa_stream *']), 'pa_stream_ref': ('pa_stream *', ['pa_stream *']), 'pa_stream_set_buffer_attr': ('pa_operation *', ['pa_stream *', 'pa_buffer_attr *', 'pa_stream_success_cb_t', 'void *']), 'pa_stream_set_buffer_attr_callback': ('void', ['pa_stream *', 'pa_stream_notify_cb_t', 'void *']), 'pa_stream_set_event_callback': ('void', ['pa_stream *', 'pa_stream_event_cb_t', 'void *']), 'pa_stream_set_latency_update_callback': ('void', ['pa_stream *', 'pa_stream_notify_cb_t', 'void *']), 'pa_stream_set_monitor_stream': ('int', ['pa_stream *', 'uint32_t']), 'pa_stream_set_moved_callback': ('void', ['pa_stream *', 'pa_stream_notify_cb_t', 'void *']), 'pa_stream_set_name': ('pa_operation *', ['pa_stream *', 'char *', 'pa_stream_success_cb_t', 'void *']), 'pa_stream_set_overflow_callback': ('void', ['pa_stream *', 'pa_stream_notify_cb_t', 'void *']), 'pa_stream_set_read_callback': ('void', ['pa_stream *', 'pa_stream_request_cb_t', 'void *']), 'pa_stream_set_started_callback': ('void', ['pa_stream *', 'pa_stream_notify_cb_t', 'void *']), 'pa_stream_set_state_callback': ('void', ['pa_stream *', 'pa_stream_notify_cb_t', 'void *']), 'pa_stream_set_suspended_callback': ('void', ['pa_stream *', 'pa_stream_notify_cb_t', 'void *']), 'pa_stream_set_underflow_callback': ('void', ['pa_stream *', 'pa_stream_notify_cb_t', 'void *']), 'pa_stream_set_write_callback': ('void', ['pa_stream *', 'pa_stream_request_cb_t', 'void *']), 'pa_stream_trigger': ('pa_operation *', ['pa_stream *', 'pa_stream_success_cb_t', 'void *']), 'pa_stream_unref': ('void', ['pa_stream *']), 'pa_stream_update_sample_rate': ('pa_operation *', ['pa_stream *', 'uint32_t', 'pa_stream_success_cb_t', 'void *']), 'pa_stream_update_timing_info': ('pa_operation *', ['pa_stream *', 'pa_stream_success_cb_t', 'void *']), 'pa_stream_writable_size': ('size_t', ['pa_stream *']), 'pa_stream_write': ('int', ['pa_stream *', 'void *', 'size_t', 'pa_free_cb_t', 'int64_t', 'pa_seek_mode_t']), 'pa_stream_write_ext_free': ('int', ['pa_stream *', 'void *', 'size_t', 'pa_free_cb_t', 'void *', 'int64_t', 'pa_seek_mode_t']), 'pa_strerror': ('char *', ['int']), 'pa_sw_cvolume_divide': ('pa_cvolume *', ['pa_cvolume *', 'pa_cvolume *', 'pa_cvolume *']), 'pa_sw_cvolume_divide_scalar': ('pa_cvolume *', ['pa_cvolume *', 'pa_cvolume *', 'pa_volume_t']), 'pa_sw_cvolume_multiply': ('pa_cvolume *', ['pa_cvolume *', 'pa_cvolume *', 'pa_cvolume *']), 'pa_sw_cvolume_multiply_scalar': ('pa_cvolume *', ['pa_cvolume *', 'pa_cvolume *', 'pa_volume_t']), 'pa_sw_cvolume_snprint_dB': ('char *', ['char *', 'size_t', 'pa_cvolume *']), 'pa_sw_volume_divide': ('pa_volume_t', ['pa_volume_t', 'pa_volume_t']), 'pa_sw_volume_from_dB': ('pa_volume_t', ['double']), 'pa_sw_volume_from_linear': ('pa_volume_t', ['double']), 'pa_sw_volume_multiply': ('pa_volume_t', ['pa_volume_t', 'pa_volume_t']), 'pa_sw_volume_snprint_dB': ('char *', ['char *', 'size_t', 'pa_volume_t']), 'pa_sw_volume_to_dB': ('double', ['pa_volume_t']), 'pa_sw_volume_to_linear': ('double', ['pa_volume_t']), 'pa_thread_make_realtime': ('int', ['int']), 'pa_threaded_mainloop_accept': ('void', ['pa_threaded_mainloop *']), 'pa_threaded_mainloop_free': ('void', ['pa_threaded_mainloop *']), 'pa_threaded_mainloop_get_api': ('pa_mainloop_api *', ['pa_threaded_mainloop *']), 'pa_threaded_mainloop_get_retval': ('int', ['pa_threaded_mainloop *']), 'pa_threaded_mainloop_in_thread': ('int', ['pa_threaded_mainloop *']), 'pa_threaded_mainloop_lock': ('void', ['pa_threaded_mainloop *']), 'pa_threaded_mainloop_new': ('pa_threaded_mainloop *', ['void']), 'pa_threaded_mainloop_once_unlocked': ('void', ['pa_threaded_mainloop *', ('void', ['pa_threaded_mainloop *', 'void *']), 'void *']), 'pa_threaded_mainloop_set_name': ('void', ['pa_threaded_mainloop *', 'char *']), 'pa_threaded_mainloop_signal': ('void', ['pa_threaded_mainloop *', 'int']), 'pa_threaded_mainloop_start': ('int', ['pa_threaded_mainloop *']), 'pa_threaded_mainloop_stop': ('void', ['pa_threaded_mainloop *']), 'pa_threaded_mainloop_unlock': ('void', ['pa_threaded_mainloop *']), 'pa_threaded_mainloop_wait': ('void', ['pa_threaded_mainloop *']), 'pa_timeval_add': ('struct timeval *', ['struct timeval *', 'pa_usec_t']), 'pa_timeval_age': ('pa_usec_t', ['struct timeval *']), 'pa_timeval_cmp': ('int', ['struct timeval *', 'struct timeval *']), 'pa_timeval_diff': ('pa_usec_t', ['struct timeval *', 'struct timeval *']), 'pa_timeval_load': ('pa_usec_t', ['struct timeval *']), 'pa_timeval_store': ('struct timeval *', ['struct timeval *', 'pa_usec_t']), 'pa_timeval_sub': ('struct timeval *', ['struct timeval *', 'pa_usec_t']), 'pa_usec_to_bytes': ('size_t', ['pa_usec_t', 'pa_sample_spec *']), 'pa_utf8_filter': ('char *', ['char *']), 'pa_utf8_to_locale': ('char *', ['char *']), 'pa_utf8_valid': ('char *', ['char *']), 'pa_volume_snprint': ('char *', ['char *', 'size_t', 'pa_volume_t']), 'pa_volume_snprint_verbose': ('char *', ['char *', 'size_t', 'pa_volume_t', 'int'])}} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1721565556.7669091 libpulse-0.7/libpulse/pulse_structs.py0000644000000000000000000002416314647200565015254 0ustar00"""File pulse_structs. This file has been generated by libpulse_parser.py - DO NOT MODIFY. """ pulse_structs = \ {'pa_autoload_info': (('index', 'uint32_t'), ('name', 'char *'), ('type', 'pa_autoload_type_t'), ('module', 'char *'), ('argument', 'char *')), 'pa_buffer_attr': (('maxlength', 'uint32_t'), ('tlength', 'uint32_t'), ('prebuf', 'uint32_t'), ('minreq', 'uint32_t'), ('fragsize', 'uint32_t')), 'pa_card_info': (('index', 'uint32_t'), ('name', 'char *'), ('owner_module', 'uint32_t'), ('driver', 'char *'), ('n_profiles', 'uint32_t'), ('profiles', 'pa_card_profile_info *'), ('active_profile', 'pa_card_profile_info *'), ('proplist', 'pa_proplist *'), ('n_ports', 'uint32_t'), ('ports', 'pa_card_port_info * *'), ('profiles2', 'pa_card_profile_info2 * *'), ('active_profile2', 'pa_card_profile_info2 *')), 'pa_card_port_info': (('name', 'char *'), ('description', 'char *'), ('priority', 'uint32_t'), ('available', 'int'), ('direction', 'int'), ('n_profiles', 'uint32_t'), ('profiles', 'pa_card_profile_info * *'), ('proplist', 'pa_proplist *'), ('latency_offset', 'int64_t'), ('profiles2', 'pa_card_profile_info2 * *'), ('availability_group', 'char *'), ('type', 'uint32_t')), 'pa_card_profile_info': (('name', 'char *'), ('description', 'char *'), ('n_sinks', 'uint32_t'), ('n_sources', 'uint32_t'), ('priority', 'uint32_t')), 'pa_card_profile_info2': (('name', 'char *'), ('description', 'char *'), ('n_sinks', 'uint32_t'), ('n_sources', 'uint32_t'), ('priority', 'uint32_t'), ('available', 'int')), 'pa_channel_map': (('channels', 'uint8_t'), ('map', 'pa_channel_position_t * 32')), 'pa_client_info': (('index', 'uint32_t'), ('name', 'char *'), ('owner_module', 'uint32_t'), ('driver', 'char *'), ('proplist', 'pa_proplist *')), 'pa_context': (), 'pa_cvolume': (('channels', 'uint8_t'), ('values', 'pa_volume_t * 32')), 'pa_defer_event': (), 'pa_ext_device_restore_info': (('type', 'pa_device_type_t'), ('index', 'uint32_t'), ('n_formats', 'uint8_t'), ('formats', 'pa_format_info * *')), 'pa_format_info': (('encoding', 'pa_encoding_t'), ('plist', 'pa_proplist *')), 'pa_io_event': (), 'pa_mainloop': (), 'pa_module_info': (('index', 'uint32_t'), ('name', 'char *'), ('argument', 'char *'), ('n_used', 'uint32_t'), ('auto_unload', 'int'), ('proplist', 'pa_proplist *')), 'pa_operation': (), 'pa_proplist': (), 'pa_sample_info': (('index', 'uint32_t'), ('name', 'char *'), ('volume', 'pa_cvolume'), ('sample_spec', 'pa_sample_spec'), ('channel_map', 'pa_channel_map'), ('duration', 'pa_usec_t'), ('bytes', 'uint32_t'), ('lazy', 'int'), ('filename', 'char *'), ('proplist', 'pa_proplist *')), 'pa_sample_spec': (('format', 'pa_sample_format_t'), ('rate', 'uint32_t'), ('channels', 'uint8_t')), 'pa_server_info': (('user_name', 'char *'), ('host_name', 'char *'), ('server_version', 'char *'), ('server_name', 'char *'), ('sample_spec', 'pa_sample_spec'), ('default_sink_name', 'char *'), ('default_source_name', 'char *'), ('cookie', 'uint32_t'), ('channel_map', 'pa_channel_map')), 'pa_signal_event': (), 'pa_sink_info': (('name', 'char *'), ('index', 'uint32_t'), ('description', 'char *'), ('sample_spec', 'pa_sample_spec'), ('channel_map', 'pa_channel_map'), ('owner_module', 'uint32_t'), ('volume', 'pa_cvolume'), ('mute', 'int'), ('monitor_source', 'uint32_t'), ('monitor_source_name', 'char *'), ('latency', 'pa_usec_t'), ('driver', 'char *'), ('flags', 'pa_sink_flags_t'), ('proplist', 'pa_proplist *'), ('configured_latency', 'pa_usec_t'), ('base_volume', 'pa_volume_t'), ('state', 'pa_sink_state_t'), ('n_volume_steps', 'uint32_t'), ('card', 'uint32_t'), ('n_ports', 'uint32_t'), ('ports', 'pa_sink_port_info * *'), ('active_port', 'pa_sink_port_info *'), ('n_formats', 'uint8_t'), ('formats', 'pa_format_info * *')), 'pa_sink_input_info': (('index', 'uint32_t'), ('name', 'char *'), ('owner_module', 'uint32_t'), ('client', 'uint32_t'), ('sink', 'uint32_t'), ('sample_spec', 'pa_sample_spec'), ('channel_map', 'pa_channel_map'), ('volume', 'pa_cvolume'), ('buffer_usec', 'pa_usec_t'), ('sink_usec', 'pa_usec_t'), ('resample_method', 'char *'), ('driver', 'char *'), ('mute', 'int'), ('proplist', 'pa_proplist *'), ('corked', 'int'), ('has_volume', 'int'), ('volume_writable', 'int'), ('format', 'pa_format_info *')), 'pa_sink_port_info': (('name', 'char *'), ('description', 'char *'), ('priority', 'uint32_t'), ('available', 'int'), ('availability_group', 'char *'), ('type', 'uint32_t')), 'pa_source_info': (('name', 'char *'), ('index', 'uint32_t'), ('description', 'char *'), ('sample_spec', 'pa_sample_spec'), ('channel_map', 'pa_channel_map'), ('owner_module', 'uint32_t'), ('volume', 'pa_cvolume'), ('mute', 'int'), ('monitor_of_sink', 'uint32_t'), ('monitor_of_sink_name', 'char *'), ('latency', 'pa_usec_t'), ('driver', 'char *'), ('flags', 'pa_source_flags_t'), ('proplist', 'pa_proplist *'), ('configured_latency', 'pa_usec_t'), ('base_volume', 'pa_volume_t'), ('state', 'pa_source_state_t'), ('n_volume_steps', 'uint32_t'), ('card', 'uint32_t'), ('n_ports', 'uint32_t'), ('ports', 'pa_source_port_info * *'), ('active_port', 'pa_source_port_info *'), ('n_formats', 'uint8_t'), ('formats', 'pa_format_info * *')), 'pa_source_output_info': (('index', 'uint32_t'), ('name', 'char *'), ('owner_module', 'uint32_t'), ('client', 'uint32_t'), ('source', 'uint32_t'), ('sample_spec', 'pa_sample_spec'), ('channel_map', 'pa_channel_map'), ('buffer_usec', 'pa_usec_t'), ('source_usec', 'pa_usec_t'), ('resample_method', 'char *'), ('driver', 'char *'), ('proplist', 'pa_proplist *'), ('corked', 'int'), ('volume', 'pa_cvolume'), ('mute', 'int'), ('has_volume', 'int'), ('volume_writable', 'int'), ('format', 'pa_format_info *')), 'pa_source_port_info': (('name', 'char *'), ('description', 'char *'), ('priority', 'uint32_t'), ('available', 'int'), ('availability_group', 'char *'), ('type', 'uint32_t')), 'pa_stat_info': (('memblock_total', 'uint32_t'), ('memblock_total_size', 'uint32_t'), ('memblock_allocated', 'uint32_t'), ('memblock_allocated_size', 'uint32_t'), ('scache_size', 'uint32_t')), 'pa_stream': (), 'pa_threaded_mainloop': (), 'pa_time_event': (), 'pa_timing_info': (('timestamp', 'struct timeval'), ('synchronized_clocks', 'int'), ('sink_usec', 'pa_usec_t'), ('source_usec', 'pa_usec_t'), ('transport_usec', 'pa_usec_t'), ('playing', 'int'), ('write_index_corrupt', 'int'), ('write_index', 'int64_t'), ('read_index_corrupt', 'int'), ('read_index', 'int64_t'), ('configured_sink_usec', 'pa_usec_t'), ('configured_source_usec', 'pa_usec_t'), ('since_underrun', 'int64_t'))} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1721565556.7669091 libpulse-0.7/libpulse/pulse_types.py0000644000000000000000000000573514647200565014715 0ustar00"""File pulse_types. This file has been generated by libpulse_parser.py - DO NOT MODIFY. """ pulse_types = \ {'pa_autoload_info': 'struct pa_autoload_info', 'pa_autoload_type_t': 'enum pa_autoload_type', 'pa_buffer_attr': 'struct pa_buffer_attr', 'pa_card_info': 'struct pa_card_info', 'pa_card_port_info': 'struct pa_card_port_info', 'pa_card_profile_info': 'struct pa_card_profile_info', 'pa_card_profile_info2': 'struct pa_card_profile_info2', 'pa_channel_map': 'struct pa_channel_map', 'pa_channel_map_def_t': 'enum pa_channel_map_def', 'pa_channel_position_mask_t': 'uint64_t', 'pa_channel_position_t': 'enum pa_channel_position', 'pa_client_info': 'struct pa_client_info', 'pa_context': 'struct pa_context', 'pa_context_flags_t': 'enum pa_context_flags', 'pa_context_state_t': 'enum pa_context_state', 'pa_cvolume': 'struct pa_cvolume', 'pa_defer_event': 'struct pa_defer_event', 'pa_device_port_type_t': 'enum pa_device_port_type', 'pa_device_type_t': 'enum pa_device_type', 'pa_direction_t': 'enum pa_direction', 'pa_encoding_t': 'enum pa_encoding', 'pa_error_code_t': 'enum pa_error_code', 'pa_ext_device_restore_info': 'struct pa_ext_device_restore_info', 'pa_format_info': 'struct pa_format_info', 'pa_io_event': 'struct pa_io_event', 'pa_io_event_flags_t': 'enum pa_io_event_flags', 'pa_mainloop': 'struct pa_mainloop', 'pa_mainloop_api': 'struct pa_mainloop_api', 'pa_module_info': 'struct pa_module_info', 'pa_operation': 'struct pa_operation', 'pa_operation_state_t': 'enum pa_operation_state', 'pa_port_available_t': 'enum pa_port_available', 'pa_prop_type_t': 'enum pa_prop_type_t', 'pa_proplist': 'struct pa_proplist', 'pa_sample_format_t': 'enum pa_sample_format', 'pa_sample_info': 'struct pa_sample_info', 'pa_sample_spec': 'struct pa_sample_spec', 'pa_seek_mode_t': 'enum pa_seek_mode', 'pa_server_info': 'struct pa_server_info', 'pa_signal_event': 'struct pa_signal_event', 'pa_sink_flags_t': 'enum pa_sink_flags', 'pa_sink_info': 'struct pa_sink_info', 'pa_sink_input_info': 'struct pa_sink_input_info', 'pa_sink_port_info': 'struct pa_sink_port_info', 'pa_sink_state_t': 'enum pa_sink_state', 'pa_source_flags_t': 'enum pa_source_flags', 'pa_source_info': 'struct pa_source_info', 'pa_source_output_info': 'struct pa_source_output_info', 'pa_source_port_info': 'struct pa_source_port_info', 'pa_source_state_t': 'enum pa_source_state', 'pa_spawn_api': 'struct pa_spawn_api', 'pa_stat_info': 'struct pa_stat_info', 'pa_stream': 'struct pa_stream', 'pa_stream_direction_t': 'enum pa_stream_direction', 'pa_stream_flags_t': 'enum pa_stream_flags', 'pa_stream_state_t': 'enum pa_stream_state', 'pa_subscription_event_type_t': 'enum pa_subscription_event_type', 'pa_subscription_mask_t': 'enum pa_subscription_mask', 'pa_threaded_mainloop': 'struct pa_threaded_mainloop', 'pa_time_event': 'struct pa_time_event', 'pa_timing_info': 'struct pa_timing_info', 'pa_update_mode_t': 'enum pa_update_mode', 'pa_usec_t': 'uint64_t', 'pa_volume_t': 'uint32_t'} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1721565556.7669091 libpulse-0.7/libpulse/tests/__init__.py0000644000000000000000000000564614647200565015243 0ustar00import os import sys import subprocess import unittest import functools import re if sys.version_info >= (3, 9): functools_cache = functools.cache else: functools_cache = functools.lru_cache @functools_cache def is_pipewire(): # Check that pulseaudio or pipewire-pulse is running. proc = subprocess.run(['pactl', 'list', 'message-handlers'], capture_output=True, text=True, check=True) match = re.search('[Pp]ipe[Ww]ire', proc.stdout) return bool(match) def _id(obj): return obj @functools_cache def requires_resources(resources): """Skip the test when one of the resource is not available. 'resources' is a string or a tuple instance (MUST be hashable). """ resources = [resources] if isinstance(resources, str) else resources for res in resources: try: if res == 'os.devnull': # Check that os.devnull is writable. with open(os.devnull, 'w'): pass elif res == 'libpulse': is_pipewire() elif res == 'pulseaudio': if is_pipewire(): raise Exception else: # Otherwise check that the module can be imported. exec(f'import {res}') except Exception: return unittest.skip(f"'{res}' is not available") else: return _id def load_ordered_tests(loader, standard_tests, pattern): """Keep the tests in the order they were declared in the class. Thanks to https://stackoverflow.com/a/62073640 """ ordered_cases = [] for test_suite in standard_tests: ordered = [] for test_case in test_suite: test_case_type = type(test_case) method_name = test_case._testMethodName testMethod = getattr(test_case, method_name) line = testMethod.__code__.co_firstlineno ordered.append( (line, test_case_type, method_name) ) ordered.sort() for line, case_type, name in ordered: ordered_cases.append(case_type(name)) return unittest.TestSuite(ordered_cases) def find_in_logs(logs, logger, msg): """Return True if 'msg' from 'logger' is in 'logs'.""" for log in (log.split(':', maxsplit=2) for log in logs): if len(log) == 3 and log[1] == logger and log[2] == msg: return True return False def search_in_logs(logs, logger, matcher): """Return True if the matcher's pattern is found in a message in 'logs'.""" for log in (log.split(':', maxsplit=2) for log in logs): if (len(log) == 3 and log[1] == logger and matcher.search(log[2]) is not None): return True return False def min_python_version(sys_version): return unittest.skipIf(sys.version_info < sys_version, f'Python version {sys_version} or higher required') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1734086807.2395272 libpulse-0.7/libpulse/tests/test_libpulse.py0000644000000000000000000002501714727010227016345 0ustar00"""libPulse test cases.""" import re import asyncio import logging from unittest import TestCase, IsolatedAsyncioTestCase, mock # Load the tests in the order they are declared. from . import load_ordered_tests as load_tests from . import requires_resources, search_in_logs import libpulse.libpulse as libpulse_module from ..libpulse import * from ..pulse_functions import pulse_functions SINK_NAME= 'foo' MODULE_ARG = (f'sink_name="{SINK_NAME}" sink_properties=device.description=' fr'"{SINK_NAME}\ description"') async def get_event(facility, type, lib_pulse, ready): try: await lib_pulse.pa_context_subscribe(PA_SUBSCRIPTION_MASK_ALL) iterator = lib_pulse.get_events_iterator() ready.set_result(True) index = None async for event in iterator: if event.facility == facility and event.type == type: iterator.close() index = event.index return index except asyncio.CancelledError: print('get_event(): CancelledError') except LibPulseError as e: return e def signature(funcname): #Return the signature of a function listed in the pulse_functions module. sig = pulse_functions['signatures'].get(funcname) if sig is None: sig = pulse_functions['callbacks'][funcname] return sig class LoadModule: def __init__(self, lib_pulse, name, argument): self.lib_pulse = lib_pulse self.name = name self.argument = argument self.module_index = PA_INVALID_INDEX async def __aenter__(self): self.module_index = await self.lib_pulse.pa_context_load_module( self.name, self.argument) return self async def __aexit__(self, exc_type, exc_value, traceback): if self.module_index != PA_INVALID_INDEX: await self.lib_pulse.pa_context_unload_module(self.module_index) @requires_resources('libpulse') class LibPulseTests(IsolatedAsyncioTestCase): async def test_log_server_info(self): with self.assertLogs(level=logging.DEBUG) as m_logs: async with LibPulse('libpulse-test') as lib_pulse: await lib_pulse.log_server_info() self.assertTrue(search_in_logs(m_logs.output, 'libpuls', re.compile(fr'Server: [Pp]ulse[Aa]udio.* \d+\.\d'))) async def test_list_sinks(self): async with LibPulse('libpulse-test') as lib_pulse: async with LoadModule(lib_pulse, 'module-null-sink', MODULE_ARG) as loaded_module: for sink in \ await lib_pulse.pa_context_get_sink_info_list(): if sink.name == SINK_NAME: break else: self.fail(f"'{SINK_NAME}' is not listed in the sink" f' list') async def test_sink_by_name(self): async with LibPulse('libpulse-test') as lib_pulse: async with LoadModule(lib_pulse, 'module-null-sink', MODULE_ARG) as loaded_module: sink = (await lib_pulse.pa_context_get_sink_info_by_name(SINK_NAME)) self.assertEqual(sink.name, SINK_NAME) self.assertEqual(sink.channel_map.channels, 2) self.assertEqual(len(sink.channel_map.map), 32) async def test_sink_proplist(self): async with LibPulse('libpulse-test') as lib_pulse: async with LoadModule(lib_pulse, 'module-null-sink', MODULE_ARG) as loaded_module: sink = (await lib_pulse.pa_context_get_sink_info_by_name(SINK_NAME)) self.assertTrue(re.match(fr'{SINK_NAME}\\? description', sink.proplist['device.description'])) async def test_events(self): async with LibPulse('libpulse-test') as lib_pulse: ready = lib_pulse.loop.create_future() evt_task = asyncio.create_task(get_event('module', 'new', lib_pulse, ready)) await ready async with LoadModule(lib_pulse, 'module-null-sink', MODULE_ARG) as loaded_module: await asyncio.wait_for(evt_task, 1) self.assertEqual(evt_task.result(), loaded_module.module_index) async def test_abort_iterator(self): async def main(): try: async with LibPulse('libpulse-test') as lib_pulse: # Run the asynchronous iterator loop until it is aborted # or cancelled. ready = lib_pulse.loop.create_future() evt_task = asyncio.create_task(get_event('invalid', 'new', lib_pulse, ready)) await ready # Raise an exception to force the closing of the LibPulse # instance and the iterator abort. 1/0 except ZeroDivisionError: pass await evt_task return evt_task.result() main_task = asyncio.create_task(main()) await main_task self.assertTrue(isinstance(main_task.result(), LibPulseClosedIteratorError)) async def test_excep_ctx_mgr(self): with mock.patch.object(libpulse_module, 'pa_context_connect') as connect,\ self.assertRaises(LibPulseStateError): connect.side_effect = LibPulseStateError() async with LibPulse('libpulse-test') as lib_pulse: pass async def test_cancel_ctx_mgr(self): with mock.patch.object(libpulse_module, 'pa_context_connect') as connect,\ self.assertLogs(level=logging.DEBUG) as m_logs: connect.side_effect = asyncio.CancelledError() try: async with LibPulse('libpulse-test') as lib_pulse: pass except LibPulseStateError as e: self.assertEqual(e.args[0], ('PA_CONTEXT_UNCONNECTED', 'PA_OK')) else: self.fail('LibPulseStateError has not been raised') async def test_cancel_main(self): async def main(main_ready): try: async with LibPulse('libpulse-test') as lib_pulse: main_ready.set_result(True) ready = lib_pulse.loop.create_future() try: await ready except asyncio.CancelledError: lib_pulse.state = error_state raise except LibPulseStateError as e: return e except Exception: return None error_state = ('PA_CONTEXT_FAILED', 'PA_ERR_KILLED') loop = asyncio.get_running_loop() main_ready = loop.create_future() main_task = asyncio.create_task(main(main_ready)) await main_ready main_task.cancel() await main_task result = main_task.result() self.assertTrue(isinstance(result, LibPulseStateError)) self.assertEqual(result.args[0], error_state) async def test_fail_instance(self): with self.assertLogs(level=logging.DEBUG) as m_logs,\ self.assertRaises(LibPulseClosedError): async with LibPulse('libpulse-test') as lib_pulse: LibPulse.ASYNCIO_LOOPS = dict() async with LoadModule(lib_pulse, 'module-null-sink', MODULE_ARG): pass async def test_fail_connect(self): # This test assumes that the libpulse library calls # _context_state_callback() at least twice when connecting to the # library. with mock.patch.object(libpulse_module, 'pa_context_get_state') as connect,\ self.assertLogs(level=logging.DEBUG) as m_logs: connect.side_effect = [ PA_CONTEXT_READY, # connected PA_CONTEXT_READY, # ignored state PA_CONTEXT_READY, # idem PA_CONTEXT_FAILED, # connection failure ] async with LibPulse('libpulse-test') as lib_pulse: wait_forever = lib_pulse.loop.create_future() try: await wait_forever except asyncio.CancelledError: # Ensure that lib_pulse._close() does call # pa_context_disconnect(). lib_pulse.state = ('PA_CONTEXT_READY', 'PA_OK') else: self.fail('wait_forever has not been cancelled as expected') self.assertTrue(search_in_logs(m_logs.output, 'libpuls', re.compile('LibPulse instance .* aborted:.*PA_CONTEXT_FAILED'))) async def test_bad_args(self): with self.assertRaises(LibPulseArgumentError): async with LibPulse('my libpulse') as lib_pulse: await lib_pulse.pa_context_get_server_info('bad arg') class LibPulseClassTests(TestCase): """Check that the async methods signatures and their callbacks's match.""" def check_method(self, method): sig = signature(method) self.assertEqual(sig[0], 'pa_operation *') self.assertEqual(sig[1][-1], 'void *') self.assertTrue(sig[1][-2].endswith('_cb_t')) # The callback. callback_sig = signature(sig[1][-2]) self.assertEqual(callback_sig[0], 'void') self.assertEqual(callback_sig[1][-1], 'void *') return sig, callback_sig def test_context_methods(self): for method in LibPulse.context_methods: self.check_method(method) def test_context_success_methods(self): for method in LibPulse.context_success_methods: sig, callback_sig = self.check_method(method) self.assertEqual(sig[1][-2], 'pa_context_success_cb_t') def test_context_list_methods(self): for method in LibPulse.context_list_methods: sig, callback_sig = self.check_method(method) self.assertEqual(callback_sig[1][-2], 'int') self.assertTrue(callback_sig[1][-3].endswith(' *')) def test_stream_success_methods(self): for method in LibPulse.stream_success_methods: sig, callback_sig = self.check_method(method) self.assertEqual(sig[1][-2], 'pa_stream_success_cb_t') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1718963001.3125184 libpulse-0.7/libpulse/tests/test_libpulse_ctypes.py0000644000000000000000000000223414635245471017741 0ustar00"""libpulse_ctypes test cases.""" import io from unittest import TestCase, mock from contextlib import redirect_stdout # Load the tests in the order they are declared. from . import load_ordered_tests as load_tests from . import requires_resources import libpulse.libpulse_ctypes as libpulse_ctypes_module from ..libpulse_ctypes import PulseCTypes, PulseCTypesLibError @requires_resources('libpulse') class LibPulseCTypesTestCase(TestCase): def test_print_types(self): with redirect_stdout(io.StringIO()) as output: libpulse_ctypes_module.print_types(['types', 'structs', 'callbacks', 'prototypes']) output = output.getvalue() self.assertIn('pa_io_event_flags_t', output) self.assertIn('pa_server_info', output) self.assertIn('io_new', output) self.assertIn('pa_context_new', output) def test_missing_lib(self): with mock.patch.object(libpulse_ctypes_module, 'find_library') as find_library,\ self.assertRaises(PulseCTypesLibError): find_library.return_value = None PulseCTypes() ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1739891326.306668 libpulse-0.7/libpulse/tests/test_pactl.py0000644000000000000000000004401414755121176015636 0ustar00"""Pactl test cases.""" import io import re import asyncio import inspect import logging from contextlib import redirect_stdout from unittest import IsolatedAsyncioTestCase # Load the tests in the order they are declared. from . import load_ordered_tests as load_tests from . import requires_resources, is_pipewire, min_python_version from ..pactl import main, parse_boolean, PactlSubCommands from ..libpulse import PA_ENCODING_AC3_IEC61937 async def subcommand(result_name, cmd, *args, as_str=False): with redirect_stdout(io.StringIO()) as output: argv = ['pactl.py', cmd] argv.extend(args) await main(argv) if as_str: return output.getvalue() # Return the result as a Python object. d={} exec(output.getvalue(), globals(), d) if result_name is None: return d return d[result_name] async def get_entity_from_name(entity, name): entities = entity + 's' if not entity.endswith('s') else entity items = await subcommand(entities, 'list', entities) for item in items: if item['name'] == name: return item class NullSinkModule: def __init__(self, pactl_tests, sink_name): self.sink_name = sink_name async def close(self): await subcommand('result', 'unload-module', str(self.index)) async def __aenter__(self): self.index = await subcommand('index', 'load-module', 'module-null-sink', f'sink_name="{self.sink_name}"') try: sink = await get_entity_from_name('sink', self.sink_name) if sink is None: assert False, f"Cannot find sink named '{self.sink_name}'" return sink except Exception: await self.close() raise async def __aexit__(self, exc_type, exc_value, traceback): await self.close() @requires_resources('libpulse') class PactlTests(IsolatedAsyncioTestCase): def check_volume(self, result, int1, percent1, db1, int2, percent2, db2): expected = ( f'Volume: front-left: {int1} / {percent1}% / {db1} dB,' f' front-right: {int2} / {percent2}% / {db2} dB') self.assertTrue(result.replace(' ', '').startswith( expected.replace(' ', ''))) async def run_test_volume(self, type, name): # 'name' is either the name or the index. set = f'set-{type}-volume' get = f'get-{type}-volume' result = await subcommand('result', set, name, '1.0') self.assertEqual(result, 'PA_OPERATION_DONE') result = await subcommand('result', get, name, as_str=True) self.check_volume(result, '65536', '100', '0.00', '65536', '100', '0.00') # Additive Relative adjust. result = await subcommand('result', set, name, '+1') self.assertEqual(result, 'PA_OPERATION_DONE') result = await subcommand('result', get, name, as_str=True) self.check_volume(result, '65537', '100', '0.00', '65537', '100', '0.00') # Multiplicative Relative adjust. result = await subcommand('result', set, '--', name, '-10db') self.assertEqual(result, 'PA_OPERATION_DONE') result = await subcommand('result', get, name, as_str=True) self.check_volume(result, '44650', '68', '-10.00', '44650', '68', '-10.00') # Different values on each channel. result = await subcommand('result', set, name, '1.0', '0.5') self.assertEqual(result, 'PA_OPERATION_DONE') result = await subcommand('result', get, name, as_str=True) self.check_volume(result, '65536', '100', '0.00', '52016', '79', '-6.02') # Restore defaults. result = await subcommand('result', set, name, '1.0') self.assertEqual(result, 'PA_OPERATION_DONE') async def run_test_mute(self, type, index): def bool_to_int(flag): return parse_boolean(str(flag).lower()) get = f'get-{type}-mute' set = f'set-{type}-mute' # Get default. result = await subcommand('mute', get, index) self.assertTrue(isinstance(result, bool)) mute_default = result # Set to not default. result = await subcommand('result', set, index, str(bool_to_int(not mute_default))) self.assertEqual(result, 'PA_OPERATION_DONE') # Toggle and restore default. result = await subcommand('result', set, index, 'toggle') self.assertEqual(result, 'PA_OPERATION_DONE') result = await subcommand('mute', get, index) self.assertEqual(result, mute_default) async def run_sink_input_test(self, test_function): name = '440 Hz Sine' async with NullSinkModule(self, 'foo') as sink: result = await subcommand('result', 'set-default-sink', str(sink['index'])) self.assertEqual(result, 'PA_OPERATION_DONE') sine_index = None try: sine_index = await subcommand('index', 'load-module', 'module-sine') sink_inputs = await subcommand('sink_inputs', 'list', 'sink-inputs') for sink_input in sink_inputs: if sink_input['name'] == name: index = str(sink_input['index']) break else: assert False, f"'{name}' sink-input not found as a source" await test_function(index) finally: await subcommand('result', 'unload-module', str(sine_index)) async def test_stat(self): pa_stat_info = await subcommand('pa_stat_info', 'stat') keys = ['memblock_total', 'scache_size', 'memblock_allocated'] self.assertTrue(set(keys).issubset(pa_stat_info)) async def test_info(self): pa_server_info = await subcommand('pa_server_info', 'info') keys = ['default_sink_name', 'channel_map', 'server_name'] self.assertTrue(set(keys).issubset(pa_server_info)) async def test_list_modules(self): modules = await subcommand('modules', 'list', 'modules') self.assertTrue(isinstance(modules, list)) keys = ['index', 'name', 'auto_unload'] for module in modules: self.assertTrue(set(keys).issubset(module)) async def test_list_clients(self): clients = await subcommand('clients', 'list', 'clients') self.assertTrue(isinstance(clients, list)) names = [] keys = ['index', 'name', 'owner_module'] for client in clients: self.assertTrue(set(keys).issubset(client)) names.append(client['name']) self.assertIn('pactl.py', names) async def test_list_cards(self): cards = await subcommand('cards', 'list', 'cards') self.assertTrue(isinstance(cards, list)) keys = ['index', 'name', 'owner_module', 'driver', 'n_profiles', 'profiles', 'active_profile', 'proplist', 'n_ports', 'ports', 'profiles2', 'active_profile2'] for card in cards: self.assertTrue(set(keys).issubset(card)) async def test_list_all(self): all = await subcommand(None, 'list') keys = ['modules', 'sinks', 'sources', 'sink_inputs', 'source_outputs', 'clients', 'samples', 'cards'] self.assertTrue(set(keys).issubset(all.keys())) async def test_list_message_handlers(self): message_handlers = await subcommand('message_handlers', 'list', 'message-handlers') self.assertTrue(isinstance(message_handlers, list)) if is_pipewire(): self.assertTrue(re.search(r'[Pp]ipe[Ww]ire', message_handlers[1])) else: self.assertIn('Core message handler', message_handlers[1]) @requires_resources('pulseaudio') async def test_load_module(self): with self.assertRaises(SystemExit) as cm: await subcommand('index', 'load-module', 'this is an unknown module name') e = cm.exception self.assertTrue(re.search(r'Module initialization failed', e.args[0])) async def test_unload_by_index(self): index = await subcommand('index', 'load-module', 'module-null-sink') self.assertTrue(isinstance(index, int)) result = await subcommand('result', 'unload-module', str(index)) self.assertEqual(result, 'PA_OPERATION_DONE') async def test_unload_by_name(self): # Check that no module exists with this name. name = 'module-null-sink' modules = await subcommand('modules', 'list', 'modules') for module in modules: if module['name'] == name: self.skipTest(f"At least one module named '{name}' exists") await subcommand('index', 'load-module', name) result = await subcommand('result', 'unload-module', name) self.assertEqual(result, 'PA_OPERATION_DONE') async def test_operation_null_pointer(self): with self.assertRaises(SystemExit) as cm: # type of 'idx' argument of pa_context_move_sink_input_by_name() # is uint32_t, and the PA_INVALID_INDEX definition is # ((uint32_t) -1). The function returns NULL in that case. await subcommand(None, 'move-sink-input', '-1', 'foo') e = cm.exception self.assertTrue(re.search(r'NULL .pa_operation. pointer' r': Invalid argument', e.args[0])) async def test_unknown_argument(self): with self.assertRaises(SystemExit) as cm: await subcommand(None, 'move-source-output', '0', 'invalid_source_name') e = cm.exception self.assertTrue(re.search(r'Failure: No such entity', e.args[0])) @requires_resources('pulseaudio') async def test_move_sink_input(self): async def move_sink_input(index): async with NullSinkModule(self, 'bar') as sink: result = await subcommand('result', 'move-sink-input', index, str(sink['index'])) self.assertEqual(result, 'PA_OPERATION_DONE') await self.run_sink_input_test(move_sink_input) async def test_suspend_sink(self): name = 'foo' async with NullSinkModule(self, name): result = await subcommand('result', 'suspend-sink', name, 'true') self.assertEqual(result, 'PA_OPERATION_DONE') result = await subcommand('result', 'suspend-sink', name, 'false') self.assertEqual(result, 'PA_OPERATION_DONE') @requires_resources('pulseaudio') async def test_suspend_source(self): index = None name = 'sine_input' try: index = await subcommand('index', 'load-module', 'module-sine-source') result = await subcommand('result', 'suspend-source', name, 'true') self.assertEqual(result, 'PA_OPERATION_DONE') result = await subcommand('result', 'suspend-source', name, 'false') self.assertEqual(result, 'PA_OPERATION_DONE') finally: await subcommand('result', 'unload-module', str(index)) @requires_resources('pulseaudio') async def test_default_sink(self): name = 'foo' async with NullSinkModule(self, name) as sink: result = await subcommand('result', 'set-default-sink', str(sink['index'])) self.assertEqual(result, 'PA_OPERATION_DONE') default_sink_name = await subcommand('default_sink_name', 'get-default-sink') self.assertEqual(default_sink_name, name) @requires_resources('pulseaudio') async def test_default_source(self): name = 'foo' monitor = f'{name}.monitor' async with NullSinkModule(self, name): sources = await subcommand('sources', 'list', 'sources') for source in sources: if source['name'] == monitor: index = str(source['index']) break else: assert False, f"'{monitor}' sink monitor not found as a source" result = await subcommand('result', 'set-default-source', index) self.assertEqual(result, 'PA_OPERATION_DONE') default_source_name = await subcommand('default_source_name', 'get-default-source') self.assertEqual(default_source_name, monitor) async def test_sink_volume(self): async with NullSinkModule(self, 'foo') as sink: await self.run_test_volume('sink', str(sink['index'])) async def test_source_volume(self): name = 'foo' monitor = f'{name}.monitor' async with NullSinkModule(self, name): await self.run_test_volume('source', monitor) @requires_resources('pulseaudio') async def test_sink_input_volume(self): async def set_volume(index): # Multiplicative Relative adjust. result = await subcommand('result', 'set-sink-input-volume', '--', index, '-10db') self.assertEqual(result, 'PA_OPERATION_DONE') await self.run_sink_input_test(set_volume) async def test_sink_mute(self): async with NullSinkModule(self, 'foo') as sink: index = str(sink['index']) await self.run_test_mute('sink', index) async def test_source_mute(self): name = 'foo' monitor = f'{name}.monitor' async with NullSinkModule(self, name): sources = await subcommand('sources', 'list', 'sources') for source in sources: if source['name'] == monitor: index = str(source['index']) break else: assert False, f"'{monitor}' sink monitor not found as a source" await self.run_test_mute('source', index) @requires_resources('pulseaudio') async def test_sink_input_mute(self): async def set_mute(index): result = await subcommand('result', 'set-sink-input-mute', index, '0') self.assertEqual(result, 'PA_OPERATION_DONE') await self.run_sink_input_test(set_mute) @requires_resources('pulseaudio') async def test_sink_formats(self): name = 'foo' format_rate = '[32000, 44100, 48000]' async with NullSinkModule(self, name) as sink: index = str(sink['index']) result = await subcommand('result', 'set-sink-formats', index, f'ac3-iec61937, format.rate = "{format_rate}"') self.assertEqual(result, 'PA_OPERATION_DONE') sink = await get_entity_from_name('sink', sink['name']) self.assertEqual(sink['formats'][0]['encoding'], PA_ENCODING_AC3_IEC61937) self.assertEqual(sink['formats'][0]['plist']['format.rate'], format_rate) # Restore default. result = await subcommand('result', 'set-sink-formats', index, 'pcm') self.assertEqual(result, 'PA_OPERATION_DONE') async def test_send_message(self): response = await subcommand('response', 'send-message', '/core', 'list-handlers') for resp in response: if resp['name'] == '/core': if is_pipewire(): self.assertTrue(re.search(r'[Pp]ipe[Ww]ire', resp['description'])) else: self.assertEqual(resp['description'], 'Core message handler') break else: assert False, '/core not found in response' @min_python_version((3, 11)) async def test_subscribe(self): async def unload_by_index(): await asyncio.sleep(0.5) await main(['pactl.py', 'unload-module', str(index)]) eof = "Event 'remove' on module" async with asyncio.timeout(1): index = await subcommand('index', 'load-module', 'module-null-sink') asyncio.create_task(unload_by_index()) result = await subcommand('result', 'subscribe', eof, as_str=True) self.assertTrue(eof in result) async def test_help(self): result = await subcommand('result', 'help', as_str=True) async def test_help_commands(self): # Avoid noisy asyncio warning: # Executing =3.2,<4"] build-backend = "flit_core.buildapi" [project] name = "libpulse" readme = "README.rst" requires-python = ">=3.8,<4" license = {file = "LICENSE"} authors = [{name = "Xavier de Gaye", email = "xdegaye@gmail.com"}] keywords = ["pulseaudio", "pipewire", "asyncio", "ctypes"] classifiers = [ "Development Status :: 2 - Pre-Alpha", "Framework :: AsyncIO", "License :: OSI Approved :: MIT License", "Operating System :: Unix", "Programming Language :: Python :: 3", "Topic :: Multimedia :: Sound/Audio :: Players", ] dynamic = ["version", "description"] [project.urls] Documentation = "https://libpulse.readthedocs.io/en/stable/" Source = "https://gitlab.com/xdegaye/libpulse" Changelog = "https://libpulse.readthedocs.io/en/stable/history.html" [project.scripts] pactl-py = "libpulse.pactl:script_main" [tool.flit.module] name = "libpulse" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667661560.3930001 libpulse-0.7/tools/__init__.py0000644000000000000000000000000014331477370013375 0ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1721565556.7669091 libpulse-0.7/tools/libpulse_parser.py0000644000000000000000000001562514647200565015054 0ustar00""" Preprocess and parse the libpulse headers. Function signatures are found when parsing: - 'types' (callbacks). - 'structs' (arrays of function pointers). - 'functions'. - as argument of a function when this is a function pointer itself, for example in 'pa_mainloop_api_once'. """ import os import sys import textwrap import shutil import subprocess import pprint from pyclibrary.c_library import CParser PULSEAUDIO_H = '/usr/include/pulse/pulseaudio.h' EXT_DEVICE_RESTORE = '/usr/include/pulse/ext-device-restore.h' class ParseError(Exception): pass def dedent(txt): """A dedent that does not use the first line to compute the margin.""" lines = txt.splitlines(keepends=True) # Find the first non empty line, skipping the first line. idx = 1 for i, l in enumerate(lines[1:]): if l != '\n': idx = i + 1 break return ''.join(lines[:idx]) + textwrap.dedent(''.join(lines[idx:])) def preprocess(header, pathname): with open(pathname, 'w') as f: proc = subprocess.run(['gcc', '-E', '-P', header], stdout=f, text=True) print(f"'{pathname}' created") def get_parser(): if shutil.which('gcc') is None: print('*** Error: GNU gcc is required', file=sys.stderr) sys.exit(1) if not os.path.exists(PULSEAUDIO_H): print(f'*** Error: {PULSEAUDIO_H} does not exist', file=sys.stderr) sys.exit(1) dirname = os.path.dirname(__file__) pulse_cpp = os.path.join(dirname, 'libpulse.cpp') ext_device_restore_cpp = os.path.join(dirname, 'ext_device_restore.cpp') preprocess(PULSEAUDIO_H, pulse_cpp) preprocess(EXT_DEVICE_RESTORE, ext_device_restore_cpp) print('Please wait, this may take a while ...') parser = CParser([pulse_cpp, ext_device_restore_cpp], replace={r'__attribute__ *\(\([^)]+\)\)': ''}) return parser def lib_generator(parser, type): return ((name, item) for (name, item) in parser.defs[type].items() if name.startswith('pa_') and not name.startswith('pa_x')) def signature_index(type_instance): # A Type instance is a function signature when one of its members is a # tuple. sig_idx = 0 for i, item in enumerate(type_instance): if i == 0: # The type name. continue if isinstance(item, tuple): sig_idx = i break return sig_idx def get_type(type_instance): items = [] for item in type_instance: # An array. if (isinstance(item, list) and len(item) == 1 and isinstance(item[0], int)): count = int(item[0]) if count > 0: item = f'* {count}' else: item = '*' items.append(item) return ' '.join(items) def parse_types(parser): types = {} callbacks = {} for name, type_instance in lib_generator(parser, 'types'): if signature_index(type_instance): # Signature of a function pointer. callbacks[name] = (type_instance[0], list(get_type(s[1]) for s in type_instance[1])) else: types[name] = type_instance[0] return types, callbacks def parse_enums(parser): enums = {} for name, enum in lib_generator(parser, 'enums'): enums[name] = enum return enums def parse_array(struct): array = {} for member in struct.members: name = member[0] type_instance = member[1] sig_idx = signature_index(type_instance) if sig_idx: # A structure member as a function pointer. # The signature has a variable length as the return type is not a # Type instance as usual but a variable number of str elements. restype = ' '.join(type_instance[:sig_idx]) arg_types = list(get_type(s[1]) for s in type_instance[sig_idx]) array[name] = (restype, arg_types) else: # A structure member as a plain type. array[name] = get_type(type_instance) continue return array def parse_structs(parser): structs ={} arrays = {} try: for name, struct in lib_generator(parser, 'structs'): # An array of function pointers. if name in ('pa_mainloop_api', 'pa_spawn_api'): arrays[name] = parse_array(struct) continue result = [] for member in struct.members: result.append((member[0], get_type(member[1]))) structs[name] = tuple(result) except Exception as e: raise ParseError(f"Structure '{name}': {struct}") from e return structs, arrays def parse_functions(parser): functions = {} for name, func in lib_generator(parser, 'functions'): assert signature_index(func) try: restype = " ".join(func[0]) arg_types = [] for arg in func[1]: type_instance = arg[1] if signature_index(type_instance): # Signature of a function pointer. arg_types.append((type_instance[0], list(get_type(s[1]) for s in type_instance[1]))) else: arg_types.append(get_type(type_instance)) functions[name] = (restype, arg_types) except Exception as e: raise ParseError(f"Function '{name}': {func}") from e return functions def main(): parser = get_parser() types, callbacks = parse_types(parser) enums = parse_enums(parser) structs, arrays = parse_structs(parser) signatures = parse_functions(parser) # The files of the parsed sections are written to 'dirname'. dirname = '.' if len(sys.argv) > 1: dirname = sys.argv[1] if not os.path.isdir(dirname): print(f"*** Error: '{dirname}' is not a directory", file=sys.stderr) sys.exit(1) # Merge all signatures into a 'functions' dictionary. functions = {} functions['signatures'] = {} functions['signatures'].update(signatures) functions['callbacks'] = callbacks for name, signature in arrays['pa_mainloop_api'].items(): if name != 'userdata': functions['callbacks'][name] = signature # Create the parsed sections files. for name in ('types', 'enums', 'structs', 'functions'): pulse_name = 'pulse_' + name doc = f'''"""File {pulse_name}. This file has been generated by libpulse_parser.py - DO NOT MODIFY. """ ''' pathname = os.path.join(dirname, pulse_name + '.py') with open(pathname, 'w') as f: print(dedent(doc), file=f) print(f'{pulse_name} = ', file=f, end='\\\n') pprint.pprint(eval(name), width=100, stream=f) print(f"'{pathname}' created") if __name__ == '__main__': main() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1734098045.3448186 libpulse-0.7/tools/set_devpt_version_name.py0000644000000000000000000000364214727036175016422 0ustar00"""Update the version using 'git describe'.""" import sys import re import subprocess from packaging import version as pkg_version INIT_FILE = 'libpulse/__init__.py' def normalize(version, do_print=False): "Check and normalize 'version' as conform to PEP 440." try: v = pkg_version.Version(version) if do_print: v_dict = { 'release': v.release, 'pre': v.pre, 'post': v.post, 'dev': v.dev, 'local': v.local, } for name, value in v_dict.items(): print(f'{name}: {value}', file=sys.stderr) return v except pkg_version.InvalidVersion as e: print(e, file=sys.stderr) sys.exit(1) def main(): """Set the development version name in INIT_FILE. The 'git describe' command outputs: release'-'number_of_commits_since_last_tag'-g'short_commit_sha After all the characters after the last '-' in the output have been striped, the version is normalized (made conform to PEP 440) as: release'.post'number_of_commits_since_last_tag For example '0.14.post3'. """ version = subprocess.check_output(['git', 'describe']) version = version.decode().strip() version = normalize(version.rsplit('-', maxsplit=1)[0], do_print=True) if version.post is None: print(f'*** Error:\n Cannot set a development version name at release' f' {version}.\n It must be followed by at least one commit.', file=sys.stderr) sys.exit(1) with open(INIT_FILE) as f: txt = f.read() regexp = re.compile(r"(__version__\s+=\s+)'([^']+)'") new_txt = regexp.sub(rf"\1'{version}'", txt) with open(INIT_FILE, 'w') as f: f.write(new_txt) print(f"{INIT_FILE} has been updated with: __version__ = '{version}'", file=sys.stderr) if __name__ == '__main__': main() libpulse-0.7/PKG-INFO0000644000000000000000000001111300000000000011160 0ustar00Metadata-Version: 2.3 Name: libpulse Version: 0.7 Summary: Asyncio interface to the Pulseaudio and Pipewire pulse library. Keywords: pulseaudio,pipewire,asyncio,ctypes Author-email: Xavier de Gaye Requires-Python: >=3.8,<4 Description-Content-Type: text/x-rst Classifier: Development Status :: 2 - Pre-Alpha Classifier: Framework :: AsyncIO Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: Unix Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Multimedia :: Sound/Audio :: Players Project-URL: Changelog, https://libpulse.readthedocs.io/en/stable/history.html Project-URL: Documentation, https://libpulse.readthedocs.io/en/stable/ Project-URL: Source, https://gitlab.com/xdegaye/libpulse .. image:: images/coverage.png :alt: [libpulse test coverage] Asyncio interface to the Pulseaudio and Pipewire pulse library. Overview -------- `libpulse`_ is a Python package based on `asyncio`_, that uses `ctypes`_ to interface with the ``pulse`` library of the PulseAudio and PipeWire sound servers. The interface is meant to be complete. That is, all the constants, structures, plain functions and async functions are made available by importing the libpulse module of the libpulse package. Async functions are those ``pulse`` functions that return results through a callback. They are implemented as asyncio coroutines that return the callback results. They have the same name as the corresponding pulse async function. Non-async ``pulse`` functions have their corresponding ctypes foreign functions defined in the libpulse module namespace under the same name as the corresponding pulse function. They may be called directly. Calling an async function or a plain function is simple: .. code-block:: python import asyncio import libpulse.libpulse as libpulse async def main(): async with libpulse.LibPulse('my libpulse') as lp_instance: # A plain function. server = libpulse.pa_context_get_server(lp_instance.c_context) print('server:', server.decode()) # An async function. sink = await lp_instance.pa_context_get_sink_info_by_index(0) print('sample_spec rate:', sink.sample_spec.rate) print('proplist names:', list(sink.proplist.keys())) asyncio.run(main()) Another example processing ``pulse`` events: .. code-block:: python import asyncio import libpulse.libpulse as libpulse async def main(): async with libpulse.LibPulse('my libpulse') as lp_instance: await lp_instance.pa_context_subscribe( libpulse.PA_SUBSCRIPTION_MASK_ALL) iterator = lp_instance.get_events_iterator() async for event in iterator: # Start playing some sound to print the events. # 'event' is an instance of the PulseEvent class. print(event.__dict__) asyncio.run(main()) The libpulse package also includes the ``pactl-py`` command, which is a Python implementation of the ``pactl`` command running on Pulseaudio and Pipewire. The output of most ``pactl-py`` subcommands can be parsed by Python. When this output is redirected to a file, the file can be imported as a Python module. For example start an interactive Python session and inspect the ``cards`` object with all its nested sructures and dereferenced pointers with: .. code-block:: shell $ pactl-py list cards > cards.py && python -i cards.py Requirements ------------ Python version 3.8 or more recent. Documentation ------------- The libpulse documentation is hosted at `Read the Docs`_: - The `stable documentation`_ of the last released version. - The `latest documentation`_ of the current GitLab development version. To access the documentation as a pdf document one must click on the icon at the down-right corner of any page. It allows to switch between stable and latest versions and to select the corresponding pdf document. The documentation describing the C language API of the ``pulse`` library is at `PulseAudio Documentation`_. Installation ------------ Install ``libpulse`` with pip:: $ python -m pip install libpulse .. _libpulse: https://gitlab.com/xdegaye/libpulse .. _asyncio: https://docs.python.org/3/library/asyncio.html .. _ctypes: https://docs.python.org/3/library/ctypes.html .. _Read the Docs: https://about.readthedocs.com/ .. _stable documentation: https://libpulse.readthedocs.io/en/stable/ .. _latest documentation: https://libpulse.readthedocs.io/en/latest/ .. _`PulseAudio Documentation`: https://freedesktop.org/software/pulseaudio/doxygen/index.html