mk-fg / python-pulse-control

Python high-level interface and ctypes-based bindings for PulseAudio (libpulse)
https://pypi.org/project/pulsectl/
MIT License
170 stars 36 forks source link

Combined sink returns as running / detecting activity #33

Closed Tank-Missile closed 4 years ago

Tank-Missile commented 5 years ago

When creating a combined sink, the sink state will return "running" even though no audio is playing back through it. This is my ~/.config/pulse/default.pa:

.include /etc/pulse/default.pa

load-module module-combine-sink sink_name=micspam_output sink_properties=device.description=Micspam
load-module module-remap-source source_name=micspam_input source_properties=device.description=Micspam master=micspam_output.monitor

The media.role of the combined sink is "filter" and its state is "DRAINED". Both appear to be undocumented, as I can't find information on either.

mk-fg commented 5 years ago

Running two commands above on pulseaudio 12.2 here, here's what I end up with in pactl list sinks and pactl list sources for this sink/source: https://gist.github.com/mk-fg/9cc616b4a67800e491ade8017910d5e5

"filter" seem to be assigned to device.class here, not media.role, but doubt it's relevant, probably due to pulse version differences. EDIT: or actually media.role seem to be assigned to streams, so kinda confirms that maybe you're not looking at a sink there (see below).

pulse.sink_list()[1].state returns PulseStateEnum.idle, I'm assuming that's the "sink state" you're referring to.

This state are defined in pulse code as:

/** Sink state. \since 0.9.15 */
typedef enum pa_sink_state { /* enum serialized in u8 */
    PA_SINK_INVALID_STATE = -1,
    /**< This state is used when the server does not support sink state introspection \since 0.9.15 */

    PA_SINK_RUNNING = 0,
    /**< Running, sink is playing and used by at least one non-corked sink-input \since 0.9.15 */

    PA_SINK_IDLE = 1,
    /**< When idle, the sink is playing but there is no non-corked sink-input attached to it \since 0.9.15 */

    PA_SINK_SUSPENDED = 2,
    /**< When suspended, actual sink access can be closed, for instance \since 0.9.15 */

... plus two server-side-only states, which it won't return anyway.

There is no drained state and afaict there never was for a sink, but there was PA_SINK_INPUT_DRAINED - note the "sink_input" part. It's a state for a sink input stream, and can never be returned for a sink (entirely different enum, won't even be allowed in C code).

I don't know where you see this exactly (maybe something other than pactl?), but think it's either something interpreting states incorrectly or in a strange way (e.g. showing input state for sink), or maybe you're looking at the wrong thing there (stream state instead of sink state).

Not entirely relevant, but note the "there was PA_SINK_INPUT_DRAINED". It won't be there in future versions, even for sink inputs, check out https://github.com/pulseaudio/pulseaudio/commit/64ba239f

mk-fg commented 5 years ago

Both appear to be undocumented, as I can't find information on either.

I think that might be due to that state being removed a year ago, which is why if you're looking at current doxygen docs, there's simply no "drained" word there anywhere. "running", "idle" and "suspended" should be there though, as comments in code snippet above should be translated to these docs directly.

Wrt "media.role" - should probably be documented, as it's one of the constant properties that get assigned internally (can be changed by apps anytime though):

/** For streams: logic role of this media. One of the strings "video", "music", "game", "event", "phone", "animation", "production", "a11y", "test" */
#define PA_PROP_MEDIA_ROLE                     "media.role"

It might be hard to lookup stuff in the docs, and I never did that for pulse tbh, since grep and cscope seem to be ideal as it is, and you can always read everything that's there in the comments + actual code.

Tank-Missile commented 5 years ago

I have pulseaudio 12.2-2.2 installed, but I was using the pacmd command and not the pactl command. Also, yes, PulseStateEnum is what I was referring to. To clear things up a bit more, here's example code I used for testing:

from pulsectl import Pulse
from pulsectl import PulseStateEnum

with Pulse() as pulse:
    for sink_input in pulse.sink_input_list():
        sink_state = pulse.sink_info(sink_input.sink).state
        sink_proplist = sink_input.proplist
        role_music = sink_proplist.get('media.role')
        if sink_state is PulseStateEnum.invalid:
            print("Audio is invalid.")
        elif sink_state is PulseStateEnum.running:
            print("Audio is playing.")
        elif sink_state is PulseStateEnum.idle:
            print("Audio is idle.")
        elif sink_state is PulseStateEnum.suspended:
            print("Audio is suspended.")

