-
Notifications
You must be signed in to change notification settings - Fork 257
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PulseAudio support for volume input #7
Conversation
Before we dive into any of the details of the pull request itself, can we make it so that PulseAudio is automatically queried when it’s in use on the user’s system? The current ALSA implementation faills back to PulseAudio, but rather poorly, see #4 |
You mean if the user does not specify a device? Now that you mention it, it makes sense to try the default sink in this case and, in case of failure, fall back to ALSA. I'll try that. |
Ok, now PulseAudio is tried first, then ALSA. Also, now the default sink is fetched and used if not explicitly specified. If the device specified is anything other than "pulse" or "default" (or missing), then PulseAudio is not used even if it's available. |
Thanks for making that change. I compiled and ran this, but it seems like even when using libpulse, there still is a fork() involved every second in order to get the volume:
Is that because of how this code uses the library or is it inherent to how the library works? Also I’m wondering how libasound can avoid being linked against all these dependencies that libpulse brings in but still fall back to query pulseaudio for volume. An alternative could be using dbus, but it seems like PulseAudio is shipped with the dbus module disabled on plenty of linux distributions :(. I haven’t looked into it in detail, but would it be feasible to directly talk to PulseAudio without using libpulse? |
That's odd. I tested that here and I see no forking. Perhaps it's falling back to ALSA for some reason in your system? How is your device= line in i3status.conf? Make sure it's either unset or set to "default" or "pulse". Also make sure that you compile with One quick way to see if it's using PulseAudio is by setting the sink volume to above 100% (e.g. with pavucontrol). It will only show values above 100% if using PulseAudio, but not with ALSA. |
Should I change the default to building with PulseAudio support and, subsequently, the make flag from |
The system in question is indeed using PulseAudio. After repeating and making sure I compile with I’ve done some digging in the PulseAudio API, and have two high-level comments:
With regards to building: please just remove the conditionals entirely and make libpulse a regular dependency. These days PulseAudio is wide-spread, and having it optional will be more of a headache than it buys us. Also, can you rebase your commits on top of master and squash your history? I.e., I’m not particularly interested in your “clang-format” commit :). By the way, I think it’d be worth a shot to add a function to get the volume (or the sink info in general) to libpulse’s simple API. That way, we could eventually switch to just using their simple API, as we are basically replicating it (after going the route I suggested). Is that something you’d want to do? |
Nice catch, I completely missed that one. Done, and indeed, got rid of two callbacks.
Done. The added dependency seems to be failing the automated builds, though.
Sorry about that, it's squashed now.
Originally I considered using pa_threaded_mainloop but I had doubts about the proper way to use it and decided to go the safe way. In particular, I thought of subscribing to volume changes from the sink and waking the main thread upon volume changes so that a refresh was issued when required. I don't know if this behaviour is always desirable, hence my doubt (maybe something configurable?). I can try this direction if you think it's ok.
I think if we subscribe to volume changes like I've suggested then the simple API could not be used. |
Ah, I didn’t see that one can subscribe to volume changes. That’d be the most elegant solution, I think (subscribing, modifying a variable when the volume changes, and just returning that variable in each loop iteration). I don’t think we should make this configurable, because why would you not want it to work that way? |
I meant waking the main thread so that i3status would print the new volume immediately. Currently I have something like this in my .i3/config for changing the PulseAudio volume from the keyboard:
The |
Even tweaking the main thread to wake up is something I’d be willing to do without a configuration option, as long as the volume change event doesn’t trigger when it shouldn’t. |
Ok, converted it to a threaded mainloop plus waking of the main thread when the volume changes. Seems to be working quite well. |
* index of the PulseAudio sink then force PulseAudio, optionally | ||
* overriding the default sink */ | ||
|
||
if (device[0] == 'p' && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use strncasecmp(device, "pulse", strlen("pulse"))
instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
The default sink is now kept updated by using a server change subscription. I tested here flipping the default sink between two devices and it updates immediately, alternating the display of each of the device's current volume. The static array for volume storage was replaced by a dynamically allocated array of struct. There's no longer a hard limit on the number of sinks supported. |
|
||
if (idx < 0) /* we haven't seen this index before, store it */ | ||
{ | ||
if (used_cached_volume_elements * sizeof(*cached_volume) >= |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of doing all this reallocation yourself, why not use the TAILQ
macros I suggested?
Thanks for all the work you’re putting into this. I feel like we got the functionality mostly nailed and are dealing only with code questions now. As a heads-up, I’ll also want to see whether we can use http://clang.llvm.org/docs/ThreadSafetyAnalysis.html in order to make sure our mutex-related code is correct. |
OK, I think all of the issues you pointed out were taken care of. The value to signify the default sink changed from -1 to UINT32_MAX and the type from int to uint32_t. About TAILQ, I figured since it was ok to traverse the list of sinks to read a volume from the index, we might as well have one contiguous block which would be tighter than several tiny malloc'd blocks with the extra overhead of a doubly linked list. Of course we are talking about a few bytes here, the point being the realloc method doesn't seem more complicated to me than using TAILQ. Very interesting and useful the Clang thread safety analysis feature. The page only shows C++ examples though, but I suppose there must be ways to make it work with C as well? |
} | ||
} | ||
|
||
static int find_sink_idx(uint32_t sink_idx) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You’re not acquiring the pulse_mutex here, but in other places when accessing cached_volume.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code is correct, though. I've added a comment stating the mutex should be locked prior to calling find_sink_idx().
We’re talking about typically less than 10 mallocs. I don’t think it’s worth worrying about that, and I think using a TAILQ would make the code easier. Also note that modern libcs don’t translate every malloc() into an actual allocation, but have internal buffers. I really don’t think the memory layout is something that should concern us in such a case. |
See https://llvm.org/bugs/show_bug.cgi?id=20403 |
TAILQ it is, then. |
int pulse_initialize(void) { | ||
if (!initialized) { | ||
initialized = 1; | ||
TAILQ_INIT(&cached_volume); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you initialize the tailq again? This line is equivalent to what the TAILQ_HEAD_INITIALIZER does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, my mistake. Fixed.
Thanks for your continued patience on this. I think in one or two rounds we’ll end up with high-quality code that’s consistent with the rest of our code base. I’ll be happy to merge it then :). |
No problem at all. I also benefit from this as a user :). |
struct timespec ts; | ||
clock_gettime(CLOCK_REALTIME, &ts); | ||
ts.tv_sec += interval - (ts.tv_sec % interval); | ||
ts.tv_nsec = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now it seems like you’re missing the tv_nsec part, i.e. you’ll not wake exactly on the full second, right? (see the comment above)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually no, it behaves just like before. That includes the ":00 on every new minute" part. You can check that using a 5-second interval. The reason it looks simpler is that while nanosleep
expects a relative time (i.e. a delay), pthread_cond_timedwait
expects an absolute time, so it's actually easier to achieve the same result.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see. Can you update the comment below, then? You’re currently saying pthread_cond_timedwait
is equivalent to a nanosleep
call, but based on this observation, it clearly isn’t :).
Done, pushed commit. |
if (default_sink_idx != i->index) { | ||
/* default sink changed? */ | ||
default_sink_idx = i->index; | ||
get_sink_info(c, default_sink_idx); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of calling get_sink_info
, you could also just call store_volume_from_sink_cb
to save another round-trip, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed!
PulseAudio support for volume input
Very cool. Thanks again! |
Thank you! |
This fixes #492 and an additional buffer overflow that can happen when pango markup is enabled. Using config ``` general { output_format = "none" markup = "pango" } order += "wireless _first_" wireless _first_ { format_up = "W: (%quality at %essid, %bitrate) %ip" } ``` and renaming my phone's hotspot to `Hello world &<<<<<<hello world>>` i3status will throw an AddressSanitizer error: ``` ==1373240==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7411d720923e at pc 0x7411daa7cee9 bp 0x7ffdae6ce070 sp 0x7ffdae6cd800 WRITE of size 5 at 0x7411d720923e thread T0 #0 0x7411daa7cee8 in __interceptor_vsprintf /usr/src/debug/gcc/gcc/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:1765 #1 0x7411daa7d0ff in __interceptor_sprintf /usr/src/debug/gcc/gcc/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:1808 #2 0x653b2764cdaf in maybe_escape_markup ../src/output.c:102 #3 0x653b27677df9 in print_wireless_info ../src/print_wireless_info.c:607 #4 0x653b27640bf1 in main ../i3status.c:709 #5 0x7411da641ccf (/usr/lib/libc.so.6+0x25ccf) (BuildId: 6542915cee3354fbcf2b3ac5542201faec43b5c9) #6 0x7411da641d89 in __libc_start_main (/usr/lib/libc.so.6+0x25d89) (BuildId: 6542915cee3354fbcf2b3ac5542201faec43b5c9) #7 0x653b27633f24 in _start (/tmp/xx/i3status/build/i3status+0x4ff24) (BuildId: c737ce6288265fa02a7617c66f51ddd16b5a8275) Address 0x7411d720923e is located in stack of thread T0 at offset 574 in frame #0 0x653b276750ed in print_wireless_info ../src/print_wireless_info.c:513 This frame has 10 object(s): [48, 56) 'tmp' (line 604) [80, 168) 'info' (line 516) [208, 320) 'placeholders' (line 623) [352, 382) 'string_quality' (line 569) [416, 446) 'string_signal' (line 570) [480, 510) 'string_noise' (line 571) [544, 574) 'string_essid' (line 572) <== Memory access at offset 574 overflows this variable [608, 638) 'string_frequency' (line 573) [672, 702) 'string_ip' (line 574) [736, 766) 'string_bitrate' (line 575) ``` With pango disabled, the error is thrown elsewhere (#492): ``` ==1366779==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7bab43a0923e at pc 0x7bab4727cee9 bp 0x7ffc289d2540 sp 0x7ffc289d1cd0 WRITE of size 33 at 0x7bab43a0923e thread T0 #0 0x7bab4727cee8 in __interceptor_vsprintf /usr/src/debug/gcc/gcc/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:1765 #1 0x7bab4727d0ff in __interceptor_sprintf /usr/src/debug/gcc/gcc/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:1808 #2 0x5dd180858aa4 in maybe_escape_markup ../src/output.c:93 #3 0x5dd180883df9 in print_wireless_info ../src/print_wireless_info.c:607 #4 0x5dd18084cbf1 in main ../i3status.c:709 #5 0x7bab46843ccf (/usr/lib/libc.so.6+0x25ccf) (BuildId: 6542915cee3354fbcf2b3ac5542201faec43b5c9) #6 0x7bab46843d89 in __libc_start_main (/usr/lib/libc.so.6+0x25d89) (BuildId: 6542915cee3354fbcf2b3ac5542201faec43b5c9) #7 0x5dd18083ff24 in _start (/tmp/xx/i3status/build/i3status+0x4ff24) (BuildId: c737ce6288265fa02a7617c66f51ddd16b5a8275) Address 0x7bab43a0923e is located in stack of thread T0 at offset 574 in frame #0 0x5dd1808810ed in print_wireless_info ../src/print_wireless_info.c:513 This frame has 10 object(s): [48, 56) 'tmp' (line 604) [80, 168) 'info' (line 516) [208, 320) 'placeholders' (line 623) [352, 382) 'string_quality' (line 569) [416, 446) 'string_signal' (line 570) [480, 510) 'string_noise' (line 571) [544, 574) 'string_essid' (line 572) <== Memory access at offset 574 overflows this variable [608, 638) 'string_frequency' (line 573) [672, 702) 'string_ip' (line 574) [736, 766) 'string_bitrate' (line 575) ``` With the patch output is correct: ``` W: ( 72% at Hello world &<<<<<<hello world>>, 1,2009 Gb/s) 192.168.26.237 ``` and ``` W: ( 73% at Hello world &<<<<<<hello world>>, 1,1342 Gb/s) 192.168.26.237 ``` The patch changes the maybe_escape_markup function to use dynamic allocation instead of a static buffer. Confusing pointer arithmetic is replaced with index-based memory access. The `buffer` pointer does not move around except for `realloc`ations. Fixes #492 Closes #525 (alternative PR)
* maybe_escape_markup: Make function memory-safe This fixes #492 and an additional buffer overflow that can happen when pango markup is enabled. Using config ``` general { output_format = "none" markup = "pango" } order += "wireless _first_" wireless _first_ { format_up = "W: (%quality at %essid, %bitrate) %ip" } ``` and renaming my phone's hotspot to `Hello world &<<<<<<hello world>>` i3status will throw an AddressSanitizer error: ``` ==1373240==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7411d720923e at pc 0x7411daa7cee9 bp 0x7ffdae6ce070 sp 0x7ffdae6cd800 WRITE of size 5 at 0x7411d720923e thread T0 #0 0x7411daa7cee8 in __interceptor_vsprintf /usr/src/debug/gcc/gcc/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:1765 #1 0x7411daa7d0ff in __interceptor_sprintf /usr/src/debug/gcc/gcc/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:1808 #2 0x653b2764cdaf in maybe_escape_markup ../src/output.c:102 #3 0x653b27677df9 in print_wireless_info ../src/print_wireless_info.c:607 #4 0x653b27640bf1 in main ../i3status.c:709 #5 0x7411da641ccf (/usr/lib/libc.so.6+0x25ccf) (BuildId: 6542915cee3354fbcf2b3ac5542201faec43b5c9) #6 0x7411da641d89 in __libc_start_main (/usr/lib/libc.so.6+0x25d89) (BuildId: 6542915cee3354fbcf2b3ac5542201faec43b5c9) #7 0x653b27633f24 in _start (/tmp/xx/i3status/build/i3status+0x4ff24) (BuildId: c737ce6288265fa02a7617c66f51ddd16b5a8275) Address 0x7411d720923e is located in stack of thread T0 at offset 574 in frame #0 0x653b276750ed in print_wireless_info ../src/print_wireless_info.c:513 This frame has 10 object(s): [48, 56) 'tmp' (line 604) [80, 168) 'info' (line 516) [208, 320) 'placeholders' (line 623) [352, 382) 'string_quality' (line 569) [416, 446) 'string_signal' (line 570) [480, 510) 'string_noise' (line 571) [544, 574) 'string_essid' (line 572) <== Memory access at offset 574 overflows this variable [608, 638) 'string_frequency' (line 573) [672, 702) 'string_ip' (line 574) [736, 766) 'string_bitrate' (line 575) ``` With pango disabled, the error is thrown elsewhere (#492): ``` ==1366779==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7bab43a0923e at pc 0x7bab4727cee9 bp 0x7ffc289d2540 sp 0x7ffc289d1cd0 WRITE of size 33 at 0x7bab43a0923e thread T0 #0 0x7bab4727cee8 in __interceptor_vsprintf /usr/src/debug/gcc/gcc/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:1765 #1 0x7bab4727d0ff in __interceptor_sprintf /usr/src/debug/gcc/gcc/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:1808 #2 0x5dd180858aa4 in maybe_escape_markup ../src/output.c:93 #3 0x5dd180883df9 in print_wireless_info ../src/print_wireless_info.c:607 #4 0x5dd18084cbf1 in main ../i3status.c:709 #5 0x7bab46843ccf (/usr/lib/libc.so.6+0x25ccf) (BuildId: 6542915cee3354fbcf2b3ac5542201faec43b5c9) #6 0x7bab46843d89 in __libc_start_main (/usr/lib/libc.so.6+0x25d89) (BuildId: 6542915cee3354fbcf2b3ac5542201faec43b5c9) #7 0x5dd18083ff24 in _start (/tmp/xx/i3status/build/i3status+0x4ff24) (BuildId: c737ce6288265fa02a7617c66f51ddd16b5a8275) Address 0x7bab43a0923e is located in stack of thread T0 at offset 574 in frame #0 0x5dd1808810ed in print_wireless_info ../src/print_wireless_info.c:513 This frame has 10 object(s): [48, 56) 'tmp' (line 604) [80, 168) 'info' (line 516) [208, 320) 'placeholders' (line 623) [352, 382) 'string_quality' (line 569) [416, 446) 'string_signal' (line 570) [480, 510) 'string_noise' (line 571) [544, 574) 'string_essid' (line 572) <== Memory access at offset 574 overflows this variable [608, 638) 'string_frequency' (line 573) [672, 702) 'string_ip' (line 574) [736, 766) 'string_bitrate' (line 575) ``` With the patch output is correct: ``` W: ( 72% at Hello world &<<<<<<hello world>>, 1,2009 Gb/s) 192.168.26.237 ``` and ``` W: ( 73% at Hello world &<<<<<<hello world>>, 1,1342 Gb/s) 192.168.26.237 ``` The patch changes the maybe_escape_markup function to use dynamic allocation instead of a static buffer. Confusing pointer arithmetic is replaced with index-based memory access. The `buffer` pointer does not move around except for `realloc`ations. Fixes #492 Closes #525 (alternative PR) * Revert to snprintf
Not sure if this will be useful to anyone, it works for me. If compiled with
make
, nothing is changed (only ALSA is supported). By compiling withmake USE_PULSEAUDIO=1
, PulseAudio support for volume input is available by specifyingdevice="pulse:N"
line in i3status.conf, where N is the PulseAudio sink index. ALSA is still available if the device line does not match that format.