diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/print_volume.c | 29 | ||||
-rw-r--r-- | src/pulse.c | 245 |
2 files changed, 273 insertions, 1 deletions
diff --git a/src/print_volume.c b/src/print_volume.c index d8766b7..4359ac1 100644 --- a/src/print_volume.c +++ b/src/print_volume.c @@ -51,13 +51,40 @@ void print_volume(yajl_gen json_gen, char *buffer, const char *fmt, const char * char *outwalk = buffer; int pbval = 1; - /* Printing volume only works with ALSA at the moment */ + /* Printing volume works with ALSA and PulseAudio at the moment */ if (output_format == O_I3BAR) { char *instance; asprintf(&instance, "%s.%s.%d", device, mixer, mixer_idx); INSTANCE(instance); free(instance); } + + /* Try PulseAudio first */ + + /* If the device name has the format "pulse[:N]" where N is the + * index of the PulseAudio sink then force PulseAudio, optionally + * overriding the default sink */ + if (!strncasecmp(device, "pulse", strlen("pulse"))) { + uint32_t sink_idx = device[5] == ':' ? (uint32_t)atoi(device + 6) + : DEFAULT_SINK_INDEX; + int ivolume = pulse_initialize() ? volume_pulseaudio(sink_idx) : 0; + /* negative result means error, stick to 0 */ + if (ivolume < 0) + ivolume = 0; + outwalk = apply_volume_format(fmt, outwalk, ivolume); + goto out; + } else if (!strcasecmp(device, "default") && pulse_initialize()) { + /* no device specified or "default" set */ + int ivolume = volume_pulseaudio(DEFAULT_SINK_INDEX); + if (ivolume >= 0) { + outwalk = apply_volume_format(fmt, outwalk, ivolume); + goto out; + } + /* negative result means error, fail PulseAudio attempt */ + } +/* If some other device was specified or PulseAudio is not detected, + * proceed to ALSA / OSS */ + #ifdef LINUX int err; snd_mixer_t *m; diff --git a/src/pulse.c b/src/pulse.c new file mode 100644 index 0000000..76e2495 --- /dev/null +++ b/src/pulse.c @@ -0,0 +1,245 @@ +// vim:ts=4:sw=4:expandtab +#include <string.h> +#include <stdio.h> +#include <pulse/pulseaudio.h> +#include "i3status.h" +#include "queue.h" + +#define APP_NAME "i3status" +#define APP_ID "org.i3wm" + +typedef struct indexed_volume_s { + uint32_t idx; + int volume; + TAILQ_ENTRY(indexed_volume_s) entries; +} indexed_volume_t; + +static pa_threaded_mainloop *main_loop = NULL; +static pa_context *context = NULL; +static pa_mainloop_api *api = NULL; +static bool context_ready = false; +static uint32_t default_sink_idx = DEFAULT_SINK_INDEX; +TAILQ_HEAD(tailhead, indexed_volume_s) cached_volume = + TAILQ_HEAD_INITIALIZER(cached_volume); +static pthread_mutex_t pulse_mutex = PTHREAD_MUTEX_INITIALIZER; + +static void pulseaudio_error_log(pa_context *c) { + fprintf(stderr, + "i3status: PulseAudio: %s\n", + pa_strerror(pa_context_errno(c))); +} + +static bool pulseaudio_free_operation(pa_context *c, pa_operation *o) { + if (o) + pa_operation_unref(o); + else + pulseaudio_error_log(c); + /* return false if the operation failed */ + return o; +} + +/* + * save the volume for the specified sink index + * returning true if the value was changed + */ +static bool save_volume(uint32_t sink_idx, int new_volume) { + pthread_mutex_lock(&pulse_mutex); + indexed_volume_t *entry; + TAILQ_FOREACH(entry, &cached_volume, entries) { + if (entry->idx == sink_idx) { + const bool changed = (new_volume != entry->volume); + entry->volume = new_volume; + pthread_mutex_unlock(&pulse_mutex); + return changed; + } + } + /* index not found, store it */ + entry = malloc(sizeof(*entry)); + TAILQ_INSERT_HEAD(&cached_volume, entry, entries); + entry->idx = sink_idx; + entry->volume = new_volume; + pthread_mutex_unlock(&pulse_mutex); + return true; +} + +static void store_volume_from_sink_cb(pa_context *c, + const pa_sink_info *info, + int eol, + void *userdata) { + if (eol < 0) { + if (pa_context_errno(c) == PA_ERR_NOENTITY) + return; + + pulseaudio_error_log(c); + return; + } + + if (eol > 0) + return; + + int avg_vol = pa_cvolume_avg(&info->volume); + int vol_perc = (int)((long long)avg_vol * 100 / PA_VOLUME_NORM); + + /* if this is the default sink we must try to save it twice: once with + * DEFAULT_SINK_INDEX as the index, and another with its proper value + * (using bitwise OR to avoid early-out logic) */ + if ((info->index == default_sink_idx && + save_volume(DEFAULT_SINK_INDEX, vol_perc)) | + save_volume(info->index, vol_perc)) { + /* if the volume changed, wake the main thread */ + pthread_mutex_lock(&i3status_sleep_mutex); + pthread_cond_broadcast(&i3status_sleep_cond); + pthread_mutex_unlock(&i3status_sleep_mutex); + } +} + +static void get_sink_info(pa_context *c, uint32_t idx) { + pa_operation *o = + idx == DEFAULT_SINK_INDEX ? pa_context_get_sink_info_by_name( + c, "@DEFAULT_SINK@", store_volume_from_sink_cb, NULL) + : pa_context_get_sink_info_by_index( + c, idx, store_volume_from_sink_cb, NULL); + pulseaudio_free_operation(c, o); +} + +static void store_default_sink_cb(pa_context *c, + const pa_sink_info *i, + int eol, + void *userdata) { + if (i) { + if (default_sink_idx != i->index) { + /* default sink changed? */ + default_sink_idx = i->index; + store_volume_from_sink_cb(c, i, eol, userdata); + } + } +} + +static void update_default_sink(pa_context *c) { + pa_operation *o = pa_context_get_sink_info_by_name( + c, + "@DEFAULT_SINK@", + store_default_sink_cb, + NULL); + pulseaudio_free_operation(c, o); +} + +static void subscribe_cb(pa_context *c, pa_subscription_event_type_t t, + uint32_t idx, void *userdata) { + if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) != PA_SUBSCRIPTION_EVENT_CHANGE) + return; + pa_subscription_event_type_t facility = + t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK; + switch (facility) { + case PA_SUBSCRIPTION_EVENT_SERVER: + /* server change event, see if the default sink changed */ + update_default_sink(c); + break; + case PA_SUBSCRIPTION_EVENT_SINK: + get_sink_info(c, idx); + break; + default: + break; + } +} + +static void context_state_callback(pa_context *c, void *userdata) { + switch (pa_context_get_state(c)) { + case PA_CONTEXT_UNCONNECTED: + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + case PA_CONTEXT_TERMINATED: + default: + break; + + case PA_CONTEXT_READY: { + pa_context_set_subscribe_callback(c, subscribe_cb, NULL); + update_default_sink(c); + + pa_operation *o = pa_context_subscribe( + c, + PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SERVER, + NULL, + NULL); + if (!pulseaudio_free_operation(c, o)) + break; + context_ready = true; + } break; + + case PA_CONTEXT_FAILED: + pulseaudio_error_log(c); + break; + } +} + +/* + * returns the current volume in percent, which, as per PulseAudio, + * may be > 100% + */ +int volume_pulseaudio(uint32_t sink_idx) { + if (!context_ready || default_sink_idx == DEFAULT_SINK_INDEX) + return -1; + + pthread_mutex_lock(&pulse_mutex); + const indexed_volume_t *entry; + TAILQ_FOREACH(entry, &cached_volume, entries) { + if (entry->idx == sink_idx) { + int vol = entry->volume; + pthread_mutex_unlock(&pulse_mutex); + return vol; + } + } + pthread_mutex_unlock(&pulse_mutex); + /* first time requires a prime callback call because we only get + * updates when the volume actually changes, but we need it to + * be correct even if it never changes */ + pa_threaded_mainloop_lock(main_loop); + get_sink_info(context, sink_idx); + pa_threaded_mainloop_unlock(main_loop); + /* show 0 while we don't have this information */ + return 0; +} + +/* + * detect and, if necessary, initialize the PulseAudio API + */ +bool pulse_initialize(void) { + if (!main_loop) { + main_loop = pa_threaded_mainloop_new(); + if (!main_loop) + return false; + } + if (!api) { + api = pa_threaded_mainloop_get_api(main_loop); + if (!api) + return false; + } + if (!context) { + pa_proplist *proplist = pa_proplist_new(); + pa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, APP_NAME); + pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, APP_ID); + pa_proplist_sets(proplist, PA_PROP_APPLICATION_VERSION, VERSION); + context = pa_context_new_with_proplist(api, APP_NAME, proplist); + pa_proplist_free(proplist); + if (!context) + return false; + pa_context_set_state_callback(context, + context_state_callback, + NULL); + if (pa_context_connect(context, + NULL, + PA_CONTEXT_NOFAIL | PA_CONTEXT_NOAUTOSPAWN, + NULL) < 0) { + pulseaudio_error_log(context); + return false; + } + if (pa_threaded_mainloop_start(main_loop) < 0) { + pulseaudio_error_log(context); + pa_threaded_mainloop_free(main_loop); + main_loop = NULL; + return false; + } + } + return true; +} |