diff options
Diffstat (limited to 'plugins/lib/bluealsa/hal-bluealsa.c')
-rw-r--r-- | plugins/lib/bluealsa/hal-bluealsa.c | 723 |
1 files changed, 723 insertions, 0 deletions
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; +} |