I need both the media.role and state, as I'm helping the developer of caffeine detect and distinguish audio and video playback. Currently, there's an issue where loading a combined sink causes caffeine to activate even when no audio sources are playing.

mk-fg commented 5 years ago

Ok, so to state your question as I understand it: "How do I know when any sink is transferring some sound using sink.state?"

Straightforward answer is: "You can not. It's not what that flag is for, as far as I can tell."

Tank-Missile commented 5 years ago

But doesn't "running" designate the sink is playing, or is the term "running" much broader than I initially assumed? Sorry if I'm misinterpreting what it's supposed to be used for.

mk-fg commented 5 years ago

Trying to think of how one'd detect such "some sound is playing via this sink" state in general with pulse, don't think I know the solution. I'd suggest looking at the code or maybe asking in #pulseaudio or ML for where such thing might be exposed or how to write module to detect it, if it's not.

mk-fg commented 5 years ago

But doesn't "running" designate the sink is playing, or is the term "running" much broader than I initially assumed?

I think it's not entirely correct, as I have "State: RUNNING" atm with nothing actually playing there - just sink inputs exist (incl. from other modules), and that's enough to flip it to "running", even though they are not sending any samples at all.

Maybe it's worth raising the issue with devs about documentation at least, as well as asking the broader question mentioned above.

Sorry if I'm misinterpreting what it's supposed to be used for.

It indeed seem to be confusing, but looking at that state here, I just got used to it being empirically "always running", hence me not reading these docs exactly. But it seem to be perfectly reasonable to assume that it shouldn't work like that from that "sink is playing" part of the description.

mk-fg commented 5 years ago

Trying to think of how one'd detect such "some sound is playing via this sink" state in general with pulse, don't think I know the solution.

Hm, actually, maybe some kind of suspend-on-idle module might do that already? I.e. when no samples get sent via input(s), suspends the input(s) and that flips sink into "idle" state. Though I do have it loaded here and it doesn't seem to do that...

EDIT: by "have it loaded" meant module-suspend-on-idle, but apparently it just disconnects sinks from ALSA, and has nothing to do with what I described above, so nothing weird there.

Tank-Missile commented 5 years ago

Looking into it, I think using the sink's state is a perfectly okay way to determine if audio is playing. However, any sinks with the "filter" media.role I can ignore, as all they do is process audio in various ways. I did however think of another way to detect sound, but I don't think it's available in your library currently. Some applications such as pavucontrol have what's called a "volume meter" where the sound level, sample rate, or whatever it's called is displayed in real time as the audio plays. I looked at pavucontrol and an old volume meter application called pavumeter. They both use a function called pa_stream_peek(). Here is pa_stream_peek() in pavucontrol, and here it is in pavumeter. I'm not sure if this is the correct function to use though.

mk-fg commented 5 years ago

I did however think of another way to detect sound, but I don't think it's available in your library currently. They both use a function called pa_stream_peek().

These apps seem to create monitor streams and grab samples from them to get "peak" info in their read callback.

Using that from a synchronous module like this one would look like current event-listening interface hack, and probably not very efficient for anything but such "brief check" use-case, as such callback should probably be invoked quite a bit. So maybe packing it into exactly this kind of "monitor stream peaks for N seconds" API would be a good idea, with all the flags and init steps hidden there.

Not sure if I'll get around to implementing this myself, but will leave this issue open as a reminder, if you don't mind - seems like a useful thing to have, even for control-only module like this one. Thanks for looking into this, didn't think about it earlier.

Also interesting is the use of PA_STREAM_DONT_INHIBIT_AUTO_SUSPEND on these streams - maybe that flag missing somewhere in long-running mpv streams is one of the reasons I have sink RUNNING all the time.

mk-fg commented 4 years ago

There's now a get_peak_sample() func, but not sure if it'll be useful in your particular case.

It creates a stream with PA_STREAM_PEAK_DETECT flag and blocks until specified timeout, monitoring samples there to get the the max one, returning it afterwards. sink.monitor_source index can be specified to check if there's any sound output that way, and specific stream can be passed to only monitor samples of that stream.

