summaryrefslogtreecommitdiffstats
path: root/plugins/lib/bluealsa
diff options
context:
space:
mode:
authorThierry Bultel <thierry.bultel@iot.bzh>2018-12-05 14:05:10 +0100
committerThierry Bultel <thierry.bultel@iot.bzh>2018-12-20 16:55:46 +0000
commit01d55ed8cdd01ca4a7b391ca61c80084bd5a6f2f (patch)
treed73ce778a2177e82e07e9d31891a827467fbadd4 /plugins/lib/bluealsa
parentdbe555be80dd10bb315b14bbebcb42f8b883e2c9 (diff)
Adds support for bluetooth audio through bluez-alsaguppy_6.99.4guppy_6.99.3guppy/6.99.4guppy/6.99.36.99.46.99.3
Implements a new bluealsa plugin to the HAL manager, reacting to the changes of the available transports. This plugin is linked with the new bluealsa.so shared library. New transports (SCO & A2DP) result in softmixer invocations of the "attach" verb, that creates the new capture (eg, A2DP capture from bluealsa ioplug PCM, SCO microphone capture), playbacks (SCO playback to a softmixer zone, and SCO output to bluealsa iogplug PCM). When a transport disappears, the hal manager calls the transaction deletion verb that will tell the softmixer to remove the created streams and associated objects. Change-Id: I36037a4f14ef7fee38070fc0df66c40b4ce46e8b Signed-off-by: Thierry Bultel <thierry.bultel@iot.bzh>
Diffstat (limited to 'plugins/lib/bluealsa')
-rw-r--r--plugins/lib/bluealsa/CMakeLists.txt51
-rw-r--r--plugins/lib/bluealsa/hal-bluealsa-transports.c153
-rw-r--r--plugins/lib/bluealsa/hal-bluealsa-transports.h53
-rw-r--r--plugins/lib/bluealsa/hal-bluealsa-watch.h29
-rw-r--r--plugins/lib/bluealsa/hal-bluealsa.c723
-rw-r--r--plugins/lib/bluealsa/hal-bluealsa.h21
6 files changed, 1030 insertions, 0 deletions
diff --git a/plugins/lib/bluealsa/CMakeLists.txt b/plugins/lib/bluealsa/CMakeLists.txt
new file mode 100644
index 0000000..699ccc4
--- /dev/null
+++ b/plugins/lib/bluealsa/CMakeLists.txt
@@ -0,0 +1,51 @@
+###########################################################################
+# Copyright 2018 IoT.bzh
+#
+# author: Thierry Bultel <thierry.bultel@iot.bzh>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+###########################################################################
+
+
+PROJECT_TARGET_ADD(hal-bluealsa)
+
+ pkg_check_modules(BLUEALSA REQUIRED bluealsa>=1.3.1)
+
+ # Define targets
+ ADD_LIBRARY(${TARGET_NAME} MODULE
+ hal-bluealsa.c
+ hal-bluealsa-transports.c
+ )
+
+ # Alsa Plugin properties
+ SET_TARGET_PROPERTIES(${TARGET_NAME} PROPERTIES
+ LABELS "PLUGIN"
+ PREFIX ""
+ SUFFIX ".ctlso"
+ OUTPUT_NAME ${TARGET_NAME}
+ )
+
+ # Library dependencies (include updates automatically)
+ TARGET_LINK_LIBRARIES(${TARGET_NAME}
+ afb-helpers
+ ctl-utilities
+ ${BLUEALSA_LIBRARIES}
+ ${link_libraries}
+ )
+
+ TARGET_COMPILE_OPTIONS(${TARGET_NAME} PUBLIC ${BLUEALSA_CFLAGS})
+
+ target_include_directories(${TARGET_NAME}
+ PRIVATE "${CMAKE_SOURCE_DIR}/app-controller/ctl-lib"
+ PRIVATE "${CMAKE_SOURCE_DIR}/4a-hal/4a-hal-utilities"
+ )
diff --git a/plugins/lib/bluealsa/hal-bluealsa-transports.c b/plugins/lib/bluealsa/hal-bluealsa-transports.c
new file mode 100644
index 0000000..2c58023
--- /dev/null
+++ b/plugins/lib/bluealsa/hal-bluealsa-transports.c
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2018 "IoT.bzh"
+ * Author : Thierry Bultel <thierry.bultel@iot.bzh>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <malloc.h>
+
+#include "hal-bluealsa-transports.h"
+
+
+static bool halBlueAlsaTransportMatch(
+ const struct ba_msg_transport * transport1,
+ const struct ba_msg_transport * transport2);
+
+int halBlueAlsaTransportsInit(bluealsa_transport_t * list) {
+ CDS_INIT_LIST_HEAD(&list->list);
+ return 0;
+}
+
+bluealsa_transport_t * halBlueAlsaTransportsAdd(
+ const bluealsa_watch * watch,
+ bluealsa_transport_t * list,
+ const struct ba_msg_transport * transport) {
+
+ bluealsa_transport_t * newTransport = (bluealsa_transport_t*) calloc(1, sizeof(bluealsa_transport_t));
+ if (newTransport == NULL)
+ goto fail;
+
+ memcpy(&newTransport->transport, transport, sizeof(struct ba_msg_transport));
+
+ newTransport->watch = watch;
+
+ CDS_INIT_LIST_HEAD(&newTransport->list);
+ cds_list_add(&newTransport->list, &list->list);
+
+ return newTransport;
+fail:
+ return NULL;
+
+}
+
+bool halBlueAlsaTransportFind(
+ const bluealsa_watch * watch,
+ bluealsa_transport_t * list,
+ const struct ba_msg_transport * transport) {
+
+ bluealsa_transport_t *tmp;
+ bool found = false;
+ cds_list_for_each_entry(tmp, &list->list, list) {
+ struct ba_msg_transport * _transport = &tmp->transport;
+
+ if (watch != tmp->watch)
+ continue;
+
+ if (!halBlueAlsaTransportMatch(_transport, transport))
+ continue;
+
+ found = true;
+ break;
+ }
+ return found;
+}
+
+
+int halBlueAlsaTransportUpdate(
+ const bluealsa_watch * watch,
+ bluealsa_transport_t * list,
+ const struct ba_msg_transport *transports,
+ size_t nb,
+ transport_destructor destructor) {
+
+ bluealsa_transport_t *transport, *sav;
+
+ cds_list_for_each_entry_safe(transport, sav, &list->list, list) {
+ struct ba_msg_transport * ba_transport1 = &transport->transport;
+
+ if (watch != transport->watch)
+ continue;
+
+ bool found = false;
+
+ for (int ix = 0; ix<nb; ix++) {
+ const struct ba_msg_transport * ba_transport2 = &transports[ix];
+
+ if (halBlueAlsaTransportMatch(ba_transport1, ba_transport2)) {
+ found = true;
+ break;
+ }
+ }
+
+ if (found)
+ continue;
+
+ char transportS[HAL_BLUEALSA_TRANSPORT_LEN_MAX];
+
+ AFB_ApiInfo(watch->plugin->api,
+ "Unregister transport %s",
+ halBlueAlsaTransportAsString(transportS, HAL_BLUEALSA_TRANSPORT_LEN_MAX, &transport->transport));
+
+ cds_list_del(&transport->list);
+ if (destructor) {
+ destructor(transport);
+ }
+
+ free(transport->transactionUidS);
+ free(transport);
+
+ }
+
+ return 0;
+}
+
+
+static bool halBlueAlsaTransportMatch(const struct ba_msg_transport * transport1, const struct ba_msg_transport * transport2) {
+ bool ret = false;
+
+ if (transport1->type != transport2->type)
+ goto done;
+
+ if (transport1->stream != transport2->stream)
+ goto done;
+
+ if (bacmp(&transport1->addr, &transport2->addr) != 0) {
+ goto done;
+ }
+ ret = true;
+done:
+ return ret;
+}
+
+
+char * halBlueAlsaTransportAsString(char * buff, size_t len, const struct ba_msg_transport * transport) {
+ char addr[18];
+
+ ba2str(&transport->addr, addr);
+ snprintf(buff, len, "%s,%s,%s", addr,
+ transport->type == BA_PCM_TYPE_A2DP?"a2dp":"sco",
+ transport->stream == BA_PCM_STREAM_PLAYBACK?"playback":"capture");
+
+ return buff;
+}
diff --git a/plugins/lib/bluealsa/hal-bluealsa-transports.h b/plugins/lib/bluealsa/hal-bluealsa-transports.h
new file mode 100644
index 0000000..0c5d6a4
--- /dev/null
+++ b/plugins/lib/bluealsa/hal-bluealsa-transports.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2018 "IoT.bzh"
+ * Author : Thierry Bultel <thierry.bultel@iot.bzh>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef __INC_HAL_BLUEALSA_TRANSPORT_H
+#define __INC_HAL_BLUEALSA_TRANSPORT_H
+
+#include <bluealsa/bluealsa.h>
+#include <stdbool.h>
+#include <urcu/list.h>
+
+#include "afb-definitions.h"
+#include "hal-bluealsa-watch.h"
+
+typedef struct {
+ const bluealsa_watch * watch; /* owner */
+ struct cds_list_head list;
+ struct ba_msg_transport transport;
+ char * transactionUidS;
+} bluealsa_transport_t;
+
+extern int halBlueAlsaTransportsInit(bluealsa_transport_t * list);
+
+extern bluealsa_transport_t * halBlueAlsaTransportsAdd(const bluealsa_watch * watch, bluealsa_transport_t * list, const struct ba_msg_transport * transport);
+extern bool halBlueAlsaTransportFind(const bluealsa_watch * watch, bluealsa_transport_t * list, const struct ba_msg_transport * transport);
+
+typedef int (*transport_destructor)(bluealsa_transport_t *);
+
+extern int halBlueAlsaTransportUpdate(
+ const bluealsa_watch * watch,
+ bluealsa_transport_t * list,
+ const struct ba_msg_transport *transports,
+ size_t nb,
+ transport_destructor);
+
+#define HAL_BLUEALSA_TRANSPORT_LEN_MAX 64
+
+extern char * halBlueAlsaTransportAsString(char * s, size_t len, const struct ba_msg_transport * );
+
+#endif /* __INC_HAL_BLUEALSA_TRANSPORT_H */
diff --git a/plugins/lib/bluealsa/hal-bluealsa-watch.h b/plugins/lib/bluealsa/hal-bluealsa-watch.h
new file mode 100644
index 0000000..114f515
--- /dev/null
+++ b/plugins/lib/bluealsa/hal-bluealsa-watch.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2018 "IoT.bzh"
+ * Author : Thierry Bultel <thierry.bultel@iot.bzh>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef __INC_HAL_BLUEALSA_WATCH_H
+#define __INC_HAL_BLUEALSA_WATCH_H
+
+#include <ctl-config.h>
+
+typedef struct {
+ int fd;
+ const char * interface;
+ CtlPluginT * plugin;
+} bluealsa_watch;
+
+#endif /* __INC_HAL_BLUEALSA_WATCH_H */
diff --git a/plugins/lib/bluealsa/hal-bluealsa.c b/plugins/lib/bluealsa/hal-bluealsa.c
new file mode 100644
index 0000000..c8cfe98
--- /dev/null
+++ b/plugins/lib/bluealsa/hal-bluealsa.c
@@ -0,0 +1,723 @@
+/*
+ * Copyright (C) 2018 "IoT.bzh"
+ * Author : Thierry Bultel <thierry.bultel@iot.bzh>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <unistd.h>
+
+#include <wrap-json.h>
+
+#include <errno.h>
+#include <ctl-config.h>
+#include <systemd/sd-event.h>
+#include <bluealsa/bluealsa.h>
+
+#include "hal-bluealsa.h"
+
+#define HAL_BLUEALSA_PLUGIN_NAME "hal-bluealsa"
+
+#define SCO_TALK_RATE 44100
+#define SCO_TALK_FORMAT "S16_LE"
+
+#define SCO_TALK_ZONE "sco_talk_zone"
+#define SCO_CHANNEL_MONO "sco-mono"
+
+/* forward static declarations */
+static int halBlueAlsaTransportEventCB(sd_event_source* src, int fd, uint32_t revents, void* userData);
+static int halBlueAlsaFetchTransports(bluealsa_watch * plugin);
+static int halBlueAlsaRegisterAll(CtlPluginT* plugin);
+static int halBlueAlsaRegister(CtlPluginT* plugin, const char * interface);
+
+CTLP_CAPI_REGISTER(HAL_BLUEALSA_PLUGIN_NAME)
+
+
+// Call at initialization time ('requires' are forbidden at this stage)
+CTLP_ONLOAD(plugin, callbacks)
+{
+ AFB_ApiNotice(plugin->api, "%s Plugin Registered correctly: uid='%s' 'info='%s'", HAL_BLUEALSA_PLUGIN_NAME, plugin->uid, plugin->info);
+ return 0;
+}
+
+
+CTLP_INIT(plugin, callbacks)
+{
+ json_object *actionsToAdd = NULL;
+ CtlConfigT *ctrlConfig;
+
+ wrap_json_pack(&actionsToAdd, "{s:s s:s s:s}",
+ "uid", "init-bluealsa-plugin",
+ "info", "Init Bluez-Alsa hal plugin",
+ "action", "plugin://hal-bluealsa#init");
+
+ if (!(ctrlConfig = (CtlConfigT *) AFB_ApiGetUserData(plugin->api))) {
+ AFB_ApiError(plugin->api, "Can't get current hal controller config");
+ goto fail;
+ }
+
+ int idx = 0;
+ while (ctrlConfig->sections[idx].key && strcasecmp(ctrlConfig->sections[idx].key, "onload"))
+ idx++;
+
+ if (!ctrlConfig->sections[idx].key) {
+ AFB_ApiError(plugin->api, "Wasn't able to add '%s' as a new onload, 'onload' section not found", json_object_get_string(actionsToAdd));
+ goto fail;
+ }
+
+ if(AddActionsToSection(plugin->api, &ctrlConfig->sections[idx], actionsToAdd, 0)) {
+ AFB_ApiError(plugin->api, "Wasn't able to add '%s' as a new onload to %s", json_object_get_string(actionsToAdd), ctrlConfig->sections[idx].uid);
+ goto fail;
+ }
+
+ AFB_ApiNotice(plugin->api, "Plugin initialization of %s plugin correctly done", HAL_BLUEALSA_PLUGIN_NAME);
+
+ return 0;
+fail:
+ json_object_put(actionsToAdd);
+ return -1;
+}
+
+
+// Call at controller onload time
+CTLP_CAPI(init, source, argsJ, queryJ)
+{
+ AFB_ApiNotice(source->api, "Controller onload event");
+
+ CtlPluginT * plugin = source->plugin;
+
+ hal_bluealsa_plugin_data_t * pluginData = (hal_bluealsa_plugin_data_t*) calloc(1, sizeof(hal_bluealsa_plugin_data_t));
+ int error;
+
+ /*
+ * "params": {
+ "sco": {
+ "mic": "2CH-GENERIC-USB",
+ "zone": "full-stereo",
+ "delayms": 300
+ },
+ "a2dp": {
+ "zone": "full-stereo",
+ "delayms": 1000
+ }
+ }
+ */
+
+ json_object * scoParamsJ = NULL;
+ json_object * a2dpParamsJ = NULL;
+
+ error = wrap_json_unpack(plugin->paramsJ, "{s?o,s?o !}",
+ "sco", &scoParamsJ,
+ "a2dp", &a2dpParamsJ);
+
+ if (error) {
+ AFB_ApiError(plugin->api, "%s: wrong parameters", __func__);
+ goto fail;
+ }
+
+ if (scoParamsJ) {
+ AFB_ApiInfo(plugin->api, "%s: sco parameters: %s", __func__, json_object_get_string(scoParamsJ));
+ error = wrap_json_unpack(scoParamsJ, "{s:s,s:s,s?i !}",
+ "mic", &pluginData->sco.mic,
+ "zone", &pluginData->sco.speaker,
+ "delayms", &pluginData->sco.delayms);
+ if (error) {
+ AFB_ApiError(plugin->api, "%s: wrong sco parameters: err %s", __func__, wrap_json_get_error_string(error));
+ goto fail;
+ }
+ }
+
+ if (a2dpParamsJ) {
+ AFB_ApiInfo(plugin->api, "%s: a2dp parameters: %s", __func__, json_object_get_string(a2dpParamsJ));
+ error = wrap_json_unpack(a2dpParamsJ, "{s:s,s?i !}",
+ "zone", &pluginData->a2dp.zone,
+ "delayms", &pluginData->a2dp.delayms);
+ if (error) {
+ AFB_ApiError(plugin->api, "%s: wrong a2dp parameters: err=%s", __func__, wrap_json_get_error_string(error));
+ goto fail;
+ }
+ }
+
+ halBlueAlsaTransportsInit(&pluginData->transport_list);
+
+ setPluginContext(plugin, pluginData);
+
+ if (halBlueAlsaRegisterAll(plugin) != 0)
+ goto fail;
+
+ return 0;
+fail:
+ return -1;
+}
+
+
+static int halBlueAlsaTransportEventCB(sd_event_source* src, int fd, uint32_t revents, void* userData) {
+
+ bluealsa_watch * watch = (bluealsa_watch *)userData;
+ CtlPluginT * plugin = watch->plugin;
+
+ struct ba_msg_event event;
+ ssize_t ret;
+
+ AFB_ApiDebug(plugin->api, "--- %s ----!", __func__);
+
+ if ((revents & EPOLLIN) == 0)
+ goto done;
+
+ if (revents & EPOLLHUP) {
+ AFB_ApiInfo(plugin->api, "Lost connection with bluealsa on interface %s", watch->interface);
+ sd_event_source_unref(src);
+ close(fd);
+ halBlueAlsaRegister(plugin, watch->interface);
+ goto done;
+ }
+
+ while ((ret = recv(watch->fd, &event, sizeof(event), MSG_DONTWAIT)) == -1 && errno == EINTR)
+ continue;
+
+ if (ret != sizeof(event)) {
+ AFB_ApiError(plugin->api, "Couldn't read event: %s", strerror(ret == -1 ? errno : EBADMSG));
+ goto done;
+ }
+
+ halBlueAlsaFetchTransports(watch);
+
+done:
+ return 0;
+}
+
+
+/*
+ *
+ * The following function builds that kind of json data and send it to smixer
+ *
+ * A2DP Case:
+ *
+ * {
+ "uid":"a2dp_F6:32:15:2A:80:70",
+ "captures":
+ {
+ "uid":"a2dp_listen_capture",
+ "pcmplug_params" : "bluealsa:HCI=hci0,DEV=F6:32:15:2A:80:70,PROFILE=a2dp",
+ "source" : {
+ "channels": [
+ {
+ "uid": "a2dp-right",
+ "port": 0
+ },
+ {
+ "uid": "a2dp-left",
+ "port": 1
+ }
+ ]
+ }
+ },
+ "streams":
+ {
+ "uid" : "a2dp_listen_stream",
+ "source":"a2dp_listen_capture",
+ "zone": "full-stereo"
+ }
+ }
+ *
+ *-----------------------------------------------------------------------------------
+ *
+ * SCO Case:
+ *
+ * {
+ "uid":"sco_F6:32:15:2A:80:70",",
+ "captures":
+ {
+ "uid":"sco_listen_capture",
+ "pcmplug_params" : "bluealsa:HCI=hci0,DEV=F6:32:15:2A:80:70,PROFILE=sco",
+ "source" : {
+ "channels": [
+ {
+ "uid": "sco-right",
+ "port": 0
+ },
+ {
+ "uid": "sco-left",
+ "port": 1
+ }
+ ]
+ }
+ },
+ "playbacks" :
+ {
+ "uid"="sco_talk_playback",
+ "pcmplug_params" : "bluealsa:HCI=hci0,DEV=F6:32:15:2A:80:70,PROFILE=sco",
+ "params": {
+ "rate": 44100,
+ "format": "S16_LE",
+ },
+ "sink": {
+ "channels": [
+ {
+ "uid": "sco-mono"
+ }
+ ]
+ }
+ },
+ "zones" : [
+ {
+ "uid": "sco_talk_zone",
+ "sink": [
+ {
+ "target": "sco-mono",
+ "channel": 0,
+ "volume": 0.5
+ },
+ {
+ "target": "sco-mono",
+ "channel": 1,
+ "volume": 0.5
+ }
+ ]
+ }
+ ] ,
+ "streams": [
+ {
+ "uid" : "sco_listen_stream",
+ "source":"sco_listen_capture",
+ "zone": "full-stereo",
+ "delayms": 300
+ },
+ {
+ "uid" : "sco_talk_stream",
+ "source": "2CH-GENERIC-USB",
+ "zone": "sco_talk_zone",
+ "delayms": 300
+ }
+ ]
+ }
+ *
+ * */
+
+static json_object* halBlueAlsaA2DPTransportChannels(const char * transportTypeS) {
+ json_object* channelRightJ;
+ json_object* channelLeftJ;
+ json_object* channelsJ = json_object_new_array();
+ char * channelRightS;
+ char * channelLeftS;
+
+ channelRightJ = json_object_new_object();
+ if (asprintf(&channelRightS, "%s-right", transportTypeS) == -1)
+ goto fail;
+
+ json_object_object_add(channelRightJ, "uid", json_object_new_string(channelRightS));
+ json_object_object_add(channelRightJ, "port", json_object_new_int(0));
+
+ channelLeftJ = json_object_new_object();
+ if (asprintf(&channelLeftS, "%s-left", transportTypeS) == -1)
+ goto fail;
+
+ json_object_object_add(channelLeftJ, "uid", json_object_new_string(channelLeftS));
+ json_object_object_add(channelLeftJ, "port", json_object_new_int(1));
+
+ json_object_array_add(channelsJ, channelRightJ);
+ json_object_array_add(channelsJ, channelLeftJ);
+fail:
+ return channelsJ;
+}
+
+
+static json_object* halBlueAlsaSCOTransportChannels(const char * transportTypeS) {
+ json_object* channelMonoJ;
+
+ json_object* channelsJ = json_object_new_array();
+ char * channelMonoS;
+
+ channelMonoJ = json_object_new_object();
+ if (asprintf(&channelMonoS, "%s-mono", transportTypeS) == -1)
+ goto fail;
+
+ json_object_object_add(channelMonoJ, "uid", json_object_new_string(channelMonoS));
+ json_object_object_add(channelMonoJ, "port", json_object_new_int(0));
+
+ json_object_array_add(channelsJ, channelMonoJ);
+
+fail:
+ return channelsJ;
+}
+
+
+static json_object* halBlueAlsaPcmPlugParams(const char * interface, const char * addr, const char * transportTypeS) {
+ char * pcmplug_paramsS = NULL;
+ if (asprintf(&pcmplug_paramsS, "bluealsa:HCI=%s,DEV=%s,PROFILE=%s", interface, addr, transportTypeS) == -1)
+ goto fail;
+ return json_object_new_string(pcmplug_paramsS);
+fail:
+ return NULL;
+}
+
+
+static json_object* halBlueAlsaListenCapture(
+ const char * listenCaptureS,
+ json_object * pcmplugParamsJ,
+ json_object * sourceJ) {
+
+ json_object * captureJ = json_object_new_object();
+
+ json_object_object_add(captureJ, "uid", json_object_new_string(listenCaptureS));
+ json_object_object_add(captureJ, "pcmplug_params", pcmplugParamsJ);
+ json_object_object_add(captureJ, "source", sourceJ);
+
+ return captureJ;
+}
+
+static json_object * halBlueAlsaTalkPlayback(
+ const char * talkPlaybackS,
+ json_object* pcmplugParamsJ,
+ json_object* paramsJ,
+ json_object* sinkJ ) {
+
+ json_object * playbackJ = json_object_new_object();
+
+ json_object_object_add(playbackJ, "uid", json_object_new_string(talkPlaybackS));
+ json_object_object_add(playbackJ, "pcmplug_params", pcmplugParamsJ);
+ json_object_object_add(playbackJ, "params", paramsJ);
+ json_object_object_add(playbackJ, "sink", sinkJ);
+
+ return playbackJ;
+}
+
+static json_object * halBlueAlsaScoTalkParamsJ() {
+ json_object * paramsJ = json_object_new_object();
+
+ json_object_object_add(paramsJ, "rate", json_object_new_int(SCO_TALK_RATE));
+ json_object_object_add(paramsJ, "format", json_object_new_string(SCO_TALK_FORMAT));
+ return paramsJ;
+}
+
+static json_object * halBlueAlsaScoZone() {
+ json_object * zoneJ = json_object_new_object();
+ json_object * sinkJ = json_object_new_array();
+ json_object_object_add(zoneJ, "uid", json_object_new_string(SCO_TALK_ZONE));
+
+ json_object * channel1J = json_object_new_object();
+ json_object_object_add(channel1J, "target" , json_object_new_string(SCO_CHANNEL_MONO));
+ json_object_object_add(channel1J, "channel", json_object_new_int(0));
+ json_object_object_add(channel1J, "volume", json_object_new_double(0.5));
+
+ json_object * channel2J = json_object_new_object();
+ json_object_object_add(channel2J, "target" , json_object_new_string(SCO_CHANNEL_MONO));
+ json_object_object_add(channel2J, "channel", json_object_new_int(1));
+ json_object_object_add(channel2J, "volume", json_object_new_double(0.5));
+
+ json_object_array_add(sinkJ, channel1J);
+ json_object_array_add(sinkJ, channel2J);
+
+ json_object_object_add(zoneJ, "sink", sinkJ);
+
+ return zoneJ;
+}
+
+static int halBlueAlsaAttachTransportStreams(bluealsa_transport_t * transport) {
+ int ret = -1;
+ const char* transportTypeS;
+
+ struct ba_msg_transport * ba_transport = &transport->transport;
+ const bluealsa_watch * watch = transport->watch;
+
+ CtlPluginT* plugin = watch->plugin;
+ hal_bluealsa_plugin_data_t * pluginData=getPluginContext(plugin);
+
+ json_object* requestJ = NULL;
+ json_object* returnJ = NULL;
+
+ json_object* sourceJ = NULL;
+ json_object* playbackJ = NULL;
+
+ json_object* pcmplugParamsJ, *pcmplugParamsJ2 = NULL;
+
+ json_object* streamsJ = NULL;
+ json_object* streamJ = NULL;
+
+ json_object* zonesJ = NULL;
+
+ char * playbackZoneS;
+ uint32_t delayms;
+
+ if (ba_transport->type == BA_PCM_TYPE_SCO) {
+ transportTypeS = "sco";
+ delayms = pluginData->sco.delayms;
+ playbackZoneS = pluginData->sco.speaker;
+ }
+ else if (ba_transport->type == BA_PCM_TYPE_A2DP) {
+ transportTypeS = "a2dp";
+ delayms = pluginData->a2dp.delayms;
+ playbackZoneS = pluginData->a2dp.zone;
+ } else {
+ AFB_ApiError(plugin->api, "%s: unsupported transport type", __func__ );
+ goto fail;
+ }
+
+ char * captureS = NULL;
+ if (asprintf(&captureS, "%s_listen_capture", transportTypeS) == -1)
+ goto fail;
+
+ // for SCO only
+ char * playbackS = NULL;
+ if (asprintf(&playbackS, "%s_talk_playback", transportTypeS) == -1)
+ goto fail;
+
+ char * transactionUidS = NULL;
+ char * streamS = NULL;
+
+ char addr[18];
+ ba2str(&ba_transport->addr, addr);
+
+ requestJ = json_object_new_object();
+ if (!requestJ)
+ goto fail;
+
+ if (asprintf(&transactionUidS, "%s_%s", transportTypeS, addr) == -1)
+ goto fail;
+
+ json_object_object_add(requestJ, "uid", json_object_new_string(transactionUidS));
+
+ pcmplugParamsJ = halBlueAlsaPcmPlugParams(watch->interface, addr, transportTypeS);
+
+ sourceJ = json_object_new_object();
+ if (ba_transport->type == BA_PCM_TYPE_A2DP)
+ json_object_object_add(sourceJ, "channels", halBlueAlsaA2DPTransportChannels(transportTypeS));
+ else
+ json_object_object_add(sourceJ, "channels", halBlueAlsaSCOTransportChannels(transportTypeS));
+
+ json_object_object_add(requestJ, "captures", halBlueAlsaListenCapture(captureS, pcmplugParamsJ, sourceJ));
+
+ if (ba_transport->type == BA_PCM_TYPE_SCO) {
+ playbackJ = json_object_new_object();
+ // that is a shame that deep copy in not available in the current json-c version
+ pcmplugParamsJ2 = halBlueAlsaPcmPlugParams(watch->interface, addr, transportTypeS);
+ json_object * paramsJ = halBlueAlsaScoTalkParamsJ();
+
+ json_object_object_add(playbackJ, "channels", halBlueAlsaSCOTransportChannels(transportTypeS));
+ json_object_object_add(requestJ, "playbacks", halBlueAlsaTalkPlayback(playbackS, pcmplugParamsJ2, paramsJ, playbackJ));
+
+ }
+
+ /* ZONES */
+
+ if (ba_transport->type == BA_PCM_TYPE_SCO) {
+ zonesJ = json_object_new_array();
+ json_object_array_add(zonesJ, halBlueAlsaScoZone());
+ json_object_object_add(requestJ, "zones", zonesJ);
+ }
+
+ /* STREAMS */
+
+ /* Build the array of streams */
+ streamsJ = json_object_new_array();
+
+ streamJ = json_object_new_object();
+ if (asprintf(&streamS, "%s_listen_stream", transportTypeS) == -1)
+ goto fail;
+
+ json_object_object_add(streamJ, "uid", json_object_new_string(streamS));
+ json_object_object_add(streamJ, "source", json_object_new_string(captureS));
+ json_object_object_add(streamJ, "zone", json_object_new_string(playbackZoneS));
+ if (delayms != 0) {
+ json_object_object_add(streamJ, "delayms", json_object_new_int(delayms));
+ }
+
+ json_object_array_add(streamsJ, streamJ);
+
+ /* In case of SCO, to have full-duplex, we instantiate a stream for talk */
+ if (ba_transport->type == BA_PCM_TYPE_SCO ) {
+
+ streamJ = json_object_new_object();
+ if (asprintf(&streamS, "%s_talk_stream", transportTypeS) == -1)
+ goto fail;
+
+ json_object_object_add(streamJ, "uid", json_object_new_string(streamS));
+ json_object_object_add(streamJ, "source", json_object_new_string(pluginData->sco.mic));
+ json_object_object_add(streamJ, "zone", json_object_new_string(SCO_TALK_ZONE));
+ if (delayms != 0)
+ json_object_object_add(streamJ, "delayms", json_object_new_int(delayms));
+ json_object_array_add(streamsJ, streamJ);
+ }
+
+ json_object_object_add(requestJ, "streams", streamsJ);
+
+ /* In softmixer, this will create a transaction verb (whose name is transactionUidS),
+ * will be used later to destroy all the created objects upon transport removal */
+
+ if (AFB_ServiceSync(plugin->api, SMIXER_API_NAME, "attach", requestJ, &returnJ)) {
+ AFB_ApiError(plugin->api, "Error calling attach verb of mixer" );
+ goto done;
+ }
+
+ transport->transactionUidS = transactionUidS;
+
+ if (returnJ)
+ json_object_put(returnJ);
+
+ ret = 0;
+ goto done;
+
+fail:
+ return -1;
+done:
+ AFB_ApiDebug(plugin->api, "DONE.");
+ return ret;
+}
+
+
+static int halBluezAlsaRemoveTransportStream(bluealsa_transport_t * transport) {
+
+ CtlPluginT * plugin = transport->watch->plugin;
+ json_object* requestJ = NULL;
+ json_object* returnJ = NULL;
+
+ AFB_ApiInfo(plugin->api, "Call transaction detach verb %s", transport->transactionUidS);
+ if (transport->transactionUidS == NULL)
+ goto fail;
+
+ requestJ = json_object_new_object();
+ if (!requestJ)
+ goto fail;
+
+ json_object_object_add(requestJ, "action", json_object_new_string("remove"));
+
+ if (AFB_ServiceSync(plugin->api, SMIXER_API_NAME, transport->transactionUidS, requestJ, &returnJ)) {
+ AFB_ApiError(plugin->api, "Error calling attach verb of mixer" );
+ goto fail;
+ }
+
+ if (returnJ)
+ json_object_put(returnJ);
+
+ return 0;
+
+fail:
+ return -1;
+}
+
+static int halBlueAlsaFetchTransports(bluealsa_watch * watch) {
+ ssize_t nbTransports;
+ struct ba_msg_transport *transports;
+ CtlPluginT * plugin = watch->plugin;
+
+ hal_bluealsa_plugin_data_t * pluginData = (hal_bluealsa_plugin_data_t*)getPluginContext(plugin);
+ bluealsa_transport_t * transport_list = &pluginData->transport_list;
+ bluealsa_transport_t * transport = NULL;
+
+ AFB_ApiDebug(plugin->api, "Fetching available transports of interface %s", watch->interface);
+
+ if ((nbTransports = bluealsa_get_transports(watch->fd, &transports)) == -1) {
+ AFB_ApiError(plugin->api, "Couldn't get transports: %s", strerror(errno));
+ goto done;
+ }
+
+ AFB_ApiDebug(plugin->api, "Got %zu transport(s)", nbTransports);
+
+ for (int ix=0; ix<nbTransports; ix++) {
+ char addr[18];
+ struct ba_msg_transport * ba_transport = &transports[ix];
+ ba2str(&ba_transport->addr, addr);
+ const char * typeS;
+ if (ba_transport->type == BA_PCM_TYPE_SCO)
+ typeS = "sco";
+ else if (ba_transport->type == BA_PCM_TYPE_A2DP)
+ typeS = "a2dp";
+ else
+ typeS = "unknown";
+
+ AFB_ApiDebug(plugin->api, "Transport %d: type %s, dev %s", ix, typeS, addr);
+
+ if (halBlueAlsaTransportFind(watch, transport_list, ba_transport)) {
+ AFB_ApiDebug(plugin->api, "This transport is already streamed");
+ continue;
+ }
+
+ AFB_ApiInfo(plugin->api, "Registering transport type %s, dev %s", typeS, addr);
+ transport = halBlueAlsaTransportsAdd(watch, transport_list, ba_transport);
+ if (transport == NULL) {
+ AFB_ApiError(plugin->api, "Failed to register this transport");
+ goto done;
+ }
+
+ // Do the softmixer stuff
+ if (halBlueAlsaAttachTransportStreams(transport) != 0) {
+ AFB_ApiError(plugin->api, "Failed create transport streams");
+ goto done;
+ }
+
+ AFB_ApiDebug(plugin->api, "%s: transaction id %s", __func__, transport->transactionUidS);
+
+ }
+
+ halBlueAlsaTransportUpdate(watch, transport_list, transports, nbTransports , halBluezAlsaRemoveTransportStream);
+
+done:
+ free(transports);
+ return 0;
+}
+
+static int halBlueAlsaRegister(CtlPluginT* plugin, const char * interface) {
+ int ret;
+ sd_event *sdLoop;
+ sd_event_source* evtsrc;
+
+ sdLoop = AFB_GetEventLoop(plugin->api);
+
+ enum ba_event transport_mask = BA_EVENT_TRANSPORT_ADDED |
+// BA_EVENT_TRANSPORT_CHANGED |
+ BA_EVENT_TRANSPORT_REMOVED;
+
+ bluealsa_watch * watch = (bluealsa_watch *)malloc(sizeof(bluealsa_watch));
+ if (watch == NULL)
+ goto fail;
+
+ watch->interface = interface;
+ watch->plugin = plugin;
+
+ if ((watch->fd = bluealsa_open(interface)) == -1) {
+ AFB_ApiError(plugin->api, "BlueALSA connection failed: %s", strerror(errno));
+ goto fail;
+ }
+
+ halBlueAlsaFetchTransports(watch);
+
+ if (bluealsa_subscribe(watch->fd, transport_mask) == -1) {
+ AFB_ApiError(plugin->api, "BlueALSA subscription failed: %s", strerror(errno));
+ goto fail;
+ }
+
+ // Register sound event to the main loop of the binder
+ if ((ret = sd_event_add_io(sdLoop, &evtsrc, watch->fd, EPOLLIN, halBlueAlsaTransportEventCB, watch)) < 0) {
+ AFB_ApiError(plugin->api,
+ "%s: Failed to register event fd to io loop",
+ __func__);
+ goto fail;
+ }
+ return 0;
+
+fail:
+ if (watch->fd)
+ close(watch->fd);
+ if (watch)
+ free(watch);
+ return -1;
+}
+
+
+static int halBlueAlsaRegisterAll(CtlPluginT* plugin) {
+ /* TODO add a watch to all the available interfaces */
+ halBlueAlsaRegister(plugin, "hci0");
+ return 0;
+}
diff --git a/plugins/lib/bluealsa/hal-bluealsa.h b/plugins/lib/bluealsa/hal-bluealsa.h
new file mode 100644
index 0000000..86fe45a
--- /dev/null
+++ b/plugins/lib/bluealsa/hal-bluealsa.h
@@ -0,0 +1,21 @@
+#ifndef __INC_HAL_BLUEALSA_H
+#define __INC_HAL_BLUEALSA_H
+
+#include "hal-bluealsa-transports.h"
+
+typedef struct {
+ bluealsa_transport_t transport_list;
+ struct {
+ char * mic; // capture for talking
+ char * speaker; // playback zone
+ uint32_t delayms;
+ } sco;
+ struct {
+ char * zone;
+ uint32_t delayms;
+ } a2dp;
+} hal_bluealsa_plugin_data_t;
+
+#define SMIXER_API_NAME "smixer"
+
+#endif /* __INC_HAL_BLUEALSA_H */