Pulstar/src/utils/pulsectl/pulsectl.py

993 lines
40 KiB
Python

# -*- coding: utf-8 -*-
from __future__ import print_function
import itertools as it, operator as op, functools as ft
from collections import defaultdict
from contextlib import contextmanager
import os, sys, inspect, traceback
from . import _pulsectl as c
if sys.version_info.major >= 3:
long, unicode = int, str
print_err = ft.partial(print, file=sys.stderr, flush=True)
def wrapper_with_sig_info(func, wrapper, index_arg=False):
sig = inspect.signature(func or (lambda: None))
if index_arg:
sig = sig.replace(parameters=[inspect.Parameter( 'index',
inspect.Parameter.POSITIONAL_OR_KEYWORD )] + list(sig.parameters.values()))
wrapper.__name__, wrapper.__signature__, wrapper.__doc__ = '', sig, func.__doc__
return wrapper
else:
range, map = xrange, it.imap
def print_err(*args, **kws):
kws.setdefault('file', sys.stderr)
print(*args, **kws)
kws['file'].flush()
def wrapper_with_sig_info(func, wrapper, index_arg=False):
func_args = list(inspect.getargspec(func or (lambda: None)))
func_args[0] = list(func_args[0])
if index_arg: func_args[0] = ['index'] + func_args[0]
wrapper.__name__ = '...'
wrapper.__doc__ = 'Signature: func' + inspect.formatargspec(*func_args)
if func.__doc__: wrapper.__doc__ += '\n\n' + func.__doc__
return wrapper
is_str = lambda v,ext=None,native=False: (
isinstance(v, ( (unicode, bytes)
if not native else (str,) ) + ((ext,) if ext else ())) )
is_str_native = ft.partial(is_str, native=True)
is_num = lambda v: isinstance(v, (int, float, long))
is_list = lambda v: isinstance(v, (tuple, list))
is_dict = lambda v: isinstance(v, dict)
def assert_pulse_object(obj):
if not isinstance(obj, PulseObject):
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):
'String-based enum value, comparable to native strings.'
__slots__ = '_t', '_value', '_c_val'
def __init__(self, t, value, c_value=None):
self._t, self._value, self._c_val = t, value, c_value
def __repr__(self): return '<EnumValue {}={}>'.format(self._t, self._value)
def __eq__(self, val):
if isinstance(val, EnumValue): val = val._value
return self._value == val
def __ne__(self, val): return not (self == val)
def __lt__(self, val):
if isinstance(val, EnumValue): val = val._value
return self._value < val
def __hash__(self): return hash(self._value)
class Enum(object):
def __init__(self, name, values_or_map):
vals = values_or_map
if is_str_native(vals): vals = vals.split()
if is_list(vals): vals = zip(it.repeat(None), vals)
if is_dict(vals): vals = vals.items()
self._name, self._values, self._c_vals = name, dict(), dict()
for c_val, k in vals:
v = EnumValue(name, k, c_val)
setattr(self, k.replace('-', '_'), v)
self._c_vals[c_val] = self._values[k] = v
def __getitem__(self, k, *default):
if isinstance(k, EnumValue):
t, k, v = k._t, k._value, k
if t != self._name: raise KeyError(v)
try: return getattr(self, k.replace('-', '_'), *default)
except AttributeError: raise KeyError(k)
def _get(self, k, default=None): return self.__getitem__(k, default)
def __contains__(self, k): return self._get(k) is not None
def _c_val(self, c_val, default=KeyError):
v = self._c_vals.get(c_val)
if v is not None: return v
if default is not KeyError:
return EnumValue(self._name, default, c_val)
raise KeyError(c_val)
def __repr__(self):
return '<Enum {} [{}]>'.format(self._name, ' '.join(sorted(self._values.keys())))
PulseEventTypeEnum = Enum('event-type', c.PA_EVENT_TYPE_MAP)
PulseEventFacilityEnum = Enum('event-facility', c.PA_EVENT_FACILITY_MAP)
PulseEventMaskEnum = Enum('event-mask', c.PA_EVENT_MASK_MAP)
PulseStateEnum = Enum('sink/source-state', c.PA_OBJ_STATE_MAP)
PulseUpdateEnum = Enum('update-type', c.PA_UPDATE_MAP)
PulsePortAvailableEnum = Enum('available', c.PA_PORT_AVAILABLE_MAP)
PulseDirectionEnum = Enum('direction', c.PA_DIRECTION_MAP)
class PulseError(Exception): pass
class PulseOperationFailed(PulseError): pass
class PulseOperationInvalid(PulseOperationFailed): pass
class PulseIndexError(PulseError): pass
class PulseLoopStop(Exception): pass
class PulseDisconnected(Exception): pass
class PulseObject(object):
c_struct_wrappers = dict()
def __init__(self, struct=None, *field_data_list, **field_data_dict):
field_data, fields = dict(), getattr(self, 'c_struct_fields', list())
if is_str_native(fields): fields = self.c_struct_fields = fields.split()
if field_data_list: field_data.update(zip(fields, field_data_list))
if field_data_dict: field_data.update(field_data_dict)
if struct is None: field_data, struct = dict(), field_data
assert not set(field_data.keys()).difference(fields)
if field_data: self._copy_struct_fields(field_data, fields=field_data.keys())
self._copy_struct_fields(struct, fields=set(fields).difference(field_data.keys()))
if struct:
if hasattr(struct, 'proplist'):
self.proplist, state = dict(), c.c_void_p()
while True:
k = c.pa.proplist_iterate(struct.proplist, c.byref(state))
if not k: break
self.proplist[c.force_str(k)] = c.force_str(c.pa.proplist_gets(struct.proplist, k))
if hasattr(struct, 'volume'):
self.volume = self._get_wrapper(PulseVolumeInfo)(struct.volume)
if hasattr(struct, 'base_volume'):
self.base_volume = struct.base_volume / c.PA_VOLUME_NORM
if hasattr(struct, 'n_ports'):
cls_port = self._get_wrapper(PulsePortInfo)
self.port_list = list(
cls_port(struct.ports[n].contents) for n in range(struct.n_ports) )
if hasattr(struct, 'active_port'):
cls_port = self._get_wrapper(PulsePortInfo)
self.port_active = (
None if not struct.active_port else cls_port(struct.active_port.contents) )
if hasattr(struct, 'channel_map'):
self.channel_count, self.channel_list = struct.channel_map.channels, list()
self.channel_list_raw = struct.channel_map.map[:self.channel_count]
if self.channel_count > 0:
s = c.create_string_buffer(b'\0' * 512)
c.pa.channel_map_snprint(s, len(s), struct.channel_map)
self.channel_list.extend(map(c.force_str, s.value.strip().split(b',')))
if hasattr(struct, 'state'):
self.state = PulseStateEnum._c_val(
struct.state, u'state.{}'.format(struct.state) )
self.state_values = sorted(PulseStateEnum._values.values())
if hasattr(struct, 'corked'): self.corked = bool(struct.corked)
self._init_from_struct(struct)
def _get_wrapper(self, cls_base):
return self.c_struct_wrappers.get(cls_base, cls_base)
def _copy_struct_fields(self, struct, fields=None, str_errors='strict'):
if not fields: fields = self.c_struct_fields
for k in fields:
setattr(self, k, c.force_str( getattr(struct, k)
if not is_dict(struct) else struct[k], str_errors ))
def _init_from_struct(self, struct): pass # to parse fields in subclasses
def _as_str(self, ext=None, fields=None, **kws):
kws = list(it.starmap('{}={}'.format, kws.items()))
if fields:
if is_str_native(fields): fields = fields.split()
kws.extend('{}={!r}'.format(k, getattr(self, k)) for k in fields)
kws = sorted(kws)
if ext: kws.append(str(ext))
return ', '.join(kws)
def __str__(self):
return self._as_str(fields=self.c_struct_fields)
def __repr__(self):
return '<{} at {:x} - {}>'.format(self.__class__.__name__, id(self), str(self))
class PulsePortInfo(PulseObject):
c_struct_fields = 'name description available priority'
def _init_from_struct(self, struct):
self.available = PulsePortAvailableEnum._c_val(struct.available)
self.available_state = self.available # for compatibility with <=17.6.0
def __eq__(self, o):
if not isinstance(o, PulsePortInfo): raise TypeError(o)
return self.name == o.name
def __hash__(self): return hash(self.name)
class PulseClientInfo(PulseObject):
c_struct_fields = 'name index driver owner_module'
class PulseServerInfo(PulseObject):
c_struct_fields = ( 'user_name host_name'
' server_version server_name default_sink_name default_source_name cookie' )
class PulseModuleInfo(PulseObject):
c_struct_fields = 'index name argument n_used auto_unload'
class PulseSinkInfo(PulseObject):
c_struct_fields = ( 'index name mute'
' description sample_spec owner_module latency driver'
' monitor_source monitor_source_name flags configured_latency card' )
def __str__(self):
return self._as_str(self.volume, fields='index name description mute')
class PulseSinkInputInfo(PulseObject):
c_struct_fields = ( 'index name mute corked client'
' owner_module sink sample_spec'
' buffer_usec sink_usec resample_method driver' )
def __str__(self):
return self._as_str(fields='index name mute')
class PulseSourceInfo(PulseObject):
c_struct_fields = ( 'index name mute'
' description sample_spec owner_module latency driver monitor_of_sink'
' monitor_of_sink_name flags configured_latency card' )
def __str__(self):
return self._as_str(self.volume, fields='index name description mute')
class PulseSourceOutputInfo(PulseObject):
c_struct_fields = ( 'index name mute corked client'
' owner_module source sample_spec'
' buffer_usec source_usec resample_method driver' )
def __str__(self):
return self._as_str(fields='index name mute')
class PulseCardProfileInfo(PulseObject):
c_struct_fields = 'name description n_sinks n_sources priority available'
class PulseCardPortInfo(PulsePortInfo):
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):
c_struct_fields = 'name index driver owner_module n_profiles'
c_struct_wrappers = {PulsePortInfo: PulseCardPortInfo}
def __init__(self, struct):
super(PulseCardInfo, self).__init__(struct)
self.profile_list = list(
PulseCardProfileInfo(struct.profiles2[n][0]) for n in range(self.n_profiles) )
self.profile_active = PulseCardProfileInfo(struct.active_profile2.contents)
def __str__(self):
return self._as_str(
fields='name index driver n_profiles',
profile_active='[{}]'.format(self.profile_active.name) )
class PulseVolumeInfo(PulseObject):
def __init__(self, struct_or_values=None, channels=None):
if is_num(struct_or_values):
assert channels is not None, 'Channel count specified if volume value is not a list.'
self.values = [struct_or_values] * channels
elif is_list(struct_or_values): self.values = struct_or_values
else:
self.values = list( (x / c.PA_VOLUME_NORM)
for x in map(float, struct_or_values.values[:struct_or_values.channels]) )
@property
def value_flat(self): return (sum(self.values) / float(len(self.values))) if self.values else 0
@value_flat.setter
def value_flat(self, v): self.values = [v] * len(self.values)
def to_struct(self):
return c.PA_CVOLUME(
len(self.values), tuple(min( c.PA_VOLUME_UI_MAX,
int(round(v * c.PA_VOLUME_NORM)) ) for v in self.values) )
def __str__(self):
return self._as_str(
channels=len(self.values), volumes='[{}]'.format(
' '.join('{}%'.format(int(round(v*100))) for v in self.values) ) )
class PulseExtStreamRestoreInfo(PulseObject):
c_struct_fields = 'name channel_map volume mute device'
@classmethod
def struct_from_value( cls, name, volume,
channel_list=None, mute=False, device=None ):
'Same arguments as with class instance init.'
chan_map = c.PA_CHANNEL_MAP()
if not channel_list: c.pa.channel_map_init_mono(chan_map)
else:
if not is_str(channel_list):
channel_list = b','.join(map(c.force_bytes, channel_list))
c.pa.channel_map_parse(chan_map, channel_list)
if not isinstance(volume, PulseVolumeInfo):
volume = PulseVolumeInfo(volume, chan_map.channels)
struct = c.PA_EXT_STREAM_RESTORE_INFO(
name=c.force_bytes(name),
mute=int(bool(mute)), device=c.force_bytes(device),
channel_map=chan_map, volume=volume.to_struct() )
return struct
def __init__( self, struct_or_name=None,
volume=None, channel_list=None, mute=False, device=None ):
'''If string name is passed instead of C struct, will be initialized from args/kws.
"volume" can be either a float number
(same level for all channels) or list (value per channel).
"channel_list" can be a pulse channel map string (comma-separated) or list
of channel names. Defaults to stereo map, should probably match volume channels.
"device" - name of sink/source or None (default).'''
if is_str(struct_or_name):
struct_or_name = self.struct_from_value(
struct_or_name, volume, channel_list, mute, device )
super(PulseExtStreamRestoreInfo, self).__init__(struct_or_name)
def to_struct(self):
return self.struct_from_value(**dict(
(k, getattr(self, k)) for k in 'name volume channel_list mute device'.split() ))
def __str__(self):
return self._as_str(self.volume, fields='name mute device')
class PulseEventInfo(PulseObject):
def __init__(self, ev_t, facility, index):
self.t, self.facility, self.index = ev_t, facility, index
def __str__(self):
return self._as_str(fields='t facility index'.split())
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.
"connect=False" can also be used here to
have control over options passed to connect() method.
"threading_lock" option (either bool or lock instance) can be used to wrap
non-threadsafe eventloop polling (can only be done from one thread at a time)
into a mutex lock, and should only be needed if same-instance methods
will/should/might be called from different threads at the same time.'''
self.name = client_name or 'pulsectl'
self.server, self.connected = server, None
self._ret = self._ctx = self._loop = self._api = None
self._actions, self._action_ids = dict(),\
it.chain.from_iterable(map(range, it.repeat(2**30)))
self.init()
if threading_lock:
if threading_lock is True:
import threading
threading_lock = threading.Lock()
self._loop_lock = threading_lock
if connect:
try: self.connect(autospawn=True)
except PulseError:
self.close()
raise
def init(self):
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(), 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_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
chan_names = dict()
for n in range(256):
name = c.pa.channel_position_to_string(n)
if name is None: break
chan_names[n] = name
self.channel_list_enum = Enum('channel_pos', chan_names)
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, timeout=None):
'''Connect to pulseaudio server.
"autospawn" option will start new pulse daemon, if necessary.
Specifying "wait" option will make function block until pulseaudio server appears.
"timeout" (in seconds) will raise PulseError if connection not established within it.'''
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
try: c.pa.context_connect(self._ctx, self.server, flags, None)
except c.pa.CallError: self.connected = False
if not timeout: # simplier process
while self.connected is None: self._pulse_iterate()
else:
self._loop_stop, delta, ts_deadline = True, 1, c.mono_time() + timeout
while self.connected is None:
delta = ts_deadline - c.mono_time()
self._pulse_poll(delta)
if delta <= 0: break
self._loop_stop = False
if not self.connected:
c.pa.context_disconnect(self._ctx)
while self.connected is not False: self._pulse_iterate()
raise PulseError('Timed-out connecting to pulseaudio server [{:,.1f}s]'.format(timeout))
if self.connected is False: raise PulseError('Failed to connect to pulseaudio server')
def disconnect(self):
if not self._ctx or not self.connected: return
c.pa.context_disconnect(self._ctx)
def close(self):
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
def __enter__(self): return self
def __exit__(self, err_t, err, err_tb): self.close()
def _pulse_state_cb(self, ctx, userdata):
state = c.pa.context_get_state(ctx)
if state >= c.PA_CONTEXT_READY:
if state == c.PA_CONTEXT_READY: self.connected = True
elif state in [c.PA_CONTEXT_FAILED, c.PA_CONTEXT_TERMINATED]:
self.connected, self._loop_stop = False, True
def _pulse_subscribe_cb(self, ctx, ev, idx, userdata):
if not self.event_callback: return
n = ev & c.PA_SUBSCRIPTION_EVENT_FACILITY_MASK
ev_fac = PulseEventFacilityEnum._c_val(n, 'ev.facility.{}'.format(n))
n = ev & c.PA_SUBSCRIPTION_EVENT_TYPE_MASK
ev_t = PulseEventTypeEnum._c_val(n, 'ev.type.{}'.format(n))
try: self.event_callback(PulseEventInfo(ev_t, ev_fac, idx))
except PulseLoopStop: self._loop_stop = True
def _pulse_poll_cb(self, func, func_err, ufds, nfds, timeout, userdata):
fd_list = list(ufds[n] for n in range(nfds))
try: nfds = func(fd_list, timeout / 1000.0)
except Exception as err:
func_err(*sys.exc_info())
return -1
return nfds
@contextmanager
def _pulse_loop(self):
with self._loop_lock:
if not self._loop: return
if self._loop_running:
raise PulseError(
'Running blocking pulse operations from pulse eventloop callbacks'
' or other threads while loop is running is not supported by this python module.'
' Supporting this would require threads or proper asyncio/twisted-like async code.'
' Workaround can be to stop the loop'
' (raise PulseLoopStop in callback or event_loop_stop() from another thread),'
' doing whatever pulse calls synchronously and then resuming event_listen() loop.' )
self._loop_running, self._loop_stop = True, False
try: yield self._loop
finally:
self._loop_running = False
if self._loop_closed: self.close() # to free() after stopping it
def _pulse_run(self):
with self._pulse_loop() as loop: c.pa.mainloop_run(loop, self._ret)
def _pulse_iterate(self, block=True):
with self._pulse_loop() as loop: c.pa.mainloop_iterate(loop, int(block), self._ret)
@contextmanager
def _pulse_op_cb(self, raw=False):
act_id = next(self._action_ids)
self._actions[act_id] = None
try:
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.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)
def _pulse_poll(self, timeout=None):
'''timeout should be in seconds (float),
0 for non-blocking poll and None (default) for no timeout.'''
with self._pulse_loop() as loop:
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
c.pa.mainloop_prepare(loop, delay) # delay in us
c.pa.mainloop_poll(loop)
if self._loop_closed: break # interrupted by close() or such
c.pa.mainloop_dispatch(loop)
if self._loop_stop: break
ts = c.mono_time()
if ts_deadline and ts >= ts_deadline: break
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]))
def _pulse_get_list(cb_t, pulse_func, info_cls, singleton=False, index_arg=True):
def _wrapper_method(self, index=None):
data = list()
with self._pulse_op_cb(raw=True) as cb:
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() )
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)
data, = data
return data
return wrapper_with_sig_info( None, _wrapper_method,
not (pulse_func.__name__.endswith('_list') or singleton or not index_arg) )
get_sink_by_name = _pulse_get_list(
c.PA_SINK_INFO_CB_T,
c.pa.context_get_sink_info_by_name, PulseSinkInfo )
get_source_by_name = _pulse_get_list(
c.PA_SOURCE_INFO_CB_T,
c.pa.context_get_source_info_by_name, PulseSourceInfo )
get_card_by_name = _pulse_get_list(
c.PA_CARD_INFO_CB_T,
c.pa.context_get_card_info_by_name, PulseCardInfo )
sink_input_list = _pulse_get_list(
c.PA_SINK_INPUT_INFO_CB_T,
c.pa.context_get_sink_input_info_list, PulseSinkInputInfo )
sink_input_info = _pulse_get_list(
c.PA_SINK_INPUT_INFO_CB_T,
c.pa.context_get_sink_input_info, PulseSinkInputInfo )
source_output_list = _pulse_get_list(
c.PA_SOURCE_OUTPUT_INFO_CB_T,
c.pa.context_get_source_output_info_list, PulseSourceOutputInfo )
source_output_info = _pulse_get_list(
c.PA_SOURCE_OUTPUT_INFO_CB_T,
c.pa.context_get_source_output_info, PulseSourceOutputInfo )
sink_list = _pulse_get_list(
c.PA_SINK_INFO_CB_T, c.pa.context_get_sink_info_list, PulseSinkInfo )
sink_info = _pulse_get_list(
c.PA_SINK_INFO_CB_T, c.pa.context_get_sink_info_by_index, PulseSinkInfo )
source_list = _pulse_get_list(
c.PA_SOURCE_INFO_CB_T, c.pa.context_get_source_info_list, PulseSourceInfo )
source_info = _pulse_get_list(
c.PA_SOURCE_INFO_CB_T, c.pa.context_get_source_info_by_index, PulseSourceInfo )
card_list = _pulse_get_list(
c.PA_CARD_INFO_CB_T, c.pa.context_get_card_info_list, PulseCardInfo )
card_info = _pulse_get_list(
c.PA_CARD_INFO_CB_T, c.pa.context_get_card_info_by_index, PulseCardInfo )
client_list = _pulse_get_list(
c.PA_CLIENT_INFO_CB_T, c.pa.context_get_client_info_list, PulseClientInfo )
client_info = _pulse_get_list(
c.PA_CLIENT_INFO_CB_T, c.pa.context_get_client_info, PulseClientInfo )
server_info = _pulse_get_list(
c.PA_SERVER_INFO_CB_T, c.pa.context_get_server_info, PulseServerInfo, singleton=True )
module_info = _pulse_get_list(
c.PA_MODULE_INFO_CB_T, c.pa.context_get_module_info, PulseModuleInfo )
module_list = _pulse_get_list(
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):
'''Creates following synchronous wrapper for async pa_operation callable:
wrapper(index, ...) -> pulse_op(index, [*]args_func(...))
index_arg=False: wrapper(...) -> pulse_op([*]args_func(...))'''
def _wrapper(self, *args, **kws):
if index_arg:
if 'index' in kws: index = kws.pop('index')
else: index, args = args[0], args[1:]
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)
with self._pulse_op_cb() as cb:
try: pulse_op(self._ctx, *(list(pulse_args) + [cb, None]))
except c.ArgumentError as err: raise TypeError(err.args)
except c.pa.CallError as err: raise PulseOperationInvalid(err.args[-1])
return wrapper_with_sig_info(func, _wrapper, index_arg)
card_profile_set_by_index = _pulse_method_call(
c.pa.context_set_card_profile_by_index, lambda profile_name: profile_name )
sink_default_set = _pulse_method_call(
c.pa.context_set_default_sink, index_arg=False,
func=lambda sink: sink.name if isinstance(sink, PulseSinkInfo) else sink )
source_default_set = _pulse_method_call(
c.pa.context_set_default_source, index_arg=False,
func=lambda source: source.name if isinstance(source, PulseSourceInfo) else source )
sink_input_mute = _pulse_method_call(
c.pa.context_set_sink_input_mute, lambda mute=True: mute )
sink_input_move = _pulse_method_call(
c.pa.context_move_sink_input_by_index, lambda sink_index: sink_index )
sink_mute = _pulse_method_call(
c.pa.context_set_sink_mute_by_index, lambda mute=True: mute )
sink_input_volume_set = _pulse_method_call(
c.pa.context_set_sink_input_volume, lambda vol: vol.to_struct() )
sink_volume_set = _pulse_method_call(
c.pa.context_set_sink_volume_by_index, lambda vol: vol.to_struct() )
sink_suspend = _pulse_method_call(
c.pa.context_suspend_sink_by_index, lambda suspend=True: suspend )
sink_port_set = _pulse_method_call(
c.pa.context_set_sink_port_by_index,
lambda port: port.name if isinstance(port, PulsePortInfo) else port )
source_output_mute = _pulse_method_call(
c.pa.context_set_source_output_mute, lambda mute=True: mute )
source_output_move = _pulse_method_call(
c.pa.context_move_source_output_by_index, lambda sink_index: sink_index )
source_mute = _pulse_method_call(
c.pa.context_set_source_mute_by_index, lambda mute=True: mute )
source_output_volume_set = _pulse_method_call(
c.pa.context_set_source_output_volume, lambda vol: vol.to_struct() )
source_volume_set = _pulse_method_call(
c.pa.context_set_source_volume_by_index, lambda vol: vol.to_struct() )
source_suspend = _pulse_method_call(
c.pa.context_suspend_source_by_index, lambda suspend=True: suspend )
source_port_set = _pulse_method_call(
c.pa.context_set_source_port_by_index,
lambda port: port.name if isinstance(port, PulsePortInfo) else port )
def module_load(self, name, args=''):
if is_list(args): args = ' '.join(args)
name, args = map(c.force_bytes, [name, args])
data = list()
with self._pulse_op_cb(raw=True) as cb:
cb = c.PA_CONTEXT_INDEX_CB_T(
lambda ctx, index, userdata, cb=cb: data.append(index) or cb() )
try: c.pa.context_load_module(self._ctx, name, args, cb, None)
except c.pa.CallError as err: raise PulseOperationInvalid(err.args[-1])
index, = data
if index == c.PA_INVALID:
raise PulseError('Failed to load module: {} {}'.format(name, args))
return index
module_unload = _pulse_method_call(c.pa.context_unload_module, None)
def stream_restore_test(self):
'Returns module-stream-restore version int (e.g. 1) or None if it is unavailable.'
data = list()
with self._pulse_op_cb(raw=True) as cb:
cb = c.PA_EXT_STREAM_RESTORE_TEST_CB_T(
lambda ctx, version, userdata, cb=cb: data.append(version) or cb() )
try: c.pa.ext_stream_restore_test(self._ctx, cb, None)
except c.pa.CallError as err: raise PulseOperationInvalid(err.args[-1])
version, = data
return version if version != c.PA_INVALID else None
stream_restore_read = _pulse_get_list(
c.PA_EXT_STREAM_RESTORE_READ_CB_T,
c.pa.ext_stream_restore_read, PulseExtStreamRestoreInfo, index_arg=False )
stream_restore_list = stream_restore_read # for consistency with other *_list methods
@ft.partial(_pulse_method_call, c.pa.ext_stream_restore_write, index_arg=False)
def stream_restore_write( obj_name_or_list,
mode='merge', apply_immediately=False, **obj_kws ):
'''Update module-stream-restore db entry for specified name.
Can be passed PulseExtStreamRestoreInfo object or list of them as argument,
or name string there and object init keywords (e.g. volume, mute, channel_list, etc).
"mode" is PulseUpdateEnum value of
'merge' (default), 'replace' or 'set' (replaces ALL entries!!!).'''
mode = PulseUpdateEnum[mode]._c_val
if is_str(obj_name_or_list):
obj_name_or_list = PulseExtStreamRestoreInfo(obj_name_or_list, **obj_kws)
if isinstance(obj_name_or_list, PulseExtStreamRestoreInfo):
obj_name_or_list = [obj_name_or_list]
# obj_array is an array of structs, laid out contiguously in memory, not pointers
obj_array = (c.PA_EXT_STREAM_RESTORE_INFO * len(obj_name_or_list))()
for n, obj in enumerate(obj_name_or_list):
obj_struct, dst_struct = obj.to_struct(), obj_array[n]
for k,t in obj_struct._fields_: setattr(dst_struct, k, getattr(obj_struct, k))
return mode, obj_array, len(obj_array), int(bool(apply_immediately))
@ft.partial(_pulse_method_call, c.pa.ext_stream_restore_delete, index_arg=False)
def stream_restore_delete(obj_name_or_list):
'''Can be passed string name,
PulseExtStreamRestoreInfo object or a list of any of these.'''
if is_str(obj_name_or_list, PulseExtStreamRestoreInfo):
obj_name_or_list = [obj_name_or_list]
name_list = list((obj.name if isinstance( obj,
PulseExtStreamRestoreInfo ) else obj) for obj in obj_name_or_list)
name_struct = (c.c_char_p * len(name_list))()
name_struct[:] = list(map(c.force_bytes, name_list))
return [name_struct]
def default_set(self, obj):
'Set passed sink or source to be used as default one by pulseaudio server.'
assert_pulse_object(obj)
method = {
PulseSinkInfo: self.sink_default_set,
PulseSourceInfo: self.source_default_set }.get(type(obj))
if not method: raise NotImplementedError(type(obj))
method(obj)
def mute(self, obj, mute=True):
assert_pulse_object(obj)
method = {
PulseSinkInfo: self.sink_mute,
PulseSinkInputInfo: self.sink_input_mute,
PulseSourceInfo: self.source_mute,
PulseSourceOutputInfo: self.source_output_mute }.get(type(obj))
if not method: raise NotImplementedError(type(obj))
method(obj.index, mute)
obj.mute = mute
def port_set(self, obj, port):
assert_pulse_object(obj)
method = {
PulseSinkInfo: self.sink_port_set,
PulseSourceInfo: self.source_port_set }.get(type(obj))
if not method: raise NotImplementedError(type(obj))
method(obj.index, port)
obj.port_active = port
def card_profile_set(self, card, profile):
assert_pulse_object(card)
if is_str(profile):
profile_dict = dict((p.name, p) for p in card.profile_list)
if profile not in profile_dict:
raise PulseIndexError( 'Card does not have'
' profile with specified name: {!r}'.format(profile) )
profile = profile_dict[profile]
self.card_profile_set_by_index(card.index, profile.name)
card.profile_active = profile
def volume_set(self, obj, vol):
assert_pulse_object(obj)
method = {
PulseSinkInfo: self.sink_volume_set,
PulseSinkInputInfo: self.sink_input_volume_set,
PulseSourceInfo: self.source_volume_set,
PulseSourceOutputInfo: self.source_output_volume_set }.get(type(obj))
if not method: raise NotImplementedError(type(obj))
method(obj.index, vol)
obj.volume = vol
def volume_set_all_chans(self, obj, vol):
assert_pulse_object(obj)
obj.volume.value_flat = vol
self.volume_set(obj, obj.volume)
def volume_change_all_chans(self, obj, inc):
assert_pulse_object(obj)
obj.volume.values = [max(0, v + inc) for v in obj.volume.values]
self.volume_set(obj, obj.volume)
def volume_get_all_chans(self, obj):
# Purpose of this func can be a bit confusing, being here next to set/change ones
'''Get "flat" volume float value for info-object as a mean of all channel values.
Note that this DOES NOT query any kind of updated values from libpulse,
and simply returns value(s) stored in passed object, i.e. same ones for same object.'''
assert_pulse_object(obj)
return obj.volume.value_flat
def event_mask_set(self, *masks):
mask = 0
for m in masks: mask |= PulseEventMaskEnum[m]._c_val
with self._pulse_op_cb() as cb:
c.pa.context_subscribe(self._ctx, mask, cb, None)
def event_callback_set(self, func):
'''Call event_listen() to start receiving these,
and be sure to raise PulseLoopStop in a callback to stop the loop.
Callback should accept single argument - PulseEventInfo instance.
Passing None will disable the thing.'''
self.event_callback = func
def event_listen(self, timeout=None, raise_on_disconnect=True):
'''Does not return until PulseLoopStop
gets raised in event callback or timeout passes.
timeout should be in seconds (float),
0 for non-blocking poll and None (default) for no timeout.
raise_on_disconnect causes PulseDisconnected exceptions by default.
Do not run any pulse operations from these callbacks.'''
assert self.event_callback
try: self._pulse_poll(timeout)
except c.pa.CallError: pass # e.g. from mainloop_dispatch() on disconnect
if raise_on_disconnect and not self.connected: raise PulseDisconnected()
def event_listen_stop(self):
'''Stop event_listen() loop from e.g. another thread.
Does nothing if libpulse poll is not running yet, so might be racey with
event_listen() - be sure to call it in a loop until event_listen returns or something.'''
self._loop_stop = True
c.pa.mainloop_wakeup(self._loop)
def set_poll_func(self, func, func_err_handler=None):
'''Can be used to integrate pulse client into existing eventloop.
Function will be passed a list of pollfd structs and timeout value (seconds, float),
which it is responsible to use and modify (set poll flags) accordingly,
returning int value >= 0 with number of fds that had any new events within timeout.
func_err_handler defaults to traceback.print_exception(),
and will be called on any exceptions from callback (to e.g. log these),
returning poll error code (-1) to libpulse after that.'''
if not func_err_handler: func_err_handler = traceback.print_exception
self._pa_poll_cb = c.PA_POLL_FUNC_T(ft.partial(self._pulse_poll_cb, func, func_err_handler))
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_FLOAT32NE 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_FLOAT32NE, 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
c.pa.stream_unref(s)
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
or pulseaudio startup files (e.g. "default.pa").
"server" option can be specified to use non-standard unix socket path
(when passed absolute path string) or remote tcp socket,
when passed remote host address (to use default port) or (host, port) tuple.
Be sure to adjust "socket_timeout" option for tcp sockets over laggy internet.
Returned file object has line-buffered output,
so there should be no need to use flush() after every command.
Be sure to read from the socket line-by-line until
"### EOF" or timeout for commands that have output (e.g. "dump\\n").
If default server socket is used (i.e. not specified),
server pid will be signaled to load module-cli between connection attempts.
Completely separate protocol from the regular API, as wrapped by libpulse.
PulseError is raised on any failure.'''
import socket, errno, signal, time
s, n = None, attempts if attempts > 0 else None
try:
pid_path, sock_af, sock_t = None, socket.AF_UNIX, socket.SOCK_STREAM
if not server: server, pid_path = map(c.pa.runtime_path, ['cli', 'pid'])
else:
if not is_list(server):
server = c.force_str(server)
if not server.startswith('/'): server = server, 4712 # default port
if is_list(server):
try:
addrinfo = socket.getaddrinfo(
server[0], server[1], 0, sock_t, socket.IPPROTO_TCP )
if not addrinfo: raise socket.gaierror('No addrinfo for socket: {}'.format(server))
except (socket.gaierror, socket.error) as err:
raise PulseError( 'Failed to resolve socket parameters'
' (address, family) via getaddrinfo: {!r} - {} {}'.format(server, type(err), err) )
sock_af, sock_t, _, _, server = addrinfo[0]
s = socket.socket(sock_af, sock_t)
s.settimeout(socket_timeout)
while True:
ts = c.mono_time()
try: s.connect(server)
except socket.error as err:
if err.errno not in [errno.ECONNREFUSED, errno.ENOENT, errno.ECONNABORTED]: raise
else: break
if n:
n -= 1
if n <= 0: raise PulseError('Number of connection attempts ({}) exceeded'.format(attempts))
if pid_path:
with open(pid_path) as src: os.kill(int(src.read().strip()), signal.SIGUSR2)
time.sleep(max(0, retry_delay - (c.mono_time() - ts)))
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'
' cli socket {!r}: {} {}'.format(server, type(err), err) )
finally:
if s: s.close()