Closed ulno closed 4 years ago
Hi,
Yeah, can totally see why it'd look like volume_get_all_chans would be fetching the value, as it's right next to volume_set functions, which actually go to libpulse and change stuff.
But it is defined simply as:
def volume_get_all_chans(self, obj):
assert_pulse_object(obj)
return obj.volume.value_flat
I.e. polling it would be about same as doing something like volume = get_volume(); print(volume); sleep(5); print(volume); ...
- i.e. just reusing same stored value.
You'd need to do something like pulse.sink_input_info(...)
to get current sink input parameters.
This might illustrate it better with pulsectl:
from pulsectl.lookup import pulse_obj_lookup
import os, sys, pulsectl, signal, functools as ft
signal.signal(signal.SIGINT, lambda sig,frm: sys.exit(0))
def callback(mpv, ev):
if ev.index == mpv.index:
raise pulsectl.PulseLoopStop
with pulsectl.Pulse() as pulse:
mpv, = pulse_obj_lookup(pulse, 'media.role:music')
pulse.event_mask_set(pulsectl.PulseEventMaskEnum.sink_input)
pulse.event_callback_set(ft.partial(callback, mpv))
while True:
pulse.event_listen()
print('----------')
# Printing same value from same exact struct
print('Initial value (always same):', pulse.volume_get_all_chans(mpv))
print('Initial value (also same thing):', mpv.volume.value_flat)
# Getting new SinkInputInfo object with updated value
print('Actual value:', pulse.sink_input_info(mpv.index).volume.value_flat)
Last line actually gets new sink input info struct from libpulse with updated values, while first two prints just dump same value from same pre-fetched struct in a loop.
One quirk of this particular use-case, with polling value based on events, is that it's an eventloop (think async/await and asyncio), and this module is written to wrap underlying libpulse eventloop (glib) into blocking API, and can't easily switch back.
Which is why there's raise pulsectl.PulseLoopStop
in above example, as you can't easily run libpulse calls from within event handler itself with it.
(you can cheat in various ways however, e.g. have with pulsectl.Pulse() as pulse_events, pulsectl.Pulse() as pulse:
and run event_listen on one connection, while doing anything on the other one from within callbacks - object indexes are same/global)
Guess I'll at least add comment on pulse.volume_get_all_chans(...)
to say that it's not actually querying anything, thanks for raising the issue.
Thanks for the solution - this is quite something to wrap your head around comparing it to a bash script using pamixer.
I was taking a look at pyaudio and wow, I hadn't thought that audio in Python is in such a bad state - guess I have mainly used audio so far in pygame. I was hoping to use something that abstracts a little bit of the system-grit away, so I should probably blame general audio handling complexity for this, but still wondering if it wouldn't be possible to write some kind of (even if it's threaded) sound abstraction layer that allowed direct interaction with mixers and sound cards.
However, this should probably be discussed somewhere else.
The example just hangs in event_listen() even if volume gets changed.
If I give event_listen a timeout, the example fails with an PulseIndexError exception.
Is there any disadvantage in just calling sink_info each time before I want to have a current volume value? That seems to work.
That example also looks up mpv, = pulse_obj_lookup(pulse, 'media.role:music')
and handles events only from that, you probably don't have it running on your system.
I changed that line to a local device:
from pulsectl.lookup import pulse_obj_lookup
import os, sys, pulsectl, signal, functools as ft
signal.signal(signal.SIGINT, lambda sig,frm: sys.exit(0))
def callback(mpv, ev):
if ev.index == mpv.index:
raise pulsectl.PulseLoopStop
with pulsectl.Pulse() as pulse:
mpv, = pulse_obj_lookup(pulse, 'sink:device.description::Altec')
print("mpv:", mpv)
pulse.event_mask_set(pulsectl.PulseEventMaskEnum.sink_input)
pulse.event_callback_set(ft.partial(callback, mpv))
while True:
pulse.event_listen(1)
print('----------')
# Printing same value from same exact struct
print('Initial value (always same):', pulse.volume_get_all_chans(mpv))
print('Initial value (also same thing):', mpv.volume.value_flat)
# Getting new SinkInputInfo object with updated value
print('Actual value:', pulse.sink_input_info(mpv.index).volume.value_flat)
crashes now after 1s with:
pv: description='Altec Lansing XT2 - USB Audio Analog Stereo', index=70, mute=0, name='alsa_output.usb-Altec_Lansing_Technologies__Inc._Altec_Lansing_XT2_-_USB_Audio-00.analog-stereo', channels=2, volumes=[78% 78%]
----------
Initial value (always same): 0.780914306640625
Initial value (also same thing): 0.780914306640625
Traceback (most recent call last):
File "pulsectl_test.py", line 25, in <module>
print('Actual value:', pulse.sink_input_info(mpv.index).volume.value_flat)
File "/home/ulno/sync/work/devel/iotempower/.local/vp/lib/python3.8/site-packages/pulsectl/pulsectl.py", line 536, in _decorator_or_method
return _wrapper(func_or_self, index)
File "/home/ulno/sync/work/devel/iotempower/.local/vp/lib/python3.8/site-packages/pulsectl/pulsectl.py", line 526, in _wrapper
if not data: raise PulseIndexError(index)
pulsectl.pulsectl.PulseIndexError: 70
I was hoping to use something that abstracts a little bit of the system-grit away, so I should probably blame general audio handling complexity for this
Maybe it's just overcomplicated example that I've used above?
I mean, if you want something simple like "get volume of some random sink", something like first example from the README should work:
with pulsectl.Pulse() as pulse: pulse.volume_get_all_chans(0)
Seem to be simple enough.
Wrt this line too:
mpv, = pulse_obj_lookup(pulse, 'sink:device.description::Altec')
Probably shouldn't have used it in example above, as it's kinda-private API which I've added in response to some Issue here (which you can probably lookup), mostly to demonstrate how complicated "way to get sink by string" might be :)
It's not really intended to be used by anyone.
Yep, that was why I asked above if just calling sink_info before evaluating volume would have any busy-waiting side effects? So, I might not need the example if this way of polling would be ok. It seems to take here less than 1% of cpu load.
Actual problem with code you pasted above is here:
mpv, = pulse_obj_lookup(pulse, 'sink:device.description::Altec')
...
pulse.event_mask_set(pulsectl.PulseEventMaskEnum.sink_input)
...
print('Actual value:', pulse.sink_input_info(mpv.index).volume.value_flat)
You're getting a SINK object, and then using its index to lookup SINK INPUTs (well, lookup or filter events by, with latter probably being cause for "hang" - nothing matches). In my original example "mpv" (popular media player) is not a sink, but a stream from a background music player.
Ah, that mpv (I was wondering about the acronym) <- I actually do try to control the volume of machines where it plays here ;-)
Thanks for the help, I think I have a solution, I will link my code example here when done (need to fix the mqtt loop still).
Yep, that was why I asked above if just calling sink_info before evaluating volume would have any busy-waiting side effects?
Don't think I understand the question.
If you want to do something like while True: print(pulse.sink_info(...).volume); sleep(0.1)
- can just run and check how it'd impact CPU.
Ideally it'd just be libpulse making a command, sending it over unix socket, pulse server handling it and then parsing the response in client - not too heavy.
And there's no real "busy-wait" wrt socket layer - module/libpulse will run this exchange in a glib eventloop, though guess you mean something else there.
Thought to also comment on this thing I said above:
mostly to demonstrate how complicated "way to get sink by string" might be
You can totally get sinks/sources by name without going to that crazy lookup api, kinda like one you've used above, e.g. have "alsa_output.pci-0000_00_14.2.analog-stereo" for built-in Intel HDA here.
These names should be unique and probably good enough for "something to put into config file to ID sound sink" purposes.
Works now, thanks again.
That's what I wanted to accomplish: https://github.com/iotempire/iotempower/blob/master/examples/integriot/volume/pulse_mqtt_volume.py
Hope it's not a too embarrassing use of your library.
Eh, if it works, it works :) I think using events in one of ways suggested above (e.g. like in example) might be just as much code as comparing values though.
Oh, also, I think you have some dead code there, and some typos like "integriot" (maybe a fancy new word though) :)
some typos like "integriot" (maybe a fancy new word though)
Nah, it's a project name, but "nad" after it definitely a typo.
fixed typo - integriot is a merge between integrate and iot, I wanted something simpler than paho for my students to build small integrations of mqtt components, but this is slowly going off topic, guess I have cluttered this issue now enough.
The volume control over mqtt from a node-red network now works perfectly both for setting and reporting the volume thanks to pulsectl.
I am developing a small mqtt volume controller for various Linux based satellite speakers in my home. I wanted to use a simple polling mechanism checking with pulse.volume_get_all_chans(sink) for the volume and posting this then via mqtt to my network to show on a dashboard.
Unfortunately, pulse.volume_get_all_chans(sink) always returns the same value (also when I change the volumes with pavucontrol) - I seem to be missing some update calls. I do get events when registering a handler with event_callback_set, however the values in volume_get_all_chans still stay the same. Only when restarting my program, I get once an updated volume value.
Is this the expected behavior? How would I get up-to-date values (like when calling pamixer from the cli).