"Not sure" because blocking for some time like that is usually undesirable, and if timespan is very brief (to not block whatever app for too long), I think there's a good chance that it might happen to miss sound output due to coinciding with whatever gap there. Blocking module like this one isn't well-suited for such continuous operations, which would ideally be integrated into eventloop of whatever app would need to monitor for these peaks (likely asyncio with modern python), as mentioned.

Tank-Missile commented 4 years ago

The problem with blocking is that caffeine already runs on a timer to determine if an application is on the whitelist or if fullscreen is activated. Essentially, let's say the timer is 5 seconds. Monitoring for peak samples for another 5 seconds would slow down caffeine's detection of the other two checks. I see two solutions to this problem though: separate the various checks into their own threads to prevent them from blocking each other, use asyncio, or rewrite the code to not use a timer and instead use some sort of "waiting" function that only executes when a program is launched, fullscreened, etc. I'm not too familar with asyncio, and it may still produce the same problem listed above. Thank you for implementing this.

mk-fg commented 4 years ago

Yeah, asyncio loop is imo the cleanest solution for such things, but indeed there'll be a problem that this module isn't written for that, but only for simple blocking operation (running pulse eventloop on each under the hood).

There's section and link(s?) on how it can be made eventloop-friendly in the README (libpulse eventloop provides some options, and there're always threads), but ideally you'd want proper asyncio module with "async def ..." all over the place for that, not this blocking code.

mk-fg commented 4 years ago

Did actually write this module when working on https://github.com/AccelerateNetworks/PagingServer project, and person in charge of that contacted me to update it few months ago, which would involve asyncio rewrite for new sip module api there.

Remembered to write them back when implementing this earlier today (was busy with other contract at the time), so might end up making asyncio module like this one eventually, if they still need that job done at least :)

Tank-Missile commented 4 years ago

I gave get_peak_sample() a try, and it seems to be working as sinks that aren't outputting any sound (like the combined sink and remapped sink I mentioned) have a peak sample of 0. However, there seems to be a strange bug where the function never stops blocking even after the allotted timeout. Here's the steps to reproduce it: Create a combined sink and a remapped source to duplicate my configuration in default.pa. After relogging in, you'll notice from testing get_peak_sample() that the peaks for the sink and source are 0. They're detected as running for some god forsaken reason. Play audio from any application, and then stop the audio. After waiting a bit, play audio and test get_peak_sample() again. It should never stop blocking.

mk-fg commented 4 years ago

Used exact two lines that you posted in the first post to create micspam_output sink + micspam_input source via pacmd on-the-fly, then play sound via PULSE_SINK=micspam_output paplay -p test.wav. Can hear and detect it (using micspam_input index as source_idx) when playing, getting 0 when not, but nothing hanged so far.

For a more clean experiment, did:

So couldn't get it to hang with such simple test. Also tried to run all 3 sampling tests from same file with time.sleep(3) in-between, with both opening/closing libpulse connection and keeping it open, couldn't get it to hang either. Tried with both python3 and python2.

If you haven't already done so, I'd suggest confirming that it's that exact call that hangs, i.e. put some print(..., flush=True) before/after it (note - flush=True is essential for such checks), see what gets printed.

If it indeed hangs on that call, maybe lookup _pulsectl.py file installed as part of the module (run e.g. python3 -c 'import pulsectl._pulsectl as p; print(p)' to get its path), and at the bottom of it, under def _wrapper(*args):, uncomment two print() + flush lines, then run hanging code again. It should print a long list of libpulse calls - every single one that it runs, with hanging one at the bottom, presumably. Post last few dozen lines here, maybe will help to understand what's going on, even if it's configuration or version-dependent.

Tank-Missile commented 4 years ago

Here's the code I used for testing:

from pulsectl import Pulse

with Pulse() as pulse:
    for sink in pulse.sink_input_list():
        print("Before peak sample.", flush=True)
        print(pulse.get_peak_sample(sink.index, 5))
        print("After peak sample.", flush=True)

The first message is printed to console, but the second message never prints. I waited an extra while just to be sure. Here's the full output:

