diff options
author | Arun Raghavan <arun.raghavan@collabora.co.uk> | 2010-08-21 00:39:54 +0530 |
---|---|---|
committer | Arun Raghavan <arun.raghavan@collabora.co.uk> | 2010-12-17 16:58:19 +0530 |
commit | 60fe6e264b7baf1ef598e047d6da8dc56e3f65e3 (patch) | |
tree | b8e1c9b06179538e458257e4bd47578373421776 | |
parent | 0317f5e59b4cfeaa56b9a3d356a086a999fb6a1c (diff) |
vff: Add a volume-follows-focus modulevff
Work in progress
-rw-r--r-- | src/Makefile.am | 8 | ||||
-rw-r--r-- | src/modules/module-volume-follows-focus.c | 576 |
2 files changed, 584 insertions, 0 deletions
diff --git a/src/Makefile.am b/src/Makefile.am index 499a5bada..377c732a4 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1033,6 +1033,7 @@ modlibexec_LTLIBRARIES += \ module-tunnel-source.la \ module-position-event-sounds.la \ module-augment-properties.la \ + module-volume-follows-focus.la \ module-cork-music-on-phone.la \ module-loopback.la \ module-virtual-sink.la \ @@ -1306,6 +1307,7 @@ SYMDEF_FILES = \ modules/gconf/module-gconf-symdef.h \ modules/module-position-event-sounds-symdef.h \ modules/module-augment-properties-symdef.h \ + modules/module-volume-follows-focus-symdef.h \ modules/module-cork-music-on-phone-symdef.h \ modules/module-console-kit-symdef.h \ modules/dbus/module-dbus-protocol-symdef.h \ @@ -1320,6 +1322,12 @@ $(SYMDEF_FILES): modules/module-defs.h.m4 $(MKDIR_P) $(dir $@) $(M4) -Dfname="$@" $< > $@ +# Volume follows focus + +module_volume_follows_focus_la_SOURCES = modules/module-volume-follows-focus.c +module_volume_follows_focus_la_LDFLAGS = $(MODULE_LDFLAGS) +module_volume_follows_focus_la_LIBADD = $(AM_LIBADD) libpulsecore-@PA_MAJORMINOR@.la libpulsecommon-@PA_MAJORMINOR@.la libpulse.la + # Simple protocol module_simple_protocol_tcp_la_SOURCES = modules/module-protocol-stub.c diff --git a/src/modules/module-volume-follows-focus.c b/src/modules/module-volume-follows-focus.c new file mode 100644 index 000000000..ed8088c25 --- /dev/null +++ b/src/modules/module-volume-follows-focus.c @@ -0,0 +1,576 @@ +/*** + This file is part of PulseAudio. + + Copyright 2010 Arun Raghavan + + PulseAudio is free software; you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published + by the Free Software Foundation; either version 2 of the License, + or (at your option) any later version. + + PulseAudio is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with PulseAudio; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + USA. +***/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <unistd.h> +#include <string.h> +#include <errno.h> +#include <sys/types.h> +#include <stdio.h> +#include <stdlib.h> +#include <ctype.h> + +#include <pulse/xmalloc.h> +#include <pulse/volume.h> +#include <pulse/timeval.h> +#include <pulse/util.h> + +#include <pulsecore/core-error.h> +#include <pulsecore/module.h> +#include <pulsecore/core-util.h> +#include <pulsecore/modargs.h> +#include <pulsecore/log.h> +#include <pulsecore/sink-input.h> +#include <pulsecore/pstream.h> +#include <pulsecore/pstream-util.h> +#include <pulsecore/llist.h> + +#include <pulsecore/x11wrap.h> +#include <X11/Xatom.h> +#include <X11/Xproto.h> + +#include "module-volume-follows-focus-symdef.h" + +PA_MODULE_AUTHOR("Arun Raghavan"); +PA_MODULE_DESCRIPTION("Automatically lower/raise volume of an application when it's window is lowered/raised"); +PA_MODULE_VERSION(PACKAGE_VERSION); +PA_MODULE_LOAD_ONCE(TRUE); + +#define VFF_SCALE_FACTOR 0.05 + +struct userdata { + pa_core *core; + pa_module *module; + pa_modargs *modargs; + pa_hook_slot *sink_input_put_hook_slot; + pa_hook_slot *sink_input_unlink_hook_slot; + Display *display; + Window active_window; + /* XXX: does this need to be protected with a lock? */ + pa_hashmap *maps; + + pa_x11_wrapper *x11_wrapper; + pa_x11_client *x11_client; +}; + +static const char* const valid_modargs[] = { + "display", + NULL +}; + +struct windows { + Window w; + PA_LLIST_FIELDS(struct windows); +}; + +struct streams { + pa_sink_input *si; + pa_cvolume old_volume; + PA_LLIST_FIELDS(struct streams); +}; + +struct entry { + char *key; + pa_bool_t damped; + PA_LLIST_HEAD(struct windows, windows); + PA_LLIST_HEAD(struct streams, streams); +}; + +static Atom _net_active_window, _net_wm_pid, _wm_client_machine; +static XErrorHandler _x_old_handler = NULL; + +static void dump_map(struct userdata *u) +{ + struct entry *e; + struct windows *w; + struct streams *s; + void *state = NULL; + + pa_log("----"); + while ((e = pa_hashmap_iterate(u->maps, &state, NULL))) { + pa_log("%s%s", e->key, e->damped ? "(damped)" : ""); + PA_LLIST_FOREACH(w, e->windows) + pa_log(" w: %lu", w->w); + PA_LLIST_FOREACH(s, e->streams) + pa_log(" s: %u", s->si->index); + } + pa_log("----"); +} + +static void entry_free_cb(void *e, void *u) +{ + struct entry *entry = e; + struct windows *w1, *w2; + struct streams *s1, *s2; + + PA_LLIST_FOREACH_SAFE(w1, w2, entry->windows) + pa_xfree(w1); + PA_LLIST_FOREACH_SAFE(s1, s2, entry->streams) + pa_xfree(s1); + + pa_xfree(entry->key); + pa_xfree(entry); +} + +static void clean_map(struct pa_hashmap *map, struct entry *e) +{ + if (e->windows == NULL && e->streams == NULL) { + pa_hashmap_remove (map, e->key); + pa_xfree(e); + } +} + +static char *get_name(pa_proplist *p, const char *prefix) { + const char *r; + + if (!p) + return NULL; + + if ((r = pa_proplist_gets(p, PA_PROP_MEDIA_ROLE))) + return pa_sprintf_malloc("%s-by-media-role:%s", prefix, r); + else if ((r = pa_proplist_gets(p, PA_PROP_APPLICATION_ID))) + return pa_sprintf_malloc("%s-by-application-id:%s", prefix, r); + else if ((r = pa_proplist_gets(p, PA_PROP_APPLICATION_NAME))) + return pa_sprintf_malloc("%s-by-application-name:%s", prefix, r); + else if ((r = pa_proplist_gets(p, PA_PROP_MEDIA_NAME))) + return pa_sprintf_malloc("%s-by-media-name:%s", prefix, r); + + return pa_sprintf_malloc("%s-fallback:%s", prefix, r); +} + +static char *pa_get_app_id_for_stream(pa_proplist *p) +{ + char *ret; + + ret = pa_sprintf_malloc("%s:%s", + pa_proplist_gets(p, PA_PROP_APPLICATION_PROCESS_MACHINE_ID), + pa_proplist_gets(p, PA_PROP_APPLICATION_PROCESS_ID)); + + return ret; +} + +static char *pa_get_app_id_for_window(Display *d, Window w) +{ + Atom t; + int f; + unsigned long n, b; + unsigned char *data = NULL; + pid_t pid; + char *ret = NULL; + + if ((XGetWindowProperty(d, w, _net_wm_pid, 0, sizeof(pid)/4, False, XA_CARDINAL, &t, &f, &n, &b, &data) != Success) || t != XA_CARDINAL || b != 0 || !data || f != 32) { + pa_log("Could not get pid"); + goto done; + } + + pid = (pid_t) ((uint32_t*)data)[0]; + XFree(data); + data = NULL; + + if ((XGetWindowProperty(d, w, _wm_client_machine, 0, 64, False, XA_STRING, &t, &f, &n, &b, &data) != Success) || t != XA_STRING || b != 0 || !data || f != 8) { + pa_log("Could not get hostname"); + goto done; + } + + ret = pa_sprintf_malloc("%s:%u", data, (int)pid); + +done: + if (data) + XFree(data); + + return ret; +} + +static struct entry* get_entry_for_window(struct userdata *u, Display *d, Window w) +{ + char *id = pa_get_app_id_for_window(d, w); + struct entry *e = NULL; + + if (id) + e = pa_hashmap_get(u->maps, id); + + pa_xfree(id); + return e; +} + +static void add_window(struct userdata *u, Display *d, Window w) +{ + char *id = pa_get_app_id_for_window(d, w); + struct entry *e = NULL; + struct windows *window; + + if (!id) + return; + + e = pa_hashmap_get(u->maps, id); + if (!e) { + e = pa_xnew0(struct entry, 1); + e->key = id; + e->damped = FALSE; + pa_hashmap_put(u->maps, e->key, e); + } else + pa_xfree(id); + + window = pa_xnew0(struct windows, 1); + window->w = w; + PA_LLIST_PREPEND(struct windows, e->windows, window); + pa_log("add window"); + dump_map(u); +} + +static void damp_stream(struct streams *s) +{ + pa_volume_t scale = pa_sw_volume_from_linear(VFF_SCALE_FACTOR); + pa_cvolume new_volume; + + pa_sink_input_get_volume(s->si, &s->old_volume, FALSE); + + new_volume = s->old_volume; + pa_cvolume_scale(&new_volume, scale); + + pa_sink_input_set_volume(s->si, &new_volume, FALSE, FALSE); +} + +static void do_damp(struct entry *e) +{ + struct streams *s; + + /* Don't mark entries which have no streams as damped */ + if (e->damped || !e->streams) + return; + + e->damped = TRUE; + PA_LLIST_FOREACH(s, e->streams) { + damp_stream(s); + } +} + +static void do_undamp(struct entry *e) +{ + struct streams *s; + + if (e->damped) { + e->damped = FALSE; + + PA_LLIST_FOREACH(s, e->streams) { + pa_sink_input_set_volume(s->si, &s->old_volume, FALSE, FALSE); + } + } +} + +static void vff_adjust_volumes(struct userdata *u, struct entry *new) +{ + struct entry *e; + void *state; + + PA_HASHMAP_FOREACH(e, u->maps, state) { + if (e != new) + do_damp(e); + } + + do_undamp(new); +} + +static pa_hook_result_t vff_sink_input_put_hook_callback(pa_core *c, pa_sink_input *si, struct userdata *u) +{ + char *id; + struct entry *e = NULL; + struct streams *stream; + struct windows *w; + pa_assert(si); + + id = pa_get_app_id_for_stream(si->proplist); + if (!id) + goto done; + + e = pa_hashmap_get(u->maps, id); + if (!e) { + e = pa_xnew0(struct entry, 1); + e->key = id; + e->damped = FALSE; + pa_hashmap_put(u->maps, e->key, e); + } else { + pa_xfree(id); + + } + + stream = pa_xnew0(struct streams, 1);; + stream->si = si; + PA_LLIST_PREPEND(struct streams, e->streams, stream); + + /* damp all other entries in case its not been done yet - this will + * happen for the case where a program has a window with focus and + * creates a first stream */ + PA_LLIST_FOREACH(w, e->windows) { + if (w->w == u->active_window) { + vff_adjust_volumes(u, e); + break; + } + } + + pa_log("add stream"); + dump_map(u); +done: + return PA_HOOK_OK; +} + +static pa_hook_result_t vff_sink_input_unlink_hook_callback(pa_core *c, pa_sink_input *si, struct userdata *u) +{ + char *id; + struct entry *e = NULL; + struct streams *s1, *s2; + pa_assert(si); + + id = pa_get_app_id_for_stream(si->proplist); + if (id) + e = pa_hashmap_get(u->maps, id); + + if (e) { + PA_LLIST_FOREACH_SAFE(s1, s2, e->streams) { + if (s1->si == si) { + PA_LLIST_REMOVE(struct streams, e->streams, s1); + pa_xfree(s1); + + if (!e->streams) { + /* No streams => we're no longer damped */ + e->damped = FALSE; + + /* XXX: deque and undamp whatever stream was playing last + * before we got focus */ + } + + clean_map(u->maps, e); + break; + } + } + + pa_log("del stream"); + dump_map(u); + } else { + char *name = get_name(si->proplist, "sink-input"); + pa_log("Weird, got a stream (%s) that wasn't in our stream map", name); + pa_xfree(name); + } + + pa_xfree(id); + return PA_HOOK_OK; +} + +static int vff_x11_error_handler(Display *d, XErrorEvent *ev) +{ + if (ev->request_code == X_GetProperty) + return 0; + else + return _x_old_handler(d, ev); +} + +static int vff_x11_event_cb(pa_x11_wrapper *w, XEvent *ev, void *userdata) +{ + struct userdata *u = userdata; + Atom t; + int f; + unsigned long n, b; + unsigned char *data = NULL; + Window win; + + pa_assert(w); + pa_assert(u); + pa_assert(u->x11_wrapper == w); + + switch (ev->type) { + case CreateNotify: + { + add_window(u, ev->xcreatewindow.display, ev->xcreatewindow.window); + break; + } + + case DestroyNotify: + { + /* By the time we get here, the properties on the window are lost, + * so we have to do a full iteration. This can be optimised with + * another hash (FIXME). */ + struct entry *e; + void *state = NULL; + + while ((e = pa_hashmap_iterate(u->maps, &state, NULL))) { + struct windows *w1, *w2; + + PA_LLIST_FOREACH_SAFE(w1, w2, e->windows) { + if (w1->w == ev->xcreatewindow.window) { + PA_LLIST_REMOVE(struct windows, e->windows, w1); + pa_xfree(w1); + clean_map(u->maps, e); + break; + } + } + + /* XXX: deque and undamp whatever stream was playing + * last before we got focus */ + } + pa_log("del window"); + dump_map(u); + break; + } + + case PropertyNotify: + { + if (ev->xproperty.atom != _net_active_window) + return 0; + + if ((XGetWindowProperty(u->display, XDefaultRootWindow(u->display), _net_active_window, 0, 1, False, XA_WINDOW, &t, &f, &n, &b, &data) != Success) || t != XA_WINDOW || b != 0 || !data || f != 32) { + pa_log("Could not get active window"); + goto done; + } + + win = (Window) ((uint32_t *)data)[0]; + pa_log("Window %lu (%lu)", win, u->active_window); + + if (win && win != u->active_window) { + struct entry *e; + u->active_window = win; + + e = get_entry_for_window (u, u->display, u->active_window); + if (!e) { + add_window(u, u->display, u->active_window); + e = get_entry_for_window (u, u->display, u->active_window); + } else if (e->streams) { + vff_adjust_volumes(u, e); + } + } + + XFree(data); + break; + } + + default: + return 0; + } + +done: + return 1; +} + +static void vff_x11_kill_cb(pa_x11_wrapper *w, void *userdata) +{ + struct userdata *u = userdata; + + pa_assert(w); + pa_assert(u); + pa_assert(u->x11_wrapper == w); + + if (u->x11_client) + pa_x11_client_free(u->x11_client); + + if (u->x11_wrapper) + pa_x11_wrapper_unref(u->x11_wrapper); + + if (u->maps) + pa_hashmap_free(u->maps, entry_free_cb, NULL); + + u->x11_client = NULL; + u->x11_wrapper = NULL; + u->maps = NULL; + + pa_module_unload_request(u->module, TRUE); +} + +int pa__init(pa_module *m) { + struct userdata *u = NULL; + pa_modargs *ma; + Window w; + XWindowAttributes wattr; + pa_assert(m); + + pa_log("Starting vff"); + + if (!(ma = pa_modargs_new(m->argument, valid_modargs))) { + pa_log("failed to parse module arguments"); + goto fail; + } + + m->userdata = u = pa_xnew0(struct userdata, 1); + u->core = m->core; + u->module = m; + u->modargs = ma; + + if (!(u->x11_wrapper = pa_x11_wrapper_get(m->core, pa_modargs_get_value(ma, "display", NULL)))) + goto fail; + + u->display = pa_x11_wrapper_get_display(u->x11_wrapper); + if (!(w = XDefaultRootWindow(u->display))) + goto fail; + + XGetWindowAttributes(u->display, w, &wattr); + XSelectInput(u->display, w, wattr.your_event_mask | PropertyChangeMask | SubstructureNotifyMask); + _net_active_window = XInternAtom(u->display, "_NET_ACTIVE_WINDOW", False); + _net_wm_pid = XInternAtom(u->display, "_NET_WM_PID", False); + _wm_client_machine = XInternAtom(u->display, "WM_CLIENT_MACHINE", False); + + XSync(u->display, FALSE); + _x_old_handler = XSetErrorHandler(vff_x11_error_handler); + + u->x11_client = pa_x11_client_new(u->x11_wrapper, vff_x11_event_cb, vff_x11_kill_cb, u); + + u->maps = pa_hashmap_new(pa_idxset_string_hash_func, pa_idxset_string_compare_func); + + u->sink_input_put_hook_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SINK_INPUT_PUT], PA_HOOK_LATE, (pa_hook_cb_t) vff_sink_input_put_hook_callback, u); + + u->sink_input_unlink_hook_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SINK_INPUT_UNLINK], PA_HOOK_LATE, (pa_hook_cb_t) vff_sink_input_unlink_hook_callback, u); + + return 0; + +fail: + if (u) + pa_xfree(u); + pa__done(m); + + return -1; +} + +void pa__done(pa_module*m) { + struct userdata* u; + + pa_assert(m); + + if (!(u = m->userdata)) + return; + + if (u->sink_input_put_hook_slot) + pa_hook_slot_free(u->sink_input_put_hook_slot); + if (u->sink_input_unlink_hook_slot) + pa_hook_slot_free(u->sink_input_unlink_hook_slot); + + if (u->x11_client) + pa_x11_client_free(u->x11_client); + if (u->x11_wrapper) + pa_x11_wrapper_unref(u->x11_wrapper); + + if (u->modargs) + pa_modargs_free(u->modargs); + + XSync(u->display, FALSE); + XSetErrorHandler(_x_old_handler); + + pa_xfree(u); +} |