Skip to content

Commit

Permalink
Optional Bluetooth LE advertisement for BLE-MIDI
Browse files Browse the repository at this point in the history
  • Loading branch information
arkq committed Jan 29, 2024
1 parent e35ddaf commit 5ca058a
Show file tree
Hide file tree
Showing 10 changed files with 327 additions and 57 deletions.
27 changes: 17 additions & 10 deletions doc/bluealsa.8.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ bluealsa
Bluetooth Audio ALSA Backend
----------------------------

:Date: July 2023
:Date: January 2024
:Manual section: 8
:Manual group: System Manager's Manual
:Version: $VERSION$
Expand All @@ -20,7 +20,7 @@ DESCRIPTION
===========

**bluealsa** is a Linux daemon to give applications access to Bluetooth audio
streams using the Bluetooth A2DP, HFP and/or HSP profiles.
streams using the Bluetooth A2DP, HFP, HSP and/or BLE-MIDI profiles.
It provides a D-Bus API to applications, and can be used by ALSA applications
via libasound plugins (see **bluealsa-plugins(7)** for details).

Expand Down Expand Up @@ -246,6 +246,9 @@ OPTIONS
- **standard** - standard quality (44.1 kHz: 606 kbps, 48 kHz: 660 kbps)
- **high** - high quality (44.1 kHz: 909 kbps, 48 kHz: 990 kbps)

--midi-advertisement
Advertise BLE-MIDI service using Bluetooth LE advertising.

--xapl-resp-name=NAME
Set the product name send in the XAPL response message.
By default, the name is set as "BlueALSA".
Expand All @@ -260,10 +263,12 @@ Profiles
--------

**bluealsa** provides support for Bluetooth Advanced Audio Distribution Profile
(A2DP), Hands-Free Profile (HFP) and Headset Profile (HSP).
(A2DP), Hands-Free Profile (HFP), Headset Profile (HSP) and Bluetooth Low
Energy MIDI (BLE-MIDI).
A2DP profile is dedicated for streaming music (i.e., stereo, 48 kHz or more
sampling frequency), while HFP and HSP for two-way voice transmission (mono, 8
kHz or 16 kHz sampling frequency).
kHz or 16 kHz sampling frequency). BLE-MIDI, on the other hand, is used for
transmitting MIDI messages over Bluetooth LE.

The Bluetooth audio profiles are not peer-to-peer; they each have a source or
gateway role (a2dp-source, hfp-ag, or hsp-ag) and a sink or target role
Expand Down Expand Up @@ -314,13 +319,15 @@ The list of profile *NAME*-s accepted by the ``--profile=NAME`` option:
- **hfp-hf** - Hands-Free
- **hsp-ag** Headset Audio Gateway
- **hsp-hs** - Headset
- **midi** - Bluetooth LE MIDI

The **hfp-ofono** is available only when **bluealsa** was compiled with oFono
support. Enabling HFP over oFono will automatically disable **hfp-hf** and
**hfp-ag**.
The **hfp-ofono** and **midi** profiles are available only when **bluealsa**
was compiled respectively with oFono and BLE-MIDI support.

BlueZ permits only one service to register the HSP and HFP profiles, and that
service is automatically registered with every HCI device.
Enabling HFP over oFono will automatically disable **hfp-hf** and **hfp-ag**.
Also, it is important to note that BlueZ permits only one service to register
the HFP profile, and that service is automatically registered with every HCI
device.

For the A2DP profile, BlueZ allows each HCI device to be registered to a
different service, so it is possible to have multiple instances of
Expand Down Expand Up @@ -452,7 +459,7 @@ Please add following lines to the BlueALSA D-Bus policy:
COPYRIGHT
=========

Copyright (c) 2016-2023 Arkadiusz Bokowy.
Copyright (c) 2016-2024 Arkadiusz Bokowy.

The bluez-alsa project is licensed under the terms of the MIT license.

Expand Down
9 changes: 8 additions & 1 deletion src/bluealsa-config.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* BlueALSA - bluealsa-config.h
* Copyright (c) 2016-2023 Arkadiusz Bokowy
* Copyright (c) 2016-2024 Arkadiusz Bokowy
*
* This file is a part of bluez-alsa.
*
Expand Down Expand Up @@ -128,6 +128,13 @@ struct ba_config {

} a2dp;

