Hello community,
here is the log from the commit of package python-pulsectl for
openSUSE:Leap:15.2 checked in at 2020-03-09 18:10:46
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Leap:15.2/python-pulsectl (Old)
and /work/SRC/openSUSE:Leap:15.2/.python-pulsectl.new.26092 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pulsectl"
Mon Mar 9 18:10:46 2020 rev:11 rq:776941 version:20.1.2
Changes:
--------
--- /work/SRC/openSUSE:Leap:15.2/python-pulsectl/python-pulsectl.changes
2020-01-15 15:51:43.419538762 +0100
+++
/work/SRC/openSUSE:Leap:15.2/.python-pulsectl.new.26092/python-pulsectl.changes
2020-03-09 18:10:47.173146735 +0100
@@ -1,0 +2,22 @@
+Mon Jan 20 13:59:09 UTC 2020 - Ondřej Súkup <[email protected]>
+
+- update to 20.1.2
+ * add pulse.play_sample() - server-side stored sample playback
+ * Add pulse.get_peak_sample() func for getting volume peak within timespan
+
+-------------------------------------------------------------------
+Wed Jun 5 08:09:07 UTC 2019 - Marketa Calabkova <[email protected]>
+
+- update to version 18.12.5
+ * pulse.connect() can now be used to reconnect to same server
+ * _pulse_op_cb: check connected state instead of _loop_stop
+ * _pulse_op_cb: fix hang if daemon dies
+ * tests: use "-F /dev/stdin" instead of -C for dummy pulse instance
+ * Add pulsectl.lookup util submodule
+
+-------------------------------------------------------------------
+Tue Dec 4 12:51:49 UTC 2018 - Matej Cepl <[email protected]>
+
+- Remove superfluous devel dependency for noarch package
+
+-------------------------------------------------------------------
Old:
----
pulsectl-17.12.2.tar.gz
New:
----
pulsectl-20.1.2.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-pulsectl.spec ++++++
--- /var/tmp/diff_new_pack.j5jglk/_old 2020-03-09 18:10:47.533147251 +0100
+++ /var/tmp/diff_new_pack.j5jglk/_new 2020-03-09 18:10:47.537147257 +0100
@@ -1,7 +1,7 @@
#
# spec file for package python-pulsectl
#
-# Copyright (c) 2017 SUSE LINUX GmbH, Nuernberg, Germany.
+# Copyright (c) 2020 SUSE LLC
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -12,32 +12,31 @@
# license that conforms to the Open Source Definition (Version 1.9)
# published by the Open Source Initiative.
-# Please submit bugfixes or comments via http://bugs.opensuse.org/
+# Please submit bugfixes or comments via https://bugs.opensuse.org/
+#
%{?!python_module:%define python_module() python-%{**} python3-%{**}}
%bcond_without test
Name: python-pulsectl
-Version: 17.12.2
+Version: 20.1.2
Release: 0
-License: MIT
Summary: Python high-level interface and ctypes-based bindings for
PulseAudio (libpulse)
-Url: http://github.com/mk-fg/python-pulse-control
+License: MIT
Group: Development/Languages/Python
+URL: http://github.com/mk-fg/python-pulse-control
Source:
https://files.pythonhosted.org/packages/source/p/pulsectl/pulsectl-%{version}.tar.gz
-BuildRequires: python-rpm-macros
-BuildRequires: %{python_module devel}
BuildRequires: %{python_module setuptools}
+BuildRequires: fdupes
+BuildRequires: python-rpm-macros
+Requires: pulseaudio >= 5.0
+Requires: python-setuptools
+BuildArch: noarch
%if %{with test}
+BuildRequires: libpulse-devel >= 5.0
BuildRequires: pulseaudio
BuildRequires: pulseaudio-utils
-BuildRequires: libpulse-devel >= 5.0
%endif
-BuildRequires: fdupes
-Requires: python-setuptools
-Requires: pulseaudio >= 5.0
-BuildArch: noarch
-
%python_subpackages
%description
@@ -58,12 +57,12 @@
%if %{with test}
%check
-%python_exec setup.py test
+%python_exec -m unittest pulsectl.tests.all
%endif
%files %{python_files}
-%defattr(-,root,root,-)
-%doc CHANGES.rst COPYING README.rst
+%license COPYING
+%doc CHANGES.rst README.rst
%{python_sitelib}/*
%changelog
++++++ pulsectl-17.12.2.tar.gz -> pulsectl-20.1.2.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pulsectl-17.12.2/CHANGES.rst
new/pulsectl-20.1.2/CHANGES.rst
--- old/pulsectl-17.12.2/CHANGES.rst 2017-12-14 21:32:25.000000000 +0100
+++ new/pulsectl-20.1.2/CHANGES.rst 2020-01-11 17:20:39.000000000 +0100
@@ -8,13 +8,22 @@
Each entry is a package version which change first appears in, followed by
description of the change itself.
-Last synced/updated: 17.12.2
+Last synced/updated: 20.1.2
---------------------------------------------------------------------------
-- 17.12.2: Use pa_card_profile_info2 / profiles2 introspection API.
+- 20.1.1: Add pulse.play_sample() - server-side stored sample playback [#36].
+
+ Loading is not implemented, would suggest something like libcanberra for
that.
+
+- 19.9.1: Add pulse.get_peak_sample() func for getting volume peak within
timespan [#33].
+
+- 18.10.5: pulse.connect() can now be used to reconnect to same server.
+
+- 17.12.2: Use pa_card_profile_info2 / profiles2 introspection API [#19].
Only adds one "available" property to PulseCardProfileInfo.
+ Requires pulseaudio/libpulse 5.0+.
- 17.9.3: Add wrappers for Pulse.get_sink_by_name / Pulse.get_source_by_name
[#17].
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pulsectl-17.12.2/PKG-INFO
new/pulsectl-20.1.2/PKG-INFO
--- old/pulsectl-17.12.2/PKG-INFO 2017-12-14 21:33:32.000000000 +0100
+++ new/pulsectl-20.1.2/PKG-INFO 2020-01-11 17:24:10.035188400 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: pulsectl
-Version: 17.12.2
+Version: 20.1.2
Summary: Python high-level interface and ctypes-based bindings for PulseAudio
(libpulse)
Home-page: http://github.com/mk-fg/python-pulse-control
Author: George Filipkin, Mike Kazantsev
@@ -104,13 +104,18 @@
and everything returned from these are "Pulse-Something-Info" objects
- thin
wrappers around C structs that describe the thing, without any methods
attached.
+ Aside from a few added convenience methods, most of them should have
similar
+ signature and do same thing as their C libpulse API counterparts, so
see
+ `pulseaudio doxygen documentation`_ for more information on them.
+
Pulse client can be integrated into existing eventloop (e.g. asyncio,
twisted,
etc) using ``Pulse.set_poll_func()`` or ``Pulse.event_listen()`` in a
separate
thread.
Somewhat extended usage example can be found in
`pulseaudio-mixer-cli`_ project
- code.
+ code, as well as tests here.
+ .. _pulseaudio doxygen documentation:
https://freedesktop.org/software/pulseaudio/doxygen/introspect_8h.html
.. _pulseaudio-mixer-cli:
https://github.com/mk-fg/pulseaudio-mixer-cli/blob/master/pa-mixer-mk3.py
@@ -318,11 +323,14 @@
issues with pulseaudio (or its daemon.conf) and underlying
dependencies.
There are no "expected" test case failures.
+ All tests can run for up to 10 seconds currently (v19.9.6), due to some
+ involving playback (using paplay from /dev/urandom) being
time-sensitive.
+
Changelog and versioning scheme
```````````````````````````````
- This package uses one-version-per commit scheme (updated by pre-commit
hook)
+ This package uses one-version-per-commit scheme (updated by pre-commit
hook)
and pretty much one release per git commit, unless more immediate
follow-up
commits are planned or too lazy to run ``py setup.py sdist bdist_wheel
upload``
for some trivial README typo fix.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pulsectl-17.12.2/README.rst
new/pulsectl-20.1.2/README.rst
--- old/pulsectl-17.12.2/README.rst 2017-10-26 12:33:28.000000000 +0200
+++ new/pulsectl-20.1.2/README.rst 2019-09-24 01:03:43.000000000 +0200
@@ -96,13 +96,18 @@
and everything returned from these are "Pulse-Something-Info" objects - thin
wrappers around C structs that describe the thing, without any methods
attached.
+Aside from a few added convenience methods, most of them should have similar
+signature and do same thing as their C libpulse API counterparts, so see
+`pulseaudio doxygen documentation`_ for more information on them.
+
Pulse client can be integrated into existing eventloop (e.g. asyncio, twisted,
etc) using ``Pulse.set_poll_func()`` or ``Pulse.event_listen()`` in a separate
thread.
Somewhat extended usage example can be found in `pulseaudio-mixer-cli`_ project
-code.
+code, as well as tests here.
+.. _pulseaudio doxygen documentation:
https://freedesktop.org/software/pulseaudio/doxygen/introspect_8h.html
.. _pulseaudio-mixer-cli:
https://github.com/mk-fg/pulseaudio-mixer-cli/blob/master/pa-mixer-mk3.py
@@ -310,11 +315,14 @@
issues with pulseaudio (or its daemon.conf) and underlying dependencies.
There are no "expected" test case failures.
+All tests can run for up to 10 seconds currently (v19.9.6), due to some
+involving playback (using paplay from /dev/urandom) being time-sensitive.
+
Changelog and versioning scheme
```````````````````````````````
-This package uses one-version-per commit scheme (updated by pre-commit hook)
+This package uses one-version-per-commit scheme (updated by pre-commit hook)
and pretty much one release per git commit, unless more immediate follow-up
commits are planned or too lazy to run ``py setup.py sdist bdist_wheel upload``
for some trivial README typo fix.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pulsectl-17.12.2/pulsectl/__init__.py
new/pulsectl-20.1.2/pulsectl/__init__.py
--- old/pulsectl-17.12.2/pulsectl/__init__.py 2017-07-13 12:23:26.000000000
+0200
+++ new/pulsectl-20.1.2/pulsectl/__init__.py 2018-03-03 01:03:48.000000000
+0100
@@ -4,8 +4,9 @@
from . import _pulsectl
from .pulsectl import (
- PulseCardInfo, PulseClientInfo, PulsePortInfo, PulseVolumeInfo,
+ PulsePortInfo, PulseClientInfo, PulseServerInfo, PulseModuleInfo,
PulseSinkInfo, PulseSinkInputInfo, PulseSourceInfo,
PulseSourceOutputInfo,
+ PulseCardProfileInfo, PulseCardPortInfo, PulseCardInfo, PulseVolumeInfo,
PulseExtStreamRestoreInfo, PulseEventInfo,
PulseEventTypeEnum, PulseEventFacilityEnum, PulseEventMaskEnum,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pulsectl-17.12.2/pulsectl/_pulsectl.py
new/pulsectl-20.1.2/pulsectl/_pulsectl.py
--- old/pulsectl-17.12.2/pulsectl/_pulsectl.py 2017-12-14 21:29:35.000000000
+0100
+++ new/pulsectl-20.1.2/pulsectl/_pulsectl.py 2020-01-11 17:06:48.000000000
+0100
@@ -4,7 +4,7 @@
# C Bindings
-import os, sys, functools as ft
+import os, sys, ctypes.util, functools as ft
from ctypes import *
@@ -95,6 +95,13 @@
PA_SUBSCRIPTION_EVENT_REMOVE = 0x0020
PA_SUBSCRIPTION_EVENT_TYPE_MASK = 0x0030
+PA_SAMPLE_FLOAT32BE = 5
+
+PA_STREAM_DONT_MOVE = 0x0200
+PA_STREAM_PEAK_DETECT = 0x0800
+PA_STREAM_ADJUST_LATENCY = 0x2000
+PA_STREAM_DONT_INHIBIT_AUTO_SUSPEND = 0x8000
+
def c_enum_map(**values):
return dict((v, force_str(k)) for k,v in values.items())
@@ -264,6 +271,7 @@
('name', c_char_p),
('owner_module', c_uint32),
('driver', c_char_p),
+ ('proplist', POINTER(PA_PROPLIST)),
]
class PA_SERVER_INFO(Structure):
@@ -339,6 +347,15 @@
('mute', c_int),
]
+class PA_BUFFER_ATTR(Structure):
+ _fields_ = [
+ ('maxlength', c_uint32),
+ ('tlength', c_uint32),
+ ('prebuf', c_uint32),
+ ('minreq', c_uint32),
+ ('fragsize', c_uint32),
+ ]
+
class POLLFD(Structure):
_fields_ = [
@@ -442,6 +459,15 @@
c_int,
c_void_p)
+PA_STREAM_REQUEST_CB_T = CFUNCTYPE(c_void_p,
+ POINTER(PA_STREAM),
+ c_int,
+ c_void_p)
+
+PA_STREAM_NOTIFY_CB_T = CFUNCTYPE(c_void_p,
+ POINTER(PA_STREAM),
+ c_void_p)
+
class LibPulse(object):
@@ -473,6 +499,7 @@
pa_context_connect=([POINTER(PA_CONTEXT), c_str_p, c_int,
POINTER(c_int)], 'int_check_ge0'),
pa_context_get_state=([POINTER(PA_CONTEXT)], c_int),
pa_context_disconnect=[POINTER(PA_CONTEXT)],
+ pa_context_unref=[POINTER(PA_CONTEXT)],
pa_context_drain=( 'pa_op',
[POINTER(PA_CONTEXT), PA_CONTEXT_DRAIN_CB_T, c_void_p]
),
pa_context_set_default_sink=( 'pa_op',
@@ -535,7 +562,7 @@
[POINTER(PA_CONTEXT), c_uint32, PA_CLIENT_INFO_CB_T,
c_void_p] ),
pa_context_get_server_info=( 'pa_op',
[POINTER(PA_CONTEXT), PA_SERVER_INFO_CB_T, c_void_p] ),
- pa_operation_unref=([POINTER(PA_OPERATION)], c_int),
+ pa_operation_unref=[POINTER(PA_OPERATION)],
pa_context_get_card_info_by_index=( 'pa_op',
[POINTER(PA_CONTEXT), c_uint32, PA_CARD_INFO_CB_T,
c_void_p] ),
pa_context_get_card_info_list=( 'pa_op',
@@ -570,13 +597,33 @@
[POINTER(PA_CHANNEL_MAP)], (POINTER(PA_CHANNEL_MAP),
'not_null') ),
pa_channel_map_snprint=([c_str_p, c_int,
POINTER(PA_CHANNEL_MAP)], c_str_p),
pa_channel_map_parse=(
- [POINTER(PA_CHANNEL_MAP), c_str_p],
(POINTER(PA_CHANNEL_MAP), 'not_null') ) )
+ [POINTER(PA_CHANNEL_MAP), c_str_p],
(POINTER(PA_CHANNEL_MAP), 'not_null') ),
+ pa_proplist_from_string=([c_str_p], POINTER(PA_PROPLIST)),
+ pa_proplist_free=[POINTER(PA_PROPLIST)],
+ pa_stream_new_with_proplist=(
+ [ POINTER(PA_CONTEXT), c_str_p,
+ POINTER(PA_SAMPLE_SPEC),
POINTER(PA_CHANNEL_MAP), POINTER(PA_PROPLIST) ],
+ POINTER(PA_STREAM) ),
+ pa_stream_set_monitor_stream=([POINTER(PA_STREAM), c_uint32],
'int_check_ge0'),
+ pa_stream_set_read_callback=[POINTER(PA_STREAM),
PA_STREAM_REQUEST_CB_T, c_void_p],
+ pa_stream_connect_record=(
+ [POINTER(PA_STREAM), c_str_p, POINTER(PA_BUFFER_ATTR),
c_int], 'int_check_ge0' ),
+ pa_stream_unref=[POINTER(PA_STREAM)],
+ pa_stream_peek=(
+ [POINTER(PA_STREAM), POINTER(c_void_p),
POINTER(c_int)], 'int_check_ge0' ),
+ pa_stream_drop=([POINTER(PA_STREAM)], 'int_check_ge0'),
+ pa_stream_disconnect=([POINTER(PA_STREAM)], 'int_check_ge0'),
+ pa_context_play_sample=( 'pa_op',
+ [POINTER(PA_CONTEXT), c_str_p, c_str_p, c_uint32,
PA_CONTEXT_SUCCESS_CB_T, c_void_p] ),
+ pa_context_play_sample_with_proplist=( 'pa_op',
+ [ POINTER(PA_CONTEXT), c_str_p, c_str_p, c_uint32,
+ POINTER(PA_PROPLIST), PA_CONTEXT_SUCCESS_CB_T,
c_void_p ] ) )
class CallError(Exception): pass
def __init__(self):
- p = CDLL('libpulse.so.0')
+ p = CDLL(ctypes.util.find_library('libpulse') or
'libpulse.so.0')
self.funcs = dict()
for k, spec in self.func_defs.items():
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pulsectl-17.12.2/pulsectl/lookup.py
new/pulsectl-20.1.2/pulsectl/lookup.py
--- old/pulsectl-17.12.2/pulsectl/lookup.py 1970-01-01 01:00:00.000000000
+0100
+++ new/pulsectl-20.1.2/pulsectl/lookup.py 2018-04-23 18:48:50.000000000
+0200
@@ -0,0 +1,93 @@
+# -*- coding: utf-8 -*-
+from __future__ import print_function, unicode_literals
+
+import itertools as it, operator as op, functools as ft
+import re
+
+
+lookup_types = {
+ 'sink': 'sink_list', 'source': 'source_list',
+ 'sink-input': 'sink_input_list', 'source-output': 'source_output_list' }
+lookup_types.update(it.chain.from_iterable(
+ ((v, lookup_types[k]) for v in v) for k,v in
+ { 'source': ['src'], 'sink-input': ['si', 'playback', 'play'],
+ 'source-output': ['so', 'record', 'rec', 'mic'] }.items() ))
+
+lookup_key_defaults = dict(
+ # No default keys for type = no implicit matches for that type
+ sink_input_list=[ # match sink_input_list objects with these keys by
default
+ 'media.name', 'media.icon_name', 'media.role',
+ 'application.name', 'application.process.binary',
'application.icon_name' ] )
+
+
+def pulse_obj_lookup(pulse, obj_lookup, prop_default=None):
+ '''Return set of pulse object(s) with proplist values matching
lookup-string.
+
+ Pattern syntax:
+ [ { 'sink' | 'source' | 'sink-input' | 'source-output'
} [ / ... ] ':' ]
+ [ proplist-key-name (non-empty) [ / ... ] ':' ] [ ':'
(for regexp match) ]
+ [ proplist-key-value ]
+
+ Examples:
+ - sink:alsa.driver_name:snd_hda_intel
+ Match sink(s) with
alsa.driver_name=snd_hda_intel (exact match!).
+ - sink/source:device.bus:pci
+ Match all sinks and sources with device.bus=pci.
+ - myprop:somevalue
+ Match any object (of all 4 supported types)
that has myprop=somevalue.
+ - mpv
+ Match any object with any of the "default
lookup props" (!!!) being equal to "mpv".
+ "default lookup props" are specified per-type
in lookup_key_defaults above.
+ For example, sink input will be looked-up by
media.name, application.name, etc.
+ - sink-input/source-output:mpv
+ Same as above, but lookup streams only (not
sinks/sources).
+ Note that "sink-input/source-output" matches
type spec, and parsed as such, not as key.
+ - si/so:mpv
+ Same as above - see aliases for types in
lookup_types.
+ - application.binary/application.icon:mpv
+ Lookup by multiple keys with "any match" logic,
same as with multiple object types.
+ - key\/with\/slashes\:and\:colons:somevalue
+ Lookup by key that has slashes and colons in it.
+ "/" and ":" must only be escaped in the
proplist key part, used as-is in values.
+ Backslash itself can be escaped as well, i.e.
as "\\".
+ -
module-stream-restore.id:sink-input-by-media-role:music
+ Value has ":" in it, but there's no need to
escape it in any way.
+ - device.description::Analog
+ Value lookup starting with : is interpreted as
a regexp,
+ i.e. any object with device.description
*containing* "Analog" in this case.
+ - si/so:application.name::^mpv\b
+ Return all sink-inputs/source-outputs ("si/so")
where
+ "application.name" proplist value
matches regexp "^mpv\b".
+ - :^mpv\b
+ Regexp lookup (stuff starting with "mpv" word)
without type or key specification.
+
+ For python2, lookup string should be unicode type.
+ "prop_default" keyword arg can be used to specify
+ default proplist value for when key is not found
there.'''
+
+ # \ue000-\uf8ff - private use area, never assigned to symbols
+ obj_lookup = obj_lookup.replace('\\\\', '\ue000').replace('\\:',
'\ue001')
+ obj_types_re = '({0})(/({0}))*'.format('|'.join(lookup_types))
+ m = re.search(
+ ( r'^((?P<t>{}):)?'.format(obj_types_re) +
+ r'((?P<k>.+?):)?' r'(?P<v>.*)$' ), obj_lookup,
re.IGNORECASE )
+ if not m: raise ValueError(obj_lookup)
+ lookup_type, lookup_keys, lookup_re = op.itemgetter('t', 'k',
'v')(m.groupdict())
+ if lookup_keys:
+ lookup_keys = list(
+ v.replace('\ue000', '\\\\').replace('\ue001',
':').replace('\ue002', '/')
+ for v in lookup_keys.replace('\\/',
'\ue002').split('/') )
+ lookup_re = lookup_re.replace('\ue000', '\\\\').replace('\ue001', '\\:')
+ obj_list_res, lookup_re = list(), re.compile( lookup_re[1:]
+ if lookup_re.startswith(':') else
'^{}$'.format(re.escape(lookup_re)) )
+ for k in set( lookup_types[k] for k in
+ (lookup_type.split('/') if lookup_type else
lookup_types.keys()) ):
+ if not lookup_keys: lookup_keys = lookup_key_defaults.get(k)
+ if not lookup_keys: continue
+ obj_list = getattr(pulse, k)()
+ if not obj_list: continue
+ for obj, k in it.product(obj_list, lookup_keys):
+ v = obj.proplist.get(k, prop_default)
+ if v is None: continue
+ if lookup_re.search(v): obj_list_res.append(obj)
+ return set(obj_list_res)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pulsectl-17.12.2/pulsectl/pulsectl.py
new/pulsectl-20.1.2/pulsectl/pulsectl.py
--- old/pulsectl-17.12.2/pulsectl/pulsectl.py 2017-12-14 21:27:45.000000000
+0100
+++ new/pulsectl-20.1.2/pulsectl/pulsectl.py 2020-01-11 17:23:05.000000000
+0100
@@ -31,6 +31,10 @@
raise TypeError( 'Pulse<something>Info'
' object is required instead of value: [{}] {}',
type(obj), obj )
+class FakeLock():
+ def __enter__(self): return self
+ def __exit__(self, *err): pass
+
@ft.total_ordering
class EnumValue(object):
@@ -229,9 +233,10 @@
c_struct_fields = 'name description n_sinks n_sources priority
available'
class PulseCardPortInfo(PulsePortInfo):
- c_struct_fields = 'name description priority direction latency_offset'
+ c_struct_fields = 'name description available priority direction
latency_offset'
def _init_from_struct(self, struct):
+ super(PulseCardPortInfo, self)._init_from_struct(struct)
self.direction = PulseDirectionEnum._c_val(struct.direction)
class PulseCardInfo(PulseObject):
@@ -327,6 +332,8 @@
class Pulse(object):
+ _ctx = None
+
def __init__(self, client_name=None, server=None, connect=True,
threading_lock=False):
'''Connects to specified pulse server by default.
Specifying "connect=False" here prevents that, but be
sure to call connect() later.
@@ -357,19 +364,26 @@
self._pa_state_cb = c.PA_STATE_CB_T(self._pulse_state_cb)
self._pa_subscribe_cb =
c.PA_SUBSCRIBE_CB_T(self._pulse_subscribe_cb)
- self._loop, self._loop_lock = c.pa.mainloop_new(), None
+ self._loop, self._loop_lock = c.pa.mainloop_new(), FakeLock()
self._loop_running = self._loop_closed = False
self._api = c.pa.mainloop_get_api(self._loop)
+ self._ret = c.pa.return_value()
- self._ctx, self._ret = c.pa.context_new(self._api, self.name),
c.pa.return_value()
- c.pa.context_set_state_callback(self._ctx, self._pa_state_cb,
None)
-
- c.pa.context_set_subscribe_callback(self._ctx,
self._pa_subscribe_cb, None)
+ self._ctx_init()
self.event_types = sorted(PulseEventTypeEnum._values.values())
self.event_facilities =
sorted(PulseEventFacilityEnum._values.values())
self.event_masks = sorted(PulseEventMaskEnum._values.values())
self.event_callback = None
+ def _ctx_init(self):
+ if self._ctx:
+ with self._loop_lock:
+ self.disconnect()
+ c.pa.context_unref(self._ctx)
+ self._ctx = c.pa.context_new(self._api, self.name)
+ c.pa.context_set_state_callback(self._ctx, self._pa_state_cb,
None)
+ c.pa.context_set_subscribe_callback(self._ctx,
self._pa_subscribe_cb, None)
+
def connect(self, autospawn=False, wait=False):
'''Connect to pulseaudio server.
"autospawn" option will start new pulse daemon, if
necessary.
@@ -377,6 +391,7 @@
if self._loop_closed:
raise PulseError('Eventloop object was already'
' destroyed and cannot be reused from this
instance.')
+ if self.connected is not None: self._ctx_init()
flags, self.connected = 0, None
if not autospawn: flags |= c.PA_CONTEXT_NOAUTOSPAWN
if wait: flags |= c.PA_CONTEXT_NOFAIL
@@ -390,13 +405,15 @@
c.pa.context_disconnect(self._ctx)
def close(self):
- if self._loop:
- if self._loop_running:
- self._loop_closed = True
- c.pa.mainloop_quit(self._loop, 0)
- return
+ if not self._loop: return
+ if self._loop_running: # called from another thread
+ self._loop_closed = True
+ c.pa.mainloop_quit(self._loop, 0)
+ return # presumably will be closed in a thread that's
running it
+ with self._loop_lock:
try:
self.disconnect()
+ c.pa.context_unref(self._ctx)
c.pa.mainloop_free(self._loop)
finally: self._ctx = self._loop = None
@@ -430,8 +447,8 @@
@contextmanager
def _pulse_loop(self):
- if self._loop_lock: self._loop_lock.acquire()
- try:
+ with self._loop_lock:
+ if not self._loop: return
if self._loop_running:
raise PulseError(
'Running blocking pulse operations from
pulse eventloop callbacks'
@@ -445,8 +462,6 @@
finally:
self._loop_running = False
if self._loop_closed: self.close() # to free()
after stopping it
- finally:
- if self._loop_lock: self._loop_lock.release()
def _pulse_run(self):
with self._pulse_loop() as loop: c.pa.mainloop_run(loop,
self._ret)
@@ -462,7 +477,7 @@
cb = lambda s=True,k=act_id: self._actions.update({k:
bool(s)})
if not raw: cb = c.PA_CONTEXT_SUCCESS_CB_T(lambda
ctx,s,d,cb=cb: cb(s))
yield cb
- while self._actions[act_id] is None:
self._pulse_iterate()
+ while self.connected and self._actions[act_id] is None:
self._pulse_iterate()
if not self._actions[act_id]: raise
PulseOperationFailed(act_id)
finally: self._actions.pop(act_id, None)
@@ -473,7 +488,7 @@
ts = c.mono_time()
ts_deadline = timeout and (ts + timeout)
while True:
- delay = max(0, int((ts_deadline - ts) *
1000000)) if ts_deadline else -1
+ delay = max(0, int((ts_deadline - ts) * 1000))
if ts_deadline else -1
c.pa.mainloop_prepare(loop, delay) # usec
c.pa.mainloop_poll(loop)
if self._loop_closed: break # interrupted by
close() or such
@@ -484,6 +499,14 @@
def _pulse_info_cb(self, info_cls, data_list, done_cb, ctx, info, eof,
userdata):
+ # No idea where callbacks with "userdata != NULL" come from,
+ # but "info" pointer in them is always invalid, so they are
discarded here.
+ # Looks like some kind of mixup or corruption in libpulse
memory?
+ # See also:
https://github.com/mk-fg/python-pulse-control/issues/35
+ if userdata is not None: return
+ # Empty result list and conn issues are checked elsewhere.
+ # Errors here are non-descriptive (errno), so should not be
useful anyway.
+ # if eof < 0: done_cb(s=False)
if eof: done_cb()
else: data_list.append(info_cls(info[0]))
@@ -494,7 +517,9 @@
cb = cb_t(
ft.partial(self._pulse_info_cb,
info_cls, data, cb) if not singleton else
lambda ctx, info, userdata, cb=cb:
data.append(info_cls(info[0])) or cb() )
- pulse_func(self._ctx, *([index, cb, None] if
index is not None else [cb, None]))
+ pa_op = pulse_func( self._ctx,
+ *([index, cb, None] if index is not
None else [cb, None]) )
+ c.pa.operation_unref(pa_op)
data = data or list()
if index is not None or singleton:
if not data: raise PulseIndexError(index)
@@ -519,7 +544,7 @@
c.PA_SINK_INFO_CB_T,
c.pa.context_get_sink_info_by_name, PulseSinkInfo )
get_source_by_name = _pulse_get_list(
- c.PA_SINK_INFO_CB_T,
+ c.PA_SOURCE_INFO_CB_T,
c.pa.context_get_source_info_by_name, PulseSourceInfo )
sink_input_list = _pulse_get_list(
@@ -800,6 +825,76 @@
c.pa.mainloop_set_poll_func(self._loop, self._pa_poll_cb, None)
+ def get_peak_sample(self, source, timeout, stream_idx=None):
+ '''Returns peak (max) value in 0-1.0 range for samples in
source/stream within timespan.
+ "source" can be either int index of pulseaudio source
+ (i.e. source.index), its name (source.name), or
None to use default source.
+ Resulting value is what pulseaudio returns as
+ PA_SAMPLE_FLOAT32BE float after "timeout"
seconds.
+ If specified source does not exist, 0 should be
returned after timeout.
+ This can be used to detect if there's any sound
+ on the microphone or any sound played through a
sink via its monitor_source index,
+ or same for any specific stream connected to
these (if "stream_idx" is passed).
+ Sample stream masquerades as
+ application.id=org.PulseAudio.pavucontrol to
avoid being listed in various mixer apps.
+ Example - get peak for specific sink input "si" for 0.8
seconds:
+
pulse.get_peak_sample(pulse.sink_info(si.sink).monitor_source, 0.8, si.index)'''
+ samples, proplist = [0],
c.pa.proplist_from_string('application.id=org.PulseAudio.pavucontrol')
+ ss = c.PA_SAMPLE_SPEC(format=c.PA_SAMPLE_FLOAT32BE, rate=25,
channels=1)
+ s = c.pa.stream_new_with_proplist(self._ctx, 'peak detect',
c.byref(ss), None, proplist)
+ c.pa.proplist_free(proplist)
+
+ @c.PA_STREAM_REQUEST_CB_T
+ def read_cb(s, bs, userdata):
+ buff, bs = c.c_void_p(), c.c_int(bs)
+ c.pa.stream_peek(s, buff, c.byref(bs))
+ try:
+ if not buff or bs.value < 4: return
+ # This assumes that native byte order for
floats is BE, same as pavucontrol
+ samples[0] = max(samples[0], c.cast(buff,
c.POINTER(c.c_float))[0])
+ finally:
+ # stream_drop() flushes buffered data (incl.
buff=NULL "hole" data)
+ # stream.h: "should not be called if the buffer
is empty"
+ if bs.value: c.pa.stream_drop(s)
+
+ if stream_idx is not None: c.pa.stream_set_monitor_stream(s,
stream_idx)
+ c.pa.stream_set_read_callback(s, read_cb, None)
+ if source is not None: source = unicode(source).encode('utf-8')
+ try:
+ c.pa.stream_connect_record( s, source,
+ c.PA_BUFFER_ATTR(fragsize=4, maxlength=2**32-1),
+ c.PA_STREAM_DONT_MOVE | c.PA_STREAM_PEAK_DETECT
|
+ c.PA_STREAM_ADJUST_LATENCY |
c.PA_STREAM_DONT_INHIBIT_AUTO_SUSPEND )
+ except c.pa.CallError:
+ c.pa.stream_unref(s)
+ raise
+
+ try: self._pulse_poll(timeout)
+ finally:
+ try: c.pa.stream_disconnect(s)
+ except c.pa.CallError: pass # stream was removed
+
+ return min(1.0, samples[0])
+
+ def play_sample(self, name, sink=None, volume=1.0, proplist_str=None):
+ '''Play specified sound sample,
+ with an optional sink object/name/index, volume
and proplist string parameters.
+ Sample must be stored on the server in advance, see
e.g. "pacmd list-samples".
+ See also libcanberra for an easy XDG theme sample
loading, storage and playback API.'''
+ if isinstance(sink, PulseSinkInfo): sink = sink.index
+ sink = str(sink) if sink is not None else None
+ proplist = c.pa.proplist_from_string(proplist_str) if
proplist_str else None
+ volume = int(round(volume*c.PA_VOLUME_NORM))
+ with self._pulse_op_cb() as cb:
+ try:
+ if not proplist:
+ c.pa.context_play_sample(self._ctx,
name, sink, volume, cb, None)
+ else:
+ c.pa.context_play_sample_with_proplist(
+ self._ctx, name, sink, volume,
proplist, cb, None )
+ except c.pa.CallError as err: raise
PulseOperationInvalid(err.args[-1])
+
+
def connect_to_cli(server=None, as_file=True, socket_timeout=1.0, attempts=5,
retry_delay=0.3):
'''Returns connected CLI interface socket (as file object, unless
as_file=False),
where one can send same commands (as lines) as to
"pacmd" tool
@@ -850,7 +945,9 @@
with open(pid_path) as src:
os.kill(int(src.read().strip()), signal.SIGUSR2)
time.sleep(max(0, retry_delay - (c.mono_time() - ts)))
- return s.makefile('rw', 1) if as_file else s
+ if as_file: res = s.makefile('rw', 1)
+ else: res, s = s, None # to avoid closing this socket
+ return res
except Exception as err: # CallError, socket.error, IOError (pidfile),
OSError (os.kill)
raise PulseError( 'Failed to connect to pulse'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pulsectl-17.12.2/pulsectl/tests/all.py
new/pulsectl-20.1.2/pulsectl/tests/all.py
--- old/pulsectl-17.12.2/pulsectl/tests/all.py 2017-07-13 12:23:26.000000000
+0200
+++ new/pulsectl-20.1.2/pulsectl/tests/all.py 2018-10-28 16:08:52.000000000
+0100
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
-from .dummy_instance import DummyTests
+from .dummy_instance import DummyTests, PulseCrashTests
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pulsectl-17.12.2/pulsectl/tests/dummy_instance.py
new/pulsectl-20.1.2/pulsectl/tests/dummy_instance.py
--- old/pulsectl-17.12.2/pulsectl/tests/dummy_instance.py 2017-07-13
12:23:26.000000000 +0200
+++ new/pulsectl-20.1.2/pulsectl/tests/dummy_instance.py 2020-01-11
16:52:54.000000000 +0100
@@ -13,107 +13,142 @@
import pulsectl
-def setup_teardown(cls):
- for sig in 'hup', 'term', 'int':
- signal.signal(getattr(signal, 'sig{}'.format(sig).upper()),
lambda sig,frm: sys.exit())
- atexit.register(cls.tearDownClass)
-class DummyTests(unittest.TestCase):
-
- tmp_dir = proc = None
- sock_unix = sock_tcp4 = sock_tcp6 = None
-
- @classmethod
- def setUpClass(cls):
- setup_teardown(cls)
+class adict(dict):
+ def __init__(self, *args, **kws):
+ super(adict, self).__init__(*args, **kws)
+ self.__dict__ = self
- # These are to allow starting pulse with debug logging
- # or using pre-started (e.g. with gdb attached) instance
- # For example:
- # t1% env -i XDG_RUNTIME_DIR=/tmp/pulsectl-tests \
- # gdb --args /usr/bin/pulseaudio --daemonize=no --fail \
- # -nF /tmp/pulsectl-tests/conf.pa --exit-idle-time=-1
--log-level=debug
- # t2% PA_TMPDIR=/tmp/pulsectl-tests PA_REUSE=t python -m -m
unittest pulsectl.tests.all
- env_tmpdir, env_debug, env_reuse = map(
- os.environ.get, ['PA_TMPDIR', 'PA_DEBUG', 'PA_REUSE'] )
-
- tmp_base = env_tmpdir or cls.tmp_dir
- if not tmp_base: tmp_base = cls.tmp_dir =
tempfile.mkdtemp(prefix='pulsectl-tests.')
- tmp_base = os.path.realpath(tmp_base)
- tmp_path = ft.partial(os.path.join, tmp_base)
+def dummy_pulse_init(info=None):
+ if not info: info = adict(proc=None, tmp_dir=None)
+ try: _dummy_pulse_init(info)
+ except Exception:
+ dummy_pulse_cleanup(info)
+ raise
+ return info
+
+def _dummy_pulse_init(info):
+ # These are to allow starting pulse with debug logging
+ # or using pre-started (e.g. with gdb attached) instance.
+ # Note: PA_REUSE=1234:1234:1235 are localhost tcp ports for tcp modules.
+ # For example:
+ # t1% env -i XDG_RUNTIME_DIR=/tmp/pulsectl-tests \
+ # gdb --args /usr/bin/pulseaudio --daemonize=no --fail \
+ # -nF /tmp/pulsectl-tests/conf.pa --exit-idle-time=-1
--log-level=debug
+ # t2% PA_TMPDIR=/tmp/pulsectl-tests PA_REUSE=1234,1235 python -m -m
unittest pulsectl.tests.all
+ env_tmpdir, env_debug, env_reuse = map(
+ os.environ.get, ['PA_TMPDIR', 'PA_DEBUG', 'PA_REUSE'] )
+
+ tmp_base = env_tmpdir or info.get('tmp_dir')
+ if not tmp_base:
+ tmp_base = info.tmp_dir =
tempfile.mkdtemp(prefix='pulsectl-tests.')
+ info.sock_unix = None
+ tmp_base = os.path.realpath(tmp_base)
+ tmp_path = ft.partial(os.path.join, tmp_base)
- # Pick some random available localhost ports
+ # Pick some random available localhost ports
+ if not info.get('sock_unix'):
bind = ( ['127.0.0.1', 0, socket.AF_INET],
['::1', 0, socket.AF_INET6], ['127.0.0.1', 0,
socket.AF_INET] )
- for spec in bind:
+ for n, spec in enumerate(bind):
+ if env_reuse:
+ spec[1] = int(env_reuse.split(':')[n])
+ continue
addr, p, af = spec
with contextlib.closing(socket.socket(af,
socket.SOCK_STREAM)) as s:
s.bind((addr, p))
s.listen(1)
spec[1] = s.getsockname()[1]
- cls.sock_unix = 'unix:{}'.format(tmp_path('pulse', 'native'))
- cls.sock_tcp4 = 'tcp4:{}:{}'.format(bind[0][0], bind[0][1])
- cls.sock_tcp6 = 'tcp6:[{}]:{}'.format(bind[1][0], bind[1][1])
- cls.sock_tcp_cli = tuple(bind[2][:2])
-
- if not env_reuse and not cls.proc:
- env = dict(XDG_RUNTIME_DIR=tmp_base,
PULSE_STATE_PATH=tmp_base)
- log_level = 'error' if not env_debug else 'debug'
- cls.proc = subprocess.Popen(
- [ 'pulseaudio', '--daemonize=no', '--fail',
- '-nC', '--exit-idle-time=-1',
'--log-level={}'.format(log_level) ],
- env=env, stdin=subprocess.PIPE )
- for line in [
- 'module-augment-properties',
-
- 'module-default-device-restore',
- 'module-rescue-streams',
- 'module-always-sink',
- 'module-intended-roles',
- 'module-suspend-on-idle',
- 'module-position-event-sounds',
- 'module-role-cork',
- 'module-filter-heuristics',
- 'module-filter-apply',
- 'module-switch-on-port-available',
- 'module-stream-restore',
-
- 'module-native-protocol-tcp
auth-anonymous=true'
- ' listen={addr4}
port={port4}'.format(addr4=bind[0][0], port4=bind[0][1]),
- 'module-native-protocol-tcp
auth-anonymous=true'
- ' listen={addr6}
port={port6}'.format(addr6=bind[1][0], port6=bind[1][1]),
- 'module-native-protocol-unix',
-
- 'module-null-sink',
- 'module-null-sink' ]:
- if line.startswith('module-'): line =
'load-module {}'.format(line)
-
cls.proc.stdin.write('{}\n'.format(line).encode('utf-8'))
- cls.proc.stdin.flush()
- timeout, checks, p = 4, 10, cls.sock_unix.split(':',
1)[-1]
- for n in range(checks):
- if not os.path.exists(p):
- time.sleep(float(timeout) / checks)
- continue
- break
- else: raise AssertionError(p)
+ info.update(
+ sock_unix='unix:{}'.format(tmp_path('pulse', 'native')),
+ sock_tcp4='tcp4:{}:{}'.format(bind[0][0], bind[0][1]),
+ sock_tcp6='tcp6:[{}]:{}'.format(bind[1][0], bind[1][1]),
+ sock_tcp_cli=tuple(bind[2][:2]) )
+
+ if info.proc and info.proc.poll() is not None: info.proc = None
+ if not env_reuse and not info.get('proc'):
+ env = dict(XDG_RUNTIME_DIR=tmp_base, PULSE_STATE_PATH=tmp_base)
+ log_level = 'error' if not env_debug else 'debug'
+ info.proc = subprocess.Popen(
+ [ 'pulseaudio', '--daemonize=no', '--fail',
+ '-nF', '/dev/stdin', '--exit-idle-time=-1',
'--log-level={}'.format(log_level) ],
+ env=env, stdin=subprocess.PIPE )
+ bind4, bind6 = info.sock_tcp4.split(':'),
info.sock_tcp6.rsplit(':', 1)
+ bind4, bind6 = (bind4[1], bind4[2]), (bind6[0].split(':',
1)[1].strip('[]'), bind6[1])
+ for line in [
+ 'module-augment-properties',
+
+ 'module-default-device-restore',
+ 'module-rescue-streams',
+ 'module-always-sink',
+ 'module-intended-roles',
+ 'module-suspend-on-idle',
+ 'module-position-event-sounds',
+ 'module-role-cork',
+ 'module-filter-heuristics',
+ 'module-filter-apply',
+ 'module-switch-on-port-available',
+ 'module-stream-restore',
+
+ 'module-native-protocol-tcp auth-anonymous=true'
+ ' listen={} port={}'.format(*bind4),
+ 'module-native-protocol-tcp auth-anonymous=true'
+ ' listen={} port={}'.format(*bind6),
+ 'module-native-protocol-unix',
+
+ 'module-null-sink',
+ 'module-null-sink' ]:
+ if line.startswith('module-'): line = 'load-module
{}'.format(line)
+
info.proc.stdin.write('{}\n'.format(line).encode('utf-8'))
+ info.proc.stdin.close()
+ timeout, checks, p = 4, 10, info.sock_unix.split(':', 1)[-1]
+ for n in range(checks):
+ if not os.path.exists(p):
+ time.sleep(float(timeout) / checks)
+ continue
+ break
+ else: raise AssertionError(p)
+
+def dummy_pulse_cleanup(info=None, proc=None, tmp_dir=None):
+ if not info: info = adict(proc=proc, tmp_dir=tmp_dir)
+ if info.proc:
+ try: info.proc.terminate()
+ except OSError: pass
+ timeout, checks = 4, 10
+ for n in range(checks):
+ if info.proc.poll() is None:
+ time.sleep(float(timeout) / checks)
+ continue
+ break
+ else:
+ try: info.proc.kill()
+ except OSError: pass
+ info.proc.wait()
+ info.proc = None
+ if info.tmp_dir:
+ shutil.rmtree(info.tmp_dir, ignore_errors=True)
+ info.tmp_dir = None
+
+
+class DummyTests(unittest.TestCase):
+
+ proc = tmp_dir = None
+
+ @classmethod
+ def setUpClass(cls):
+ assert not cls.proc and not cls.tmp_dir, [cls.proc, cls.tmp_dir]
+
+ for sig in 'hup', 'term', 'int':
+ signal.signal(getattr(signal,
'sig{}'.format(sig).upper()), lambda sig,frm: sys.exit())
+ atexit.register(cls.tearDownClass)
+
+ cls.instance_info = dummy_pulse_init()
+ for k, v in cls.instance_info.items(): setattr(cls, k, v)
@classmethod
def tearDownClass(cls):
- if cls.proc:
- cls.proc.stdin.close()
- timeout, checks = 4, 10
- for n in range(checks):
- if cls.proc.poll() is None:
- time.sleep(float(timeout) / checks)
- continue
- break
- else: cls.proc.kill()
- cls.proc.wait()
- cls.proc = None
- if cls.tmp_dir:
- shutil.rmtree(cls.tmp_dir)
- cls.tmp_dir = None
+ dummy_pulse_cleanup(cls.instance_info)
# Fuzzy float comparison is necessary for volume,
@@ -215,6 +250,13 @@
xdg_dir_prev = os.environ.get('XDG_RUNTIME_DIR')
try:
os.environ['XDG_RUNTIME_DIR'] = self.tmp_dir
+ with
contextlib.closing(pulsectl.connect_to_cli(as_file=False)) as s:
+ s.send(b'dump\n')
+ while True:
+ try: buff = s.recv(2**20)
+ except socket.error: buff = None
+ if not buff: raise AssertionError
+ if b'### EOF' in buff.splitlines():
break
with contextlib.closing(pulsectl.connect_to_cli()) as s:
s.write('dump\n')
for line in s:
@@ -271,6 +313,16 @@
self.assertEqual(pulse.sink_info(sink.index).volume.values, sink.volume.values)
pulse.volume_set_all_chans(sink, 1.0)
+ def test_get_sink_src(self):
+ with pulsectl.Pulse('t', server=self.sock_unix) as pulse:
+ src, sink = pulse.source_list(), pulse.sink_list()
+ src_nx, sink_nx = max(s.index for s in src)+1,
max(s.index for s in sink)+1
+ src, sink = src[0], sink[0]
+ self.assertEqual(sink.index,
pulse.get_sink_by_name(sink.name).index)
+ self.assertEqual(src.index,
pulse.get_source_by_name(src.name).index)
+ with self.assertRaises(pulsectl.PulseIndexError):
pulse.source_info(src_nx)
+ with self.assertRaises(pulsectl.PulseIndexError):
pulse.sink_info(sink_nx)
+
def test_module_funcs(self):
with pulsectl.Pulse('t', server=self.sock_unix) as pulse:
self.assertEqual(len(pulse.sink_list()), 2)
@@ -323,6 +375,8 @@
if paplay.poll() is None: paplay.kill()
paplay.wait()
+ with self.assertRaises(pulsectl.PulseIndexError):
pulse.sink_input_info(stream.index)
+
def test_ext_stream_restore(self):
sr_name1 = 'sink-input-by-application-name:pulsectl-test-1'
sr_name2 = 'sink-input-by-application-name:pulsectl-test-2'
@@ -376,5 +430,141 @@
self.assertNotIn(sr_name1, sr_dict)
self.assertNotIn(sr_name2, sr_dict)
+ def test_stream_move(self):
+ with pulsectl.Pulse('t', server=self.sock_unix) as pulse:
+ stream_started = list()
+ def stream_ev_cb(ev):
+ if ev.t != 'new': return
+ stream_started.append(ev.index)
+ raise pulsectl.PulseLoopStop
+ pulse.event_mask_set('sink_input')
+ pulse.event_callback_set(stream_ev_cb)
+
+ paplay = subprocess.Popen(
+ ['paplay', '--raw', '/dev/zero'],
env=dict(XDG_RUNTIME_DIR=self.tmp_dir) )
+ try:
+ if not stream_started: pulse.event_listen()
+ stream_idx, = stream_started
+ stream = pulse.sink_input_info(stream_idx)
+ sink_indexes = set(s.index for s in
pulse.sink_list())
+ sink1 = stream.sink
+ sink2 = sink_indexes.difference([sink1]).pop()
+ sink_nx = max(sink_indexes) + 1
+
+ pulse.sink_input_move(stream.index, sink2)
+ stream_new = pulse.sink_input_info(stream.index)
+ self.assertEqual(stream.sink, sink1) # old info
doesn't get updated
+ self.assertEqual(stream_new.sink, sink2)
+
+ pulse.sink_input_move(stream.index, sink1) #
move it back
+ stream_new = pulse.sink_input_info(stream.index)
+ self.assertEqual(stream_new.sink, sink1)
+
+ with
self.assertRaises(pulsectl.PulseOperationFailed):
+ pulse.sink_input_move(stream.index,
sink_nx)
+
+ finally:
+ if paplay.poll() is None: paplay.kill()
+ paplay.wait()
+
+ def test_get_peak_sample(self):
+ # Note: this test takes at least multiple seconds to run
+ with pulsectl.Pulse('t', server=self.sock_unix) as pulse:
+ source_any = max(s.index for s in pulse.source_list())
+ source_nx = source_any + 1
+
+ time.sleep(0.3) # make sure previous streams die
+ peak = pulse.get_peak_sample(source_any, 0.3)
+ self.assertEqual(peak, 0)
+
+ stream_started = list()
+ def stream_ev_cb(ev):
+ if ev.t != 'new': return
+ stream_started.append(ev.index)
+ raise pulsectl.PulseLoopStop
+ pulse.event_mask_set('sink_input')
+ pulse.event_callback_set(stream_ev_cb)
+
+ paplay = subprocess.Popen(
+ ['paplay', '--raw', '/dev/urandom'],
env=dict(XDG_RUNTIME_DIR=self.tmp_dir) )
+ try:
+ if not stream_started: pulse.event_listen()
+ stream_idx, = stream_started
+ si = pulse.sink_input_info(stream_idx)
+ sink = pulse.sink_info(si.sink)
+ source = pulse.source_info(sink.monitor_source)
+
+ # First poll can randomly fail if too short,
probably due to latency or such
+ peak =
pulse.get_peak_sample(sink.monitor_source, 3)
+ self.assertGreater(peak, 0)
+
+ peak = pulse.get_peak_sample(source.index, 0.3,
si.index)
+ self.assertGreater(peak, 0)
+ peak = pulse.get_peak_sample(source.name, 0.3,
si.index)
+ self.assertGreater(peak, 0)
+ peak = pulse.get_peak_sample(source_nx, 0.3)
+ self.assertEqual(peak, 0)
+
+ paplay.terminate()
+ paplay.wait()
+
+ peak = pulse.get_peak_sample(source.index, 0.3,
si.index)
+ self.assertEqual(peak, 0)
+
+ finally:
+ if paplay.poll() is None: paplay.kill()
+ paplay.wait()
+
+
+class PulseCrashTests(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ for sig in 'hup', 'term', 'int':
+ signal.signal(getattr(signal,
'sig{}'.format(sig).upper()), lambda sig,frm: sys.exit())
+
+ def test_crash_after_connect(self):
+ info = dummy_pulse_init()
+ try:
+ with pulsectl.Pulse('t', server=info.sock_unix) as
pulse:
+ for si in pulse.sink_list(): self.assertTrue(si)
+ info.proc.terminate()
+ info.proc.wait()
+ with
self.assertRaises(pulsectl.PulseOperationFailed):
+ for si in pulse.sink_list(): raise
AssertionError(si)
+ self.assertFalse(pulse.connected)
+ finally: dummy_pulse_cleanup(info)
+
+ def test_reconnect(self):
+ info = dummy_pulse_init()
+ try:
+ with pulsectl.Pulse('t', server=info.sock_unix,
connect=False) as pulse:
+ with self.assertRaises(Exception):
+ for si in pulse.sink_list(): raise
AssertionError(si)
+
+ pulse.connect(autospawn=False)
+ self.assertTrue(pulse.connected)
+ for si in pulse.sink_list(): self.assertTrue(si)
+ info.proc.terminate()
+ info.proc.wait()
+ with self.assertRaises(Exception):
+ for si in pulse.sink_list(): raise
AssertionError(si)
+ self.assertFalse(pulse.connected)
+
+ dummy_pulse_init(info)
+ pulse.connect(autospawn=False, wait=True)
+ self.assertTrue(pulse.connected)
+ for si in pulse.sink_list(): self.assertTrue(si)
+
+ pulse.disconnect()
+ with self.assertRaises(Exception):
+ for si in pulse.sink_list(): raise
AssertionError(si)
+ self.assertFalse(pulse.connected)
+ pulse.connect(autospawn=False)
+ self.assertTrue(pulse.connected)
+ for si in pulse.sink_list(): self.assertTrue(si)
+
+ finally: dummy_pulse_cleanup(info)
+
if __name__ == '__main__': unittest.main()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pulsectl-17.12.2/pulsectl.egg-info/PKG-INFO
new/pulsectl-20.1.2/pulsectl.egg-info/PKG-INFO
--- old/pulsectl-17.12.2/pulsectl.egg-info/PKG-INFO 2017-12-14
21:33:32.000000000 +0100
+++ new/pulsectl-20.1.2/pulsectl.egg-info/PKG-INFO 2020-01-11
17:24:09.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: pulsectl
-Version: 17.12.2
+Version: 20.1.2
Summary: Python high-level interface and ctypes-based bindings for PulseAudio
(libpulse)
Home-page: http://github.com/mk-fg/python-pulse-control
Author: George Filipkin, Mike Kazantsev
@@ -104,13 +104,18 @@
and everything returned from these are "Pulse-Something-Info" objects
- thin
wrappers around C structs that describe the thing, without any methods
attached.
+ Aside from a few added convenience methods, most of them should have
similar
+ signature and do same thing as their C libpulse API counterparts, so
see
+ `pulseaudio doxygen documentation`_ for more information on them.
+
Pulse client can be integrated into existing eventloop (e.g. asyncio,
twisted,
etc) using ``Pulse.set_poll_func()`` or ``Pulse.event_listen()`` in a
separate
thread.
Somewhat extended usage example can be found in
`pulseaudio-mixer-cli`_ project
- code.
+ code, as well as tests here.
+ .. _pulseaudio doxygen documentation:
https://freedesktop.org/software/pulseaudio/doxygen/introspect_8h.html
.. _pulseaudio-mixer-cli:
https://github.com/mk-fg/pulseaudio-mixer-cli/blob/master/pa-mixer-mk3.py
@@ -318,11 +323,14 @@
issues with pulseaudio (or its daemon.conf) and underlying
dependencies.
There are no "expected" test case failures.
+ All tests can run for up to 10 seconds currently (v19.9.6), due to some
+ involving playback (using paplay from /dev/urandom) being
time-sensitive.
+
Changelog and versioning scheme
```````````````````````````````
- This package uses one-version-per commit scheme (updated by pre-commit
hook)
+ This package uses one-version-per-commit scheme (updated by pre-commit
hook)
and pretty much one release per git commit, unless more immediate
follow-up
commits are planned or too lazy to run ``py setup.py sdist bdist_wheel
upload``
for some trivial README typo fix.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pulsectl-17.12.2/pulsectl.egg-info/SOURCES.txt
new/pulsectl-20.1.2/pulsectl.egg-info/SOURCES.txt
--- old/pulsectl-17.12.2/pulsectl.egg-info/SOURCES.txt 2017-12-14
21:33:32.000000000 +0100
+++ new/pulsectl-20.1.2/pulsectl.egg-info/SOURCES.txt 2020-01-11
17:24:09.000000000 +0100
@@ -6,6 +6,7 @@
setup.py
pulsectl/__init__.py
pulsectl/_pulsectl.py
+pulsectl/lookup.py
pulsectl/pulsectl.py
pulsectl.egg-info/PKG-INFO
pulsectl.egg-info/SOURCES.txt
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pulsectl-17.12.2/setup.cfg
new/pulsectl-20.1.2/setup.cfg
--- old/pulsectl-17.12.2/setup.cfg 2017-12-14 21:33:32.000000000 +0100
+++ new/pulsectl-20.1.2/setup.cfg 2020-01-11 17:24:10.037188500 +0100
@@ -4,5 +4,4 @@
[egg_info]
tag_build =
tag_date = 0
-tag_svn_revision = 0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/pulsectl-17.12.2/setup.py
new/pulsectl-20.1.2/setup.py
--- old/pulsectl-17.12.2/setup.py 2017-12-14 21:29:52.000000000 +0100
+++ new/pulsectl-20.1.2/setup.py 2020-01-11 17:22:41.000000000 +0100
@@ -13,7 +13,7 @@
setup(
name = 'pulsectl',
- version = '17.12.2',
+ version = '20.1.2',
author = 'George Filipkin, Mike Kazantsev',
author_email = '[email protected]',
license = 'MIT',