libpulse call: pa_mainloop_get_api (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>,)
libpulse call: pa_context_new (<pulsectl._pulsectl.LP_PA_MAINLOOP_API object at 0x7f3b85916170>, 'pulsectl')
libpulse call: pa_context_set_state_callback (<pulsectl._pulsectl.LP_PA_CONTEXT object at 0x7f3b858ad7a0>, <CFunctionType object at 0x7f3b84f966d0>, None)
libpulse call: pa_context_set_subscribe_callback (<pulsectl._pulsectl.LP_PA_CONTEXT object at 0x7f3b858ad7a0>, <CFunctionType object at 0x7f3b84f967a0>, None)
libpulse call: pa_context_connect (<pulsectl._pulsectl.LP_PA_CONTEXT object at 0x7f3b858ad7a0>, None, 0, None)
libpulse call: pa_context_get_state (<pulsectl._pulsectl.LP_PA_CONTEXT object at 0x7f3b856b7050>,)
libpulse call: pa_mainloop_iterate (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>, 1, <pulsectl._pulsectl.LP_c_int object at 0x7f3b858ad290>)
libpulse call: pa_context_get_state (<pulsectl._pulsectl.LP_PA_CONTEXT object at 0x7f3b856b7050>,)
libpulse call: pa_mainloop_iterate (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>, 1, <pulsectl._pulsectl.LP_c_int object at 0x7f3b858ad290>)
libpulse call: pa_mainloop_iterate (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>, 1, <pulsectl._pulsectl.LP_c_int object at 0x7f3b858ad290>)
libpulse call: pa_mainloop_iterate (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>, 1, <pulsectl._pulsectl.LP_c_int object at 0x7f3b858ad290>)
libpulse call: pa_mainloop_iterate (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>, 1, <pulsectl._pulsectl.LP_c_int object at 0x7f3b858ad290>)
libpulse call: pa_mainloop_iterate (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>, 1, <pulsectl._pulsectl.LP_c_int object at 0x7f3b858ad290>)
libpulse call: pa_context_get_state (<pulsectl._pulsectl.LP_PA_CONTEXT object at 0x7f3b856b7050>,)
libpulse call: pa_mainloop_iterate (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>, 1, <pulsectl._pulsectl.LP_c_int object at 0x7f3b858ad290>)
libpulse call: pa_mainloop_iterate (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>, 1, <pulsectl._pulsectl.LP_c_int object at 0x7f3b858ad290>)
libpulse call: pa_mainloop_iterate (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>, 1, <pulsectl._pulsectl.LP_c_int object at 0x7f3b858ad290>)
libpulse call: pa_mainloop_iterate (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>, 1, <pulsectl._pulsectl.LP_c_int object at 0x7f3b858ad290>)
libpulse call: pa_mainloop_iterate (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>, 1, <pulsectl._pulsectl.LP_c_int object at 0x7f3b858ad290>)
libpulse call: pa_mainloop_iterate (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>, 1, <pulsectl._pulsectl.LP_c_int object at 0x7f3b858ad290>)
libpulse call: pa_mainloop_iterate (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>, 1, <pulsectl._pulsectl.LP_c_int object at 0x7f3b858ad290>)
libpulse call: pa_mainloop_iterate (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>, 1, <pulsectl._pulsectl.LP_c_int object at 0x7f3b858ad290>)
libpulse call: pa_mainloop_iterate (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>, 1, <pulsectl._pulsectl.LP_c_int object at 0x7f3b858ad290>)
libpulse call: pa_mainloop_iterate (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>, 1, <pulsectl._pulsectl.LP_c_int object at 0x7f3b858ad290>)
libpulse call: pa_mainloop_iterate (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>, 1, <pulsectl._pulsectl.LP_c_int object at 0x7f3b858ad290>)
libpulse call: pa_mainloop_iterate (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>, 1, <pulsectl._pulsectl.LP_c_int object at 0x7f3b858ad290>)
libpulse call: pa_context_get_state (<pulsectl._pulsectl.LP_PA_CONTEXT object at 0x7f3b856b7050>,)
libpulse call: pa_context_get_sink_input_info_list (<pulsectl._pulsectl.LP_PA_CONTEXT object at 0x7f3b858ad7a0>, <CFunctionType object at 0x7f3b84f96870>, None)
libpulse call: pa_mainloop_iterate (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>, 1, <pulsectl._pulsectl.LP_c_int object at 0x7f3b858ad290>)
libpulse call: pa_mainloop_iterate (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>, 1, <pulsectl._pulsectl.LP_c_int object at 0x7f3b858ad290>)
libpulse call: pa_proplist_iterate (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b856b7830>, <cparam 'P' (0x7f3b856b7400)>)
libpulse call: pa_proplist_gets (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b856b7830>, 'media.name')
libpulse call: pa_proplist_iterate (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b856b7830>, <cparam 'P' (0x7f3b856b7400)>)
libpulse call: pa_proplist_gets (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b856b7830>, 'media.role')
libpulse call: pa_proplist_iterate (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b856b7830>, <cparam 'P' (0x7f3b856b7400)>)
libpulse call: pa_proplist_gets (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b856b7830>, 'module-stream-restore.id')
libpulse call: pa_proplist_iterate (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b856b7830>, <cparam 'P' (0x7f3b856b7400)>)
libpulse call: pa_channel_map_snprint (<ctypes.c_char_Array_513 object at 0x7f3b856b7830>, 513, <pulsectl._pulsectl.PA_CHANNEL_MAP object at 0x7f3b85761560>)
libpulse call: pa_proplist_iterate (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761950>, <cparam 'P' (0x7f3b85761880)>)
libpulse call: pa_proplist_gets (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761950>, 'media.name')
libpulse call: pa_proplist_iterate (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761950>, <cparam 'P' (0x7f3b85761880)>)
libpulse call: pa_proplist_gets (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761950>, 'media.role')
libpulse call: pa_proplist_iterate (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761950>, <cparam 'P' (0x7f3b85761880)>)
libpulse call: pa_proplist_gets (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761950>, 'module-stream-restore.id')
libpulse call: pa_proplist_iterate (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761950>, <cparam 'P' (0x7f3b85761880)>)
libpulse call: pa_channel_map_snprint (<ctypes.c_char_Array_513 object at 0x7f3b85761950>, 513, <pulsectl._pulsectl.PA_CHANNEL_MAP object at 0x7f3b85761b00>)
libpulse call: pa_proplist_iterate (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, <cparam 'P' (0x7f3b85761c70)>)
libpulse call: pa_proplist_gets (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, 'application.icon_name')
libpulse call: pa_proplist_iterate (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, <cparam 'P' (0x7f3b85761c70)>)
libpulse call: pa_proplist_gets (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, 'media.name')
libpulse call: pa_proplist_iterate (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, <cparam 'P' (0x7f3b85761c70)>)
libpulse call: pa_proplist_gets (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, 'application.name')
libpulse call: pa_proplist_iterate (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, <cparam 'P' (0x7f3b85761c70)>)
libpulse call: pa_proplist_gets (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, 'native-protocol.peer')
libpulse call: pa_proplist_iterate (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, <cparam 'P' (0x7f3b85761c70)>)
libpulse call: pa_proplist_gets (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, 'native-protocol.version')
libpulse call: pa_proplist_iterate (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, <cparam 'P' (0x7f3b85761c70)>)
libpulse call: pa_proplist_gets (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, 'application.process.id')
libpulse call: pa_proplist_iterate (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, <cparam 'P' (0x7f3b85761c70)>)
libpulse call: pa_proplist_gets (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, 'application.process.user')
libpulse call: pa_proplist_iterate (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, <cparam 'P' (0x7f3b85761c70)>)
libpulse call: pa_proplist_gets (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, 'application.process.host')
libpulse call: pa_proplist_iterate (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, <cparam 'P' (0x7f3b85761c70)>)
libpulse call: pa_proplist_gets (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, 'application.process.binary')
libpulse call: pa_proplist_iterate (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, <cparam 'P' (0x7f3b85761c70)>)
libpulse call: pa_proplist_gets (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, 'application.language')
libpulse call: pa_proplist_iterate (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, <cparam 'P' (0x7f3b85761c70)>)
libpulse call: pa_proplist_gets (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, 'window.x11.display')
libpulse call: pa_proplist_iterate (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, <cparam 'P' (0x7f3b85761c70)>)
libpulse call: pa_proplist_gets (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, 'application.process.machine_id')
libpulse call: pa_proplist_iterate (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, <cparam 'P' (0x7f3b85761c70)>)
libpulse call: pa_proplist_gets (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, 'application.process.session_id')
libpulse call: pa_proplist_iterate (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, <cparam 'P' (0x7f3b85761c70)>)
libpulse call: pa_proplist_gets (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, 'module-stream-restore.id')
libpulse call: pa_proplist_iterate (<pulsectl._pulsectl.LP_PA_PROPLIST object at 0x7f3b85761d40>, <cparam 'P' (0x7f3b85761c70)>)
libpulse call: pa_channel_map_snprint (<ctypes.c_char_Array_513 object at 0x7f3b85761d40>, 513, <pulsectl._pulsectl.PA_CHANNEL_MAP object at 0x7f3b85761e60>)
libpulse call: pa_stream_new (<pulsectl._pulsectl.LP_PA_CONTEXT object at 0x7f3b858ad7a0>, 'peak detect', <cparam 'P' (0x7f3b856b70a0)>, None)
libpulse call: pa_stream_set_read_callback (<pulsectl._pulsectl.LP_PA_STREAM object at 0x7f3b856b7170>, <CFunctionType object at 0x7f3b84f96870>, None)
libpulse call: pa_stream_connect_record (<pulsectl._pulsectl.LP_PA_STREAM object at 0x7f3b856b7170>, b'108', <pulsectl._pulsectl.PA_BUFFER_ATTR object at 0x7f3b85761c20>, 43520)
libpulse call: pa_mainloop_prepare (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>, 5000000)
libpulse call: pa_mainloop_poll (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>,)
libpulse call: pa_mainloop_dispatch (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>,)
libpulse call: pa_mainloop_prepare (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>, 4999961)
Before peak sample.
libpulse call: pa_mainloop_poll (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>,)
libpulse call: pa_mainloop_dispatch (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>,)
libpulse call: pa_mainloop_prepare (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>, 4999925)
libpulse call: pa_mainloop_poll (<pulsectl._pulsectl.LP_PA_MAINLOOP object at 0x7f3b8596c050>,)
mk-fg commented 4 years ago