#if ENABLE_MIDI
struct {
/* advertise BLE-MIDI via LE advertisement */
bool advertise;
} midi;
#endif

/* BlueALSA supports 5 SBC qualities: low, medium, high, XQ and XQ+. The XQ
* mode uses 44.1 kHz sampling rate, dual channel mode with bitpool 38, 16
* blocks in frame, 8 frequency bands and allocation method Loudness, which
Expand Down
10 changes: 9 additions & 1 deletion src/bluez-iface.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* BlueALSA - bluez-iface.h
* Copyright (c) 2016-2023 Arkadiusz Bokowy
* Copyright (c) 2016-2024 Arkadiusz Bokowy
*
* This file is a part of bluez-alsa.
*
Expand Down Expand Up @@ -69,6 +69,14 @@ OrgBluezGattService1Skeleton *org_bluez_gatt_service1_skeleton_new(
const GDBusInterfaceSkeletonVTable *vtable, void *userdata,
GDestroyNotify userdata_free_func);

typedef struct {
GDBusInterfaceSkeletonEx parent;
} OrgBluezLeadvertisement1Skeleton;

OrgBluezLeadvertisement1Skeleton *org_bluez_leadvertisement1_skeleton_new(
const GDBusInterfaceSkeletonVTable *vtable, void *userdata,
GDestroyNotify userdata_free_func);

#endif

typedef struct {
Expand Down
12 changes: 11 additions & 1 deletion src/bluez-iface.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!-- BlueALSA - bluez-iface.xml
Copyright (c) 2016-2023 Arkadiusz Bokowy
Copyright (c) 2016-2024 Arkadiusz Bokowy
This file is a part of bluez-alsa.
Expand Down Expand Up @@ -40,6 +40,16 @@
<property name="Primary" type="b" access="read"/>
</interface>

<interface name="org.bluez.LEAdvertisement1">
<annotation name="org.gtk.GDBus.CPP.if" value="ENABLE_MIDI"/>
<method name="Release">
</method>
<property name="Type" type="s" access="read"/>
<property name="ServiceUUIDs" type="as" access="read"/>
<property name="Discoverable" type="b" access="read"/>
<property name="Includes" type="as" access="read"/>
</interface>

<interface name="org.bluez.MediaEndpoint1">
<method name="SelectConfiguration">
<arg direction="in" type="ay" name="capabilities"/>
Expand Down
148 changes: 125 additions & 23 deletions src/bluez-midi.c
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
struct bluez_midi_app {
/* D-Bus object registration paths */
char path[64];
char path_adv[64 + 8];
char path_service[64 + 8];
char path_char[64 + 16];
/* associated adapter */
Expand All @@ -69,14 +70,6 @@ struct bluez_midi_app {
* D-Bus unique name of the BlueZ daemon. */
static char bluez_dbus_unique_name[64] = "";

static GVariant *variant_new_midi_service_uuid(void) {
return g_variant_new_string(BT_UUID_MIDI);
}

static GVariant *variant_new_midi_characteristic_uuid(void) {
return g_variant_new_string(BT_UUID_MIDI_CHAR);
}

static struct bluez_midi_app *bluez_midi_app_ref(struct bluez_midi_app *app) {
atomic_fetch_add_explicit(&app->ref_count, 1, memory_order_relaxed);
return app;
Expand Down Expand Up @@ -128,12 +121,69 @@ static struct ba_transport *bluez_midi_transport_new(
return t;
}

static void bluez_midi_advertisement_release(
GDBusMethodInvocation *inv, G_GNUC_UNUSED void *userdata) {
debug("Releasing MIDI LE advertisement: %s", ((struct bluez_midi_app *)userdata)->path);
g_object_unref(inv);
}

static GVariant *bluez_midi_advertisement_iface_get_property(
const char *property, G_GNUC_UNUSED GError **error, void *userdata) {
(void)userdata;

if (strcmp(property, "Type") == 0)
return g_variant_new_string("peripheral");
if (strcmp(property, "ServiceUUIDs") == 0) {
static const char *uuids[] = { BT_UUID_MIDI };
return g_variant_new_strv(uuids, ARRAYSIZE(uuids));
}
if (strcmp(property, "Discoverable") == 0)
/* advertise as general discoverable LE-only device */
return g_variant_new_boolean(TRUE);
if (strcmp(property, "Includes") == 0) {
const char *values[] = { "local-name" };
return g_variant_new_strv(values, ARRAYSIZE(values));
}

g_assert_not_reached();
return NULL;
}

static GDBusObjectSkeleton *bluez_midi_advertisement_skeleton_new(
struct bluez_midi_app *app) {

static const GDBusMethodCallDispatcher dispatchers[] = {
{ .method = "Release",
.sender = bluez_dbus_unique_name,
.handler = bluez_midi_advertisement_release },
{ 0 },
};

static const GDBusInterfaceSkeletonVTable vtable = {
.dispatchers = dispatchers,
.get_property = bluez_midi_advertisement_iface_get_property,
};

OrgBluezLeadvertisement1Skeleton *ifs_gatt_adv;
if ((ifs_gatt_adv = org_bluez_leadvertisement1_skeleton_new(&vtable,
app, (GDestroyNotify)bluez_midi_app_unref)) == NULL)
return NULL;

GDBusInterfaceSkeleton *ifs = G_DBUS_INTERFACE_SKELETON(ifs_gatt_adv);
GDBusObjectSkeleton *skeleton = g_dbus_object_skeleton_new(app->path_adv);
g_dbus_object_skeleton_add_interface(skeleton, ifs);
g_object_unref(ifs_gatt_adv);

bluez_midi_app_ref(app);
return skeleton;
}

static GVariant *bluez_midi_service_iface_get_property(
const char *property, G_GNUC_UNUSED GError **error,
G_GNUC_UNUSED void *userdata) {

if (strcmp(property, "UUID") == 0)
return variant_new_midi_service_uuid();
return g_variant_new_string(BT_UUID_MIDI);
if (strcmp(property, "Primary") == 0)
return g_variant_new_boolean(TRUE);

Expand Down Expand Up @@ -198,7 +248,7 @@ static void bluez_midi_characteristic_acquire_write(
}

int fds[2];
if (socketpair(AF_LOCAL, SOCK_SEQPACKET | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, fds) == -1) {
if (socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, fds) == -1) {
error("Couldn't create BLE-MIDI char write socket pair: %s", strerror(errno));
goto fail;
}
Expand Down Expand Up @@ -258,7 +308,7 @@ static void bluez_midi_characteristic_acquire_notify(
}

int fds[2];
if (socketpair(AF_LOCAL, SOCK_SEQPACKET | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, fds) == -1) {
if (socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, fds) == -1) {
error("Couldn't create BLE-MIDI char notify socket pair: %s", strerror(errno));
goto fail;
}
Expand Down Expand Up @@ -294,7 +344,7 @@ static GVariant *bluez_midi_characteristic_iface_get_property(
struct bluez_midi_app *app = userdata;

if (strcmp(property, "UUID") == 0)
return variant_new_midi_characteristic_uuid();
return g_variant_new_string(BT_UUID_MIDI_CHAR);
if (strcmp(property, "Service") == 0)
return g_variant_new_object_path(app->path_service);
if (strcmp(property, "WriteAcquired") == 0)
Expand Down Expand Up @@ -371,6 +421,59 @@ static void bluez_midi_app_register_finish(

}

static void bluez_midi_app_register(
struct ba_adapter *adapter, struct bluez_midi_app *app) {

GDBusMessage *msg;
msg = g_dbus_message_new_method_call(BLUEZ_SERVICE, adapter->bluez_dbus_path,
BLUEZ_IFACE_GATT_MANAGER, "RegisterApplication");

g_dbus_message_set_body(msg, g_variant_new("(oa{sv})", app->path, NULL));

debug("Registering MIDI GATT application: %s", app->path);
g_dbus_connection_send_message_with_reply(config.dbus, msg,
G_DBUS_SEND_MESSAGE_FLAGS_NONE, -1, NULL, NULL,
bluez_midi_app_register_finish, NULL);

g_object_unref(msg);
}

static void bluez_midi_app_advertise_finish(
GObject *source, GAsyncResult *result, G_GNUC_UNUSED void *userdata) {

GError *err = NULL;
GDBusMessage *rep;

if ((rep = g_dbus_connection_send_message_with_reply_finish(
G_DBUS_CONNECTION(source), result, &err)) != NULL)
g_dbus_message_to_gerror(rep, &err);

if (rep != NULL)
g_object_unref(rep);
if (err != NULL) {
error("Couldn't advertise MIDI GATT application: %s", err->message);
g_error_free(err);
}

}

static void bluez_midi_app_advertise(
struct ba_adapter *adapter, struct bluez_midi_app *app) {

GDBusMessage *msg;
msg = g_dbus_message_new_method_call(BLUEZ_SERVICE, adapter->bluez_dbus_path,
BLUEZ_IFACE_LE_ADVERTISING_MANAGER, "RegisterAdvertisement");

g_dbus_message_set_body(msg, g_variant_new("(oa{sv})", app->path_adv, NULL));

debug("Registering MIDI LE advertisement: %s", app->path);
g_dbus_connection_send_message_with_reply(config.dbus, msg,
G_DBUS_SEND_MESSAGE_FLAGS_NONE, -1, NULL, NULL,
bluez_midi_app_advertise_finish, NULL);

g_object_unref(msg);
}

GDBusObjectManagerServer *bluez_midi_app_new(
struct ba_adapter *adapter, const char *path) {

Expand All @@ -379,6 +482,7 @@ GDBusObjectManagerServer *bluez_midi_app_new(
return NULL;

snprintf(app->path, sizeof(app->path), "%s", path);
snprintf(app->path_adv, sizeof(app->path_adv), "%s/adv", path);
snprintf(app->path_service, sizeof(app->path_service), "%s/service", path);
snprintf(app->path_char, sizeof(app->path_char), "%s/char", app->path_service);
app->hci_dev_id = adapter->hci.dev_id;
Expand All @@ -404,19 +508,17 @@ GDBusObjectManagerServer *bluez_midi_app_new(
g_dbus_object_manager_server_export(manager, skeleton);
g_object_unref(skeleton);

g_dbus_object_manager_server_set_connection(manager, config.dbus);

GDBusMessage *msg;
msg = g_dbus_message_new_method_call(BLUEZ_SERVICE, adapter->bluez_dbus_path,
BLUEZ_IFACE_GATT_MANAGER, "RegisterApplication");
if (config.midi.advertise) {
skeleton = bluez_midi_advertisement_skeleton_new(app);
g_dbus_object_manager_server_export(manager, skeleton);
g_object_unref(skeleton);
}

g_dbus_message_set_body(msg, g_variant_new("(oa{sv})", path, NULL));
g_dbus_object_manager_server_set_connection(manager, config.dbus);

debug("Registering MIDI GATT application: %s", app->path);
g_dbus_connection_send_message_with_reply(config.dbus, msg,
G_DBUS_SEND_MESSAGE_FLAGS_NONE, -1, NULL, NULL,
bluez_midi_app_register_finish, NULL);
bluez_midi_app_register(adapter, app);
if (config.midi.advertise)
bluez_midi_app_advertise(adapter, app);

g_object_unref(msg);
return manager;
}
12 changes: 12 additions & 0 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ int main(int argc, char **argv) {
#if ENABLE_MP3LAME
{ "mp3-algorithm", required_argument, NULL, 12 },
{ "mp3-vbr-quality", required_argument, NULL, 13 },
#endif
#if ENABLE_MIDI
{ "midi-advertisement", no_argument, NULL, 22 },
#endif
{ "xapl-resp-name", required_argument, NULL, 16 },
{ 0, 0, 0, 0 },
Expand Down Expand Up @@ -229,6 +232,9 @@ int main(int argc, char **argv) {
#if ENABLE_MP3LAME
" --mp3-algorithm=TYPE\t\tselect LAME encoder algorithm type\n"
" --mp3-vbr-quality=MODE\tset LAME encoder VBR quality mode\n"
#endif
#if ENABLE_MIDI
" --midi-advertisement\t\tenable LE advertisement for BLE-MIDI\n"
#endif
" --xapl-resp-name=NAME\t\tset product name used by XAPL\n"
"\nAvailable BT profiles:\n"
Expand Down Expand Up @@ -532,6 +538,12 @@ int main(int argc, char **argv) {
}
#endif

#if ENABLE_MIDI
case 22 /* --midi-advertisement */ :
config.midi.advertise = true;
break;
#endif

case 16 /* --xapl-resp-name=NAME */ :
config.hfp.xapl_product_name = optarg;
break;
Expand Down
Loading

0 comments on commit 5ca058a

Please sign in to comment.