Merge pull request #7 from Watcom/master

PulseAudio support for volume input
This commit is contained in:
Michael Stapelberg 2015-04-03 14:45:40 -07:00
commit 9abe0a9d59
8 changed files with 328 additions and 17 deletions

View File

@ -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)

View File

@ -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))"

3
README
View File

@ -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 │

View File

@ -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 dont 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 dont 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(&current_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);
}
}

View File

@ -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

View File

@ -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

View File

@ -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;

245
src/pulse.c Normal file
View File

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