Currently works on bluetooth cards based on existing and incoming streams' media roles. --- src/Makefile.am | 7 + src/daemon/default.pa.in | 3 + src/modules/module-profile-switcher.c | 230 +++++++++++++++++++++++++++++++++ 3 files changed, 240 insertions(+), 0 deletions(-) create mode 100644 src/modules/module-profile-switcher.c diff --git a/src/Makefile.am b/src/Makefile.am index 521bf50..6cd9a76 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -991,6 +991,7 @@ modlibexec_LTLIBRARIES += \ module-always-sink.la \ module-rescue-streams.la \ module-intended-roles.la \ + module-profile-switcher.la \ module-suspend-on-idle.la \ module-echo-cancel.la \ module-http-protocol-tcp.la \ @@ -1291,6 +1292,7 @@ SYMDEF_FILES = \ module-always-sink-symdef.h \ module-rescue-streams-symdef.h \ module-intended-roles-symdef.h \ + module-profile-switcher-symdef.h \ module-suspend-on-idle-symdef.h \ module-echo-cancel-symdef.h \ module-hal-detect-symdef.h \ @@ -1750,6 +1752,11 @@ module_intended_roles_la_LDFLAGS = $(MODULE_LDFLAGS) module_intended_roles_la_LIBADD = $(MODULE_LIBADD) module_intended_roles_la_CFLAGS = $(AM_CFLAGS) +# Switch profiles based on (role) heuristics +module_profile_switcher_la_SOURCES = modules/module-profile-switcher.c +module_profile_switcher_la_LDFLAGS = $(MODULE_LDFLAGS) +module_profile_switcher_la_LIBADD = $(MODULE_LIBADD) + # Suspend-on-idle module module_suspend_on_idle_la_SOURCES = modules/module-suspend-on-idle.c module_suspend_on_idle_la_LDFLAGS = $(MODULE_LDFLAGS) diff --git a/src/daemon/default.pa.in b/src/daemon/default.pa.in index f2f22e8..7cf2416 100755 --- a/src/daemon/default.pa.in +++ b/src/daemon/default.pa.in @@ -163,6 +163,9 @@ load-module module-role-cork ### loading modules and rerouting streams. load-module module-filter-heuristics load-module module-filter-apply + +### Module to switch profiles based on heuristics +load-module module-profile-switcher ])dnl ifelse(@HAVE_DBUS@, 1, [dnl diff --git a/src/modules/module-profile-switcher.c b/src/modules/module-profile-switcher.c new file mode 100644 index 0000000..4607ac6 --- /dev/null +++ b/src/modules/module-profile-switcher.c @@ -0,0 +1,230 @@ +/*** + This file is part of PulseAudio. + + Copyright 2012 Arun Raghavan <arun.raghavan at collabora.co.uk> + Copyright 2012 Collabora Ltd. + + 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.1 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 <pulse/xmalloc.h> + +#include <pulsecore/core-util.h> +#include <pulsecore/idxset.h> +#include <pulsecore/core.h> +#include <pulsecore/sink.h> +#include <pulsecore/sink-input.h> +#include <pulsecore/source-output.h> +#include <pulsecore/modargs.h> +#include <pulsecore/log.h> + +#include "module-profile-switcher-symdef.h" + +PA_MODULE_AUTHOR("Arun Raghavan"); +PA_MODULE_DESCRIPTION("Switch profiles for modules based on certain heuristics"); +PA_MODULE_VERSION(PACKAGE_VERSION); +PA_MODULE_LOAD_ONCE(TRUE); + +static const char* const valid_modargs[] = { + NULL, +}; + +struct userdata { + pa_hook_slot + *sink_input_put_slot, + *sink_input_unlink_post_slot, + *source_output_unlink_post_slot; +}; + +#define GET_ROLE(i) \ + (pa_proplist_gets((i)->proplist, PA_PROP_MEDIA_ROLE) ? pa_proplist_gets((i)->proplist, PA_PROP_MEDIA_ROLE) : "") + +static pa_bool_t card_is_bluetooth(pa_card *card) { + const char *device_api; + + if (!card) + return FALSE; + + device_api = pa_proplist_gets(card->proplist, PA_PROP_DEVICE_API); + + return (device_api && pa_streq(device_api, "bluez")); +} + +static pa_bool_t card_has_active_source(pa_card *card) { + pa_source *source; + uint32_t idx; + + if (!card || !card->sources) + return FALSE; + + PA_IDXSET_FOREACH(source, card->sources, idx) { + if (pa_source_linked_by(source) > 0) + return TRUE; + } + + return FALSE; +} + +static pa_hook_result_t sink_input_put_cb(pa_core *c, pa_sink_input *i, void *userdata) { + pa_card *card; + uint32_t idx1, idx2, idx3; + + pa_assert(c); + pa_assert(i); + + /* Incoming stream -- try to switch connected Bluetooth headsets to HSP/HFP + * if this is a "phone" stream, otherwise to A2DP. */ + + PA_IDXSET_FOREACH(card, c->cards, idx1) { + pa_sink *sink; + pa_bool_t use_a2dp = TRUE; + + if (!card_is_bluetooth(card)) + continue; + + PA_IDXSET_FOREACH(sink, card->sinks, idx2) { + pa_sink_input *input; + + PA_IDXSET_FOREACH(input, sink->inputs, idx3) { + if (pa_streq(GET_ROLE(input), "phone")) { + /* There's a phone stream already attached, don't + * switch to A2DP */ + use_a2dp = FALSE; + } + } + } + + /* Make sure we don't have an active source on this card */ + if (card_has_active_source(i->sink->card)) + use_a2dp = FALSE; + + if (pa_streq(GET_ROLE(i), "phone")) { + pa_log_info("Got a phone stream, switching card '%s' to HSP/HFP profile", card->name); + pa_card_set_profile(card, "hsp", FALSE); + } else if (use_a2dp) { + pa_log_info("Got a non-phone stream, switching card '%s' to A2DP profile", card->name); + pa_card_set_profile(card, "a2dp", FALSE); + } + } + + return PA_HOOK_OK; +} + +static void process_unlink(pa_core *c, pa_sink *sink) { + pa_sink_input *input; + pa_bool_t use_a2dp = TRUE; + uint32_t idx; + + PA_IDXSET_FOREACH(input, sink->inputs, idx) { + if (pa_streq(GET_ROLE(input), "phone")) { + /* There's a phone stream attached, don't switch */ + use_a2dp = FALSE; + break; + } + } + + /* Make sure we don't have an active source on this card */ + if (card_has_active_source(sink->card)) + use_a2dp = FALSE; + + if (use_a2dp) { + pa_log_info("Lost all phone streams, switching card '%s' to A2DP profile", sink->card->name); + pa_card_set_profile(sink->card, "a2dp", FALSE); + } +} + +static pa_hook_result_t sink_input_unlink_post_cb(pa_core *c, pa_sink_input *i, void *userdata) { + pa_assert(c); + pa_assert(i); + + /* We only need to make changes when a "phone" stream goes away */ + if (!pa_streq(GET_ROLE(i), "phone") || !i->sink) + return PA_HOOK_OK; + + if (!card_is_bluetooth(i->sink->card)) + return PA_HOOK_OK; + + process_unlink(c, i->sink); + + return PA_HOOK_OK; +} + +/* We need this one in case a "phone" sink-input is detached while there's + * still a source-output connected to the device (since the latter would block + * a switch to A2DP in sink_input_unlink_post_cb()) */ +static pa_hook_result_t source_output_unlink_post_cb(pa_core *c, pa_source_output *o, void *userdata) { + pa_sink *sink; + uint32_t idx; + + pa_assert(c); + pa_assert(o); + + if (!o->source || !o->source->card) + return PA_HOOK_OK; + + if (!card_is_bluetooth(o->source->card)) + return PA_HOOK_OK; + + PA_IDXSET_FOREACH(sink, o->source->card->sinks, idx) { + process_unlink(c, sink); + } + + return PA_HOOK_OK; +} + +int pa__init(pa_module*m) { + pa_modargs *ma; + struct userdata *u; + + pa_assert(m); + + if (!(ma = pa_modargs_new(m->argument, valid_modargs))) { + pa_log("Failed to parse module arguments"); + return -1; + } + + m->userdata = u = pa_xnew(struct userdata, 1); + + /* Set these all to EARLY so we can trigger profile changes asap */ + u->sink_input_put_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SINK_INPUT_PUT], PA_HOOK_EARLY, (pa_hook_cb_t) sink_input_put_cb, u); + u->sink_input_unlink_post_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SINK_INPUT_UNLINK_POST], PA_HOOK_EARLY, (pa_hook_cb_t) sink_input_unlink_post_cb, u); + u->source_output_unlink_post_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SOURCE_OUTPUT_UNLINK_POST], PA_HOOK_EARLY, (pa_hook_cb_t) source_output_unlink_post_cb, u); + + pa_modargs_free(ma); + return 0; +} + +void pa__done(pa_module*m) { + struct userdata *u; + + pa_assert(m); + + if (!(u = m->userdata)) + return; + + if (u->sink_input_put_slot) + pa_hook_slot_free(u->sink_input_put_slot); + if (u->sink_input_unlink_post_slot) + pa_hook_slot_free(u->sink_input_unlink_post_slot); + if (u->source_output_unlink_post_slot) + pa_hook_slot_free(u->source_output_unlink_post_slot); + + pa_xfree(u); +} -- 1.7.8.4