print(pulse.get_peak_sample(sink.index, 5))

There's your problem, I think. Use sink.monitor_source instead of sink.index. Note that argument there is "source_idx", not "sink_idx" - these are different and opposite concepts in pulse, as you probably know. Every sink gets name=sink.monitor source associated with it though, which is the one you probably want to use, and sink.monitor_source should give you index for it. Docstring for that func should also have a (somewhat convoluted) example of using monitor_source and sink input at the same time (wanted to cram everything into one example there).

I'll check what happens if you specify non-existing source index - ideally that should raise an error, and I kinda assumed that pulse calls would do that (return values for them are checked), but maybe error-handling itself hangs somewhere...

Tank-Missile commented 4 years ago

You're right. I should have read the documentation you added. It's working fine now. This is much better than checking the state, as some applications such as virt-manager will have an audio stream open with no actual audio playing. All I have to do is check if the peak is greater than 0. Thanks for adding this! What's odd though is that pulsectl shows up as a capture stream to applications that interface with pulseaudio such as plasma-pa. This might be annoying to some users. Is there a way to hide it?

mk-fg commented 4 years ago

What's odd though is that pulsectl shows up as a capture stream to applications that interface with pulseaudio such as plasma-pa. This might be annoying to some users. Is there a way to hide it?

Hm, there doesn't seem to be any special flag for it in pa_stream_flags_t, so I'm guessing such stream also appears for pavucontrol (copied flags from there)?

There might be something that would make plasma-pa ignore the stream, like maybe a whitelist of "mixer" applications, checked either via name (which can be set in with Pulse('pavucontrol') as pulse: ...) or something in proplist, gotta check sources there. If there's no such mechanism, probably a good idea to ask for it, as it should be rather trivial to add either of the above checks to such app.

mk-fg commented 4 years ago

Yup, it does exactly this kind of whitelist-check for known mixer apps here: https://github.com/KDE/plasma-pa/blob/master/src/context.cpp#L111-L116

mk-fg commented 4 years ago

Added application.id=org.PulseAudio.pavucontrol to stream proplist (8c40579), which should probably fool plasma-pa and other apps that check for pavucontrol in the same way. Checked with pactl/pacmd - application.id is there, so should probably work.

Pushed updated version to pypi as well, let me know if there's still any issue with this, as I didn't test against plasma-pa specifically.

Tank-Missile commented 4 years ago

I can confirm that the addition of the application.id keeps the capture stream from being listed in plasma-pa. Thanks for all the hard work!