Closed josh-richardson closed 4 years ago
My current idea is something like this:
diff --git a/pulsectl/_pulsectl.py b/pulsectl/_pulsectl.py
index 4422ddf..e5a4c1d 100644
--- a/pulsectl/_pulsectl.py
+++ b/pulsectl/_pulsectl.py
@@ -479,6 +479,8 @@ class LibPulse(object):
func_defs = dict(
pa_strerror=([c_int], c_str_p),
pa_runtime_path=([c_str_p], (c_char_p, 'not_null')),
+ pa_operation_unref=[POINTER(PA_OPERATION)],
+
pa_mainloop_new=(POINTER(PA_MAINLOOP)),
pa_mainloop_get_api=([POINTER(PA_MAINLOOP)], POINTER(PA_MAINLOOP_API)),
pa_mainloop_run=([POINTER(PA_MAINLOOP), POINTER(c_int)], c_int),
@@ -490,9 +492,11 @@ class LibPulse(object):
pa_mainloop_set_poll_func=[POINTER(PA_MAINLOOP), PA_POLL_FUNC_T, c_void_p],
pa_mainloop_quit=([POINTER(PA_MAINLOOP), c_int]),
pa_mainloop_free=[POINTER(PA_MAINLOOP)],
+
pa_signal_init=([POINTER(PA_MAINLOOP_API)], 'int_check_ge0'),
pa_signal_new=([c_int, PA_SIGNAL_CB_T, POINTER(PA_SIGNAL_EVENT)]),
pa_signal_done=None,
+
pa_context_errno=([POINTER(PA_CONTEXT)], c_int),
pa_context_new=([POINTER(PA_MAINLOOP_API), c_str_p], POINTER(PA_CONTEXT)),
pa_context_set_state_callback=([POINTER(PA_CONTEXT), PA_STATE_CB_T, c_void_p]),
@@ -562,7 +566,6 @@ class LibPulse(object):
[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)],
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_by_name=( 'pa_op',
@@ -581,6 +584,17 @@ class LibPulse(object):
[POINTER(PA_CONTEXT), c_uint32, PA_CONTEXT_SUCCESS_CB_T, c_void_p] ),
pa_context_subscribe=( 'pa_op',
[POINTER(PA_CONTEXT), c_int, PA_CONTEXT_SUCCESS_CB_T, c_void_p] ),
+ pa_context_set_subscribe_callback=[POINTER(PA_CONTEXT), PA_SUBSCRIBE_CB_T, c_void_p],
+ 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 ] ),
+ pa_context_proplist_update=( 'pa_op',
+ [POINTER(PA_CONTEXT), c_int, POINTER(PA_PROPLIST), PA_CONTEXT_SUCCESS_CB_T, c_void_p] ),
+ pa_context_proplist_remove=( 'pa_op',
+ [POINTER(PA_CONTEXT), POINTER(POINTER(c_char_p)), PA_CONTEXT_SUCCESS_CB_T, c_void_p] ),
+
pa_ext_stream_restore_test=( 'pa_op',
[POINTER(PA_CONTEXT), PA_EXT_STREAM_RESTORE_TEST_CB_T, c_void_p] ),
pa_ext_stream_restore_read=( 'pa_op',
@@ -590,9 +604,7 @@ class LibPulse(object):
c_uint, c_int, PA_CONTEXT_SUCCESS_CB_T, c_void_p ] ),
pa_ext_stream_restore_delete=( 'pa_op',
[POINTER(PA_CONTEXT), POINTER(c_char_p), PA_CONTEXT_SUCCESS_CB_T, c_void_p] ),
- pa_context_set_subscribe_callback=[POINTER(PA_CONTEXT), PA_SUBSCRIBE_CB_T, c_void_p],
- pa_proplist_iterate=([POINTER(PA_PROPLIST), POINTER(c_void_p)], c_str_p),
- pa_proplist_gets=([POINTER(PA_PROPLIST), c_str_p], c_str_p),
+
pa_channel_map_init_mono=(
[POINTER(PA_CHANNEL_MAP)], (POINTER(PA_CHANNEL_MAP), 'not_null') ),
pa_channel_map_init_stereo=(
@@ -600,8 +612,12 @@ class LibPulse(object):
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') ),
+
pa_proplist_from_string=([c_str_p], POINTER(PA_PROPLIST)),
+ pa_proplist_iterate=([POINTER(PA_PROPLIST), POINTER(c_void_p)], c_str_p),
+ pa_proplist_gets=([POINTER(PA_PROPLIST), c_str_p], c_str_p),
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) ],
@@ -614,12 +630,7 @@ class LibPulse(object):
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 ] ) )
+ pa_stream_disconnect=([POINTER(PA_STREAM)], 'int_check_ge0') )
class CallError(Exception): pass
diff --git a/pulsectl/pulsectl.py b/pulsectl/pulsectl.py
index 4c79168..d2f8bb4 100644
--- a/pulsectl/pulsectl.py
+++ b/pulsectl/pulsectl.py
@@ -578,14 +578,16 @@ class Pulse(object):
c.PA_MODULE_INFO_CB_T, c.pa.context_get_module_info_list, PulseModuleInfo )
- def _pulse_method_call(pulse_op, func=None, index_arg=True):
+ def _pulse_method_call(pulse_op, func=None, index_arg=True, self_arg=False):
'''Creates following synchronous wrapper for async pa_operation callable:
wrapper(index, ...) -> pulse_op(index, [*]args_func(...))
- index_arg=False: wrapper(...) -> pulse_op([*]args_func(...))'''
+ index_arg=False: wrapper(...) -> pulse_op([*]args_func(...))
+ self_arg=True: args_func(self, ...)'''
def _wrapper(self, *args, **kws):
if index_arg:
if 'index' in kws: index = kws.pop('index')
else: index, args = args[0], args[1:]
+ if self_arg: args = [self] + list(args)
pulse_args = func(*args, **kws) if func else list()
if not is_list(pulse_args): pulse_args = [pulse_args]
if index_arg: pulse_args = [index] + list(pulse_args)
@@ -893,6 +895,23 @@ class Pulse(object):
except c.pa.CallError as err: raise PulseOperationInvalid(err.args[-1])
+ @ft.partial(_pulse_method_call, c.pa.context_proplist_update, index_arg=False, self_arg=True)
+ def client_proplist_update(self, props_dict, mode='merge'):
+ '''Set/update/replace properties of this pulseaudio client.
+ Property keys/values will be coerced to strings. None value = remove property.
+ See/use PulseUpdateEnum for "mode" options (set, merge, replace).'''
+ proplist, keys_del, mode = list(), list(), PulseUpdateEnum[mode]._c_val
+ for k, v in props_dict.items():
+ if v is None: keys_del.append(k)
+ else: proplist.append('{}="{}"'.format(k, str(v).replace('"', r'\"')))
+ if keys_del:
+ # XXX: convert keys_del to array of pointers here
+ with self._pulse_op_cb() as cb:
+ try: c.pa.context_proplist_remove(self._ctx, keys_del, cb, None)
+ except c.pa.CallError as err: raise PulseOperationInvalid(err.args[-1])
+ return mode, c.pa.proplist_from_string('\n'.join(proplist))
+
+
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
But need to remember how to make array of pointers in ctypes...
Ended up with something like this:
def client_proplist_update(self, props_dict, mode='merge'):
'''Set/update/replace properties of this pulseaudio client.
Property keys/values will be coerced to strings. None value = remove property.
See/use PulseUpdateEnum for "mode" options (set, merge, replace).'''
proplist, keys_del, mode = list(), list(), PulseUpdateEnum[mode]._c_val
for k, v in props_dict.items():
if v is None: keys_del.append(k)
else: proplist.append('{}="{}"'.format(k, str(v).replace('"', r'\"')))
proplist = c.pa.proplist_from_string('\n'.join(proplist))
if proplist:
with self._pulse_op_cb() as cb:
try: c.pa.context_proplist_update(self._ctx, mode, proplist, cb, None)
except c.pa.CallError as err: raise PulseOperationInvalid(err.args[-1])
if keys_del:
keys_del_arr = (c.POINTER(c.c_char)*(len(keys_del)+1))()
for n, k in enumerate(keys_del):
keys_del_arr[n] = c.cast(bytes(k, 'utf-8'), c.POINTER(c.c_char))
keys_del_arr[n+1] = None
keys_del_arr = c.cast(keys_del_arr, c.POINTER(c.c_char_p))
with self._pulse_op_cb() as cb:
try: c.pa.context_proplist_remove(self._ctx, keys_del_arr, cb, None)
except c.pa.CallError as err: raise PulseOperationInvalid(err.args[-1])
But it doesn't seem to delete keys, probably because I've messed-up something in ctypes-wrapping magic for that "NULL-terminated array of char_p" argument.
Anyhow, looks like this isn't needed after all and what you want is more like pacmd update-source-proplist mic_denoised_out.monitor device.description="MySink"
, which libpulse "native" protocol can't do, likely for various security reasons (doesn't allow full access to other objects, load .so module files, control server, etc).
pulsectl.connect_to_cli
can access that privileged pacmd api, and probably good enough too for the job in this case.
At the moment, once you have a proplist for a certain device, you're unable to edit it. It would benefit my use-case to be able to rename a sink associated with a loaded module, so this PR adds the new required calls to enable that functionality.