diff options
| author | Michael Stapelberg | 2015-04-03 14:45:40 -0700 | 
|---|---|---|
| committer | Michael Stapelberg | 2015-04-03 14:45:40 -0700 | 
| commit | 9abe0a9d593548c021c1ab8c3548578bdd58ee32 (patch) | |
| tree | 4e38a3eb94ad40e0b34838f3664645007535b9f4 | |
| parent | Merge pull request #17 from chrko/format_down (diff) | |
| parent | PulseAudio support for volume input (diff) | |
Merge pull request #7 from Watcom/master
PulseAudio support for volume input
| -rw-r--r-- | .travis.yml | 2 | ||||
| -rw-r--r-- | Makefile | 1 | ||||
| -rw-r--r-- | README | 3 | ||||
| -rw-r--r-- | i3status.c | 21 | ||||
| -rw-r--r-- | include/i3status.h | 9 | ||||
| -rw-r--r-- | man/i3status.man | 35 | ||||
| -rw-r--r-- | src/print_volume.c | 29 | ||||
| -rw-r--r-- | src/pulse.c | 245 | 
8 files changed, 328 insertions, 17 deletions
| diff --git a/.travis.yml b/.travis.yml index 9a81d97..0b18190 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ before_install:    - sudo apt-get install -t utopic clang-format-3.5    - clang-format-3.5 --version  install: -  - sudo apt-get install libconfuse-dev libyajl-dev libasound2-dev libiw-dev asciidoc libcap2-bin +  - sudo apt-get install libconfuse-dev libyajl-dev libasound2-dev libiw-dev asciidoc libcap2-bin libpulse-dev  script:    - make -j    - clang-format-3.5 -i **/*.[ch] && git diff --exit-code || (echo 'Code was not formatted using clang-format!'; false) @@ -18,6 +18,7 @@ CPPFLAGS+=-DVERSION=\"${GIT_VERSION}\"  CFLAGS+=-Iinclude  LIBS+=-lconfuse  LIBS+=-lyajl +LIBS+=-lpulse  VERSION:=$(shell git describe --tags --abbrev=0)  GIT_VERSION:="$(shell git describe --tags --always) ($(shell git log --pretty=format:%cd --date=short -n1))" @@ -20,9 +20,10 @@ i3status has the following dependencies:   • libiw-dev   • libcap2-bin (for getting network status without root permissions)   • asciidoc (only for the documentation) + • libpulse-dev (for getting the current volume using PulseAudio)  On debian-based systems, the following line will install all requirements: -apt-get install libconfuse-dev libyajl-dev libasound2-dev libiw-dev asciidoc libcap2-bin +apt-get install libconfuse-dev libyajl-dev libasound2-dev libiw-dev asciidoc libcap2-bin libpulse-dev   ┌────────────────────────────┐   │ Upstream                   │ @@ -62,6 +62,9 @@ cfg_t *cfg, *cfg_general, *cfg_section;  void **cur_instance; +pthread_cond_t i3status_sleep_cond = PTHREAD_COND_INITIALIZER; +pthread_mutex_t i3status_sleep_mutex = PTHREAD_MUTEX_INITIALIZER; +  /*   * Set the exit_upon_signal flag, because one cannot do anything in a safe   * manner in a signal handler (e.g. fprintf, which we really want to do for @@ -549,6 +552,7 @@ int main(int argc, char *argv[]) {      char buffer[4096];      void **per_instance = calloc(cfg_size(cfg, "order"), sizeof(*per_instance)); +    pthread_mutex_lock(&i3status_sleep_mutex);      while (1) {          if (exit_upon_signal) { @@ -684,13 +688,16 @@ int main(int argc, char *argv[]) {          fflush(stdout);          /* To provide updates on every full second (as good as possible) -         * we don’t use sleep(interval) but we sleep until the next -         * second (with microsecond precision) plus (interval-1) -         * seconds. We also align to 60 seconds modulo interval such +         * we don’t use sleep(interval) but we sleep until the next second. +         * We also align to 60 seconds modulo interval such           * that we start with :00 on every new minute. */ -        struct timeval current_timeval; -        gettimeofday(¤t_timeval, NULL); -        struct timespec ts = {interval - 1 - (current_timeval.tv_sec % interval), (10e5 - current_timeval.tv_usec) * 1000}; -        nanosleep(&ts, NULL); +        struct timespec ts; +        clock_gettime(CLOCK_REALTIME, &ts); +        ts.tv_sec += interval - (ts.tv_sec % interval); +        ts.tv_nsec = 0; + +        /* Sleep to absolute time 'ts', unless the condition +         * 'i3status_sleep_cond' is signaled from another thread */ +        pthread_cond_timedwait(&i3status_sleep_cond, &i3status_sleep_mutex, &ts);      }  } diff --git a/include/i3status.h b/include/i3status.h index 54aee13..8fb1b79 100644 --- a/include/i3status.h +++ b/include/i3status.h @@ -14,10 +14,14 @@ enum { O_DZEN2,  #include <yajl/yajl_version.h>  #include <unistd.h>  #include <string.h> +#include <pthread.h> +#include <stdint.h>  #define BEGINS_WITH(haystack, needle) (strncmp(haystack, needle, strlen(needle)) == 0)  #define max(a, b) ((a) > (b) ? (a) : (b)) +#define DEFAULT_SINK_INDEX UINT32_MAX +  #if defined(LINUX)  #define THERMAL_ZONE "/sys/class/thermal/thermal_zone%d/temp" @@ -195,6 +199,8 @@ void print_eth_info(yajl_gen json_gen, char *buffer, const char *interface, cons  void print_load(yajl_gen json_gen, char *buffer, const char *format, const float max_threshold);  void print_volume(yajl_gen json_gen, char *buffer, const char *fmt, const char *fmt_muted, const char *device, const char *mixer, int mixer_idx);  bool process_runs(const char *path); +int volume_pulseaudio(uint32_t sink_idx); +bool pulse_initialize(void);  /* socket file descriptor for general purposes */  extern int general_socket; @@ -203,4 +209,7 @@ extern cfg_t *cfg, *cfg_general, *cfg_section;  extern void **cur_instance; +extern pthread_cond_t i3status_sleep_cond; +extern pthread_mutex_t i3status_sleep_mutex; +  #endif diff --git a/man/i3status.man b/man/i3status.man index 5754196..a3a8c60 100644 --- a/man/i3status.man +++ b/man/i3status.man @@ -426,13 +426,26 @@ details on the format string.  === Volume -Outputs the volume of the specified mixer on the specified device. Works only -on Linux because it uses ALSA. -A simplified configuration can be used on FreeBSD and OpenBSD due to -the lack of ALSA,  the +device+ and +mixer+ options can be -ignored on these systems. On these systems the OSS API is used instead to -query +/dev/mixer+ directly if +mixer_dix+ is -1, otherwise -+/dev/mixer++mixer_idx+. +Outputs the volume of the specified mixer on the specified device.  PulseAudio +and ALSA (Linux only) are supported.  If PulseAudio is absent, a simplified +configuration can be used on FreeBSD and OpenBSD due to the lack of ALSA,  the ++device+ and +mixer+ options can be ignored on these systems. On these systems +the OSS API is used instead to query +/dev/mixer+ directly if +mixer_idx+ is +-1, otherwise +/dev/mixer++mixer_idx+. + +To get PulseAudio volume information, one must use the following format in the +device line: + + device = "pulse" + +or + + device = "pulse:N" + +where N is the index of the PulseAudio sink. If no sink is specified the +default is used. If the device string is missing or is set to "default", +PulseAudio will be tried if detected and will fallback to ALSA (Linux) +or OSS (FreeBSD/OpenBSD).  *Example order*: +volume master+ @@ -450,6 +463,14 @@ volume master {  	mixer_idx = 0  }  ------------------------------------------------------------- +*Example configuration (PulseAudio)*: +------------------------------------------------------------- +volume master { +	format = "♪: %volume" +	format_muted = "♪: muted (%volume)" +	device = "pulse:1" +} +-------------------------------------------------------------  == Universal module options 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; +} | 
