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; +} | 
