From 01d55ed8cdd01ca4a7b391ca61c80084bd5a6f2f Mon Sep 17 00:00:00 2001 From: Thierry Bultel Date: Wed, 5 Dec 2018 14:05:10 +0100 Subject: Adds support for bluetooth audio through bluez-alsa 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 --- plugins/lib/bluealsa/CMakeLists.txt | 51 ++ plugins/lib/bluealsa/hal-bluealsa-transports.c | 153 ++++++ plugins/lib/bluealsa/hal-bluealsa-transports.h | 53 ++ plugins/lib/bluealsa/hal-bluealsa-watch.h | 29 + plugins/lib/bluealsa/hal-bluealsa.c | 723 +++++++++++++++++++++++++ plugins/lib/bluealsa/hal-bluealsa.h | 21 + 6 files changed, 1030 insertions(+) create mode 100644 plugins/lib/bluealsa/CMakeLists.txt create mode 100644 plugins/lib/bluealsa/hal-bluealsa-transports.c create mode 100644 plugins/lib/bluealsa/hal-bluealsa-transports.h create mode 100644 plugins/lib/bluealsa/hal-bluealsa-watch.h create mode 100644 plugins/lib/bluealsa/hal-bluealsa.c create mode 100644 plugins/lib/bluealsa/hal-bluealsa.h (limited to 'plugins/lib/bluealsa') 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 +# +# 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 + * + * 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 + +#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; ixplugin->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 + * + * 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 +#include +#include + +#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 + * + * 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 + +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 + * + * 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 +#include + +#include + +#include +#include +#include +#include + +#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; ixaddr, 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 */ -- cgit 1.2.